mcap/modules/Screen_Record/wgc_source.jai
Musa Mahmood 9953ba6f1f [0.1.4] Video capture phase 2: Windows.Graphics.Capture, for when Desktop Duplication cannot run
- modules/Screen_Record/wgc_source.jai: the screen through Windows.Graphics.Capture, with
  hand-written bindings for its WinRT interfaces (capture item interop, item, free-threaded frame
  pool, frame, session 1-3, IDirect3DDxgiInterfaceAccess, IClosable). A small COM handler on
  FrameArrived sets an event per frame; when Windows refuses it the pool is polled each
  millisecond. The cursor is drawn by Windows; the yellow border is turned off where the system
  allows (Windows 11). A screen that changes size makes the pool again. WinRT's own calls are
  loaded from combase.dll at run time: runtimeobject.lib names no DLL for #library.
- capture_gpu.jai: what both sources share (the device on the screen's adapter, the region copy,
  the staging ring), so each is only "the next image's region into a texture".
- Record_Options.capture: AUTO (Desktop Duplication, else Windows.Graphics.Capture when it says
  fall_back), DXGI or WGC. Sources open on the capture thread, as WinRT objects belong to their
  apartment; record_start waits for it to say how that went. Record_Options.testing simulates the
  two-GPU failure, forces polling, or keeps the border, for record-probe.
- record-probe: -capture auto|dxgi|wgc, -simulate-unsupported, -poll, -border-test with
  -keep-border as its control, -cursor-test; DPI awareness set already is no longer reported as
  a failure.
- On the RTX 5080 laptop: auto falls back on the simulated failure; WGC records 4K at 60 fps with
  nothing dropped; its cursor and the one cursor.jai draws for duplication are the same; the
  border is off for an unpackaged app (0 yellow edge pixels, 14,214 with it kept);
  WDA_EXCLUDEFROMCAPTURE is respected; polling holds 30 fps. The plan records the results.
- README: Screen_Record's line, which held a carriage return in place of "\r" since 0.1.3.
2026-10-10 07:03:47 +01:00

407 lines
18 KiB
Plaintext

// Windows.Graphics.Capture: the screen as Windows' own capture API hands it over, for when Desktop
// Duplication cannot run (dxgi_source.jai). It works across GPUs, hands over frames the right way up
// on a rotated screen, and draws the cursor in itself.
//
// It is WinRT, bound here by hand: the few interfaces it needs, as vtables after IInspectable's six
// methods. Everything is made and used on the capture thread, which is in the multithreaded apartment
// (RoInitialize), with a free-threaded frame pool, so no dispatcher queue or message loop is needed.
//
// A frame's arrival sets an event, through a small handler object (Frame_Handler); if Windows will not
// take the handler, the pool is polled every millisecond instead. Windows 11 lets the yellow border it
// draws around a screen being captured be turned off (IsBorderRequired); Windows 10 always draws it.
Wgc_Source :: struct {
#as using gpu: Capture_Gpu;
screen_size: Size_Int32; // the frames' size, as the pool was made for
item: *Capture_Item;
winrt_device: *IInspectable; // IDirect3DDevice, over gpu.device
pool: *Frame_Pool;
session: *Capture_Session;
handler: Frame_Handler;
handler_token: s64;
polling: bool; // Windows would not take the handler
border: string; // what came of turning the border off, for the log
}
// Starts capturing the screen. region is in the screen's pixels and lies on it.
wgc_open :: (s: *Wgc_Source, screen: Record_Screen, region: RECT, cursor: bool, testing: Record_Testing = 0) -> ok: bool, error: string {
ok, error := gpu_open(s, screen, region);
if !ok return false, error;
session_statics: *Session_Statics;
hr := activation_factory("Windows.Graphics.Capture.GraphicsCaptureSession", IID_SESSION_STATICS, xx *session_statics);
if FAILED(hr) return false, hresult_text("Windows.Graphics.Capture is not on this system", hr);
supported: u8;
session_statics.vtable.IsSupported(session_statics, *supported);
com_release(*session_statics);
if !supported return false, "Windows.Graphics.Capture is not supported here";
interop: *Capture_Item_Interop;
hr = activation_factory("Windows.Graphics.Capture.GraphicsCaptureItem", IID_ITEM_INTEROP, xx *interop);
if FAILED(hr) return false, hresult_text("IGraphicsCaptureItemInterop", hr);
hr = interop.vtable.CreateForMonitor(interop, screen.monitor, *IID_ITEM, xx *s.item);
com_release(*interop);
if FAILED(hr) return false, hresult_text("a capture item for the screen", hr);
s.item.vtable.get_Size(s.item, *s.screen_size);
desktop_width := screen.desktop.right - screen.desktop.left;
desktop_height := screen.desktop.bottom - screen.desktop.top;
if s.screen_size.width != desktop_width || s.screen_size.height != desktop_height {
return false, tprint("the screen is % x %, but this process sees it as % x %: it must be aware of each monitor's DPI",
s.screen_size.width, s.screen_size.height, desktop_width, desktop_height);
}
dxgi_device: *IDXGIDevice;
hr = IUnknown_QueryInterface(s.device, *uid(IDXGIDevice_UUID), xx *dxgi_device);
if FAILED(hr) return false, hresult_text("the device's IDXGIDevice", hr);
inspectable: *IInspectable;
hr = CreateDirect3D11DeviceFromDXGIDevice(dxgi_device, *inspectable);
com_release(*dxgi_device);
if FAILED(hr) return false, hresult_text("CreateDirect3D11DeviceFromDXGIDevice", hr);
hr = inspectable.vtable.QueryInterface(inspectable, *IID_DIRECT3D_DEVICE, xx *s.winrt_device);
com_release(*inspectable);
if FAILED(hr) return false, hresult_text("the device as IDirect3DDevice", hr);
pool_statics: *Frame_Pool_Statics2;
hr = activation_factory("Windows.Graphics.Capture.Direct3D11CaptureFramePool", IID_POOL_STATICS2, xx *pool_statics);
if FAILED(hr) return false, hresult_text("Direct3D11CaptureFramePool's statics", hr);
hr = pool_statics.vtable.CreateFreeThreaded(pool_statics, s.winrt_device, PIXEL_FORMAT_B8G8R8A8, POOL_BUFFERS, size_packed(s.screen_size), *s.pool);
com_release(*pool_statics);
if FAILED(hr) return false, hresult_text("a free-threaded frame pool", hr);
hr = s.pool.vtable.CreateCaptureSession(s.pool, s.item, *s.session);
if FAILED(hr) return false, hresult_text("a capture session", hr);
// The cursor (Windows 10 2004 on) and the border (Windows 11): optional, by what the system has.
session2: *Capture_Session2;
if SUCCEEDED(s.session.vtable.QueryInterface(s.session, *IID_SESSION2, xx *session2)) {
session2.vtable.put_IsCursorCaptureEnabled(session2, xx cursor);
com_release(*session2);
}
session3: *Capture_Session3;
if testing & .KEEP_BORDER {
s.border = "border left on";
} else if SUCCEEDED(s.session.vtable.QueryInterface(s.session, *IID_SESSION3, xx *session3)) {
hr = session3.vtable.put_IsBorderRequired(session3, 0);
s.border = ifx SUCCEEDED(hr) then "border off" else hresult_text("the border could not be turned off", hr);
com_release(*session3);
} else {
s.border = "border always drawn (no IGraphicsCaptureSession3: Windows 10)";
}
s.handler.vtable = *frame_handler_vtable;
s.handler.event = CreateEventW(null, 0, 0, null);
hr = ifx testing & .WGC_POLL then cast,no_check(HRESULT) 0x80004002 else s.pool.vtable.add_FrameArrived(s.pool, *s.handler, *s.handler_token);
if FAILED(hr) {
s.polling = true;
timeBeginPeriod(1); // so polling every millisecond is every millisecond
log("[record] FrameArrived would not take the handler (%); polling the frame pool.", hresult_text("add_FrameArrived", hr));
}
hr = s.session.vtable.StartCapture(s.session);
if FAILED(hr) return false, hresult_text("StartCapture", hr);
return true, "";
}
// Waits up to timeout_ms for a new frame; the newest's region goes into s.latest.
wgc_next :: (s: *Wgc_Source, timeout_ms: u32) -> Capture_Next, error: string {
frame := wgc_take_newest(s);
if !frame {
if s.polling {
start := ticks_now();
frequency: LARGE_INTEGER;
QueryPerformanceFrequency(*frequency);
while !frame && (ticks_now() - start) * 1000 / frequency.QuadPart < timeout_ms {
sleep_milliseconds(1);
frame = wgc_take_newest(s);
}
} else {
WaitForSingleObject(s.handler.event, timeout_ms);
frame = wgc_take_newest(s);
}
}
if !frame return .NONE, "";
defer close_and_release(frame);
// The screen changed size (a new resolution): the pool is made again; the region must still fit.
size: Size_Int32;
frame.vtable.get_ContentSize(frame, *size);
if size.width != s.screen_size.width || size.height != s.screen_size.height {
s.screen_size = size;
hr := s.pool.vtable.Recreate(s.pool, s.winrt_device, PIXEL_FORMAT_B8G8R8A8, POOL_BUFFERS, size_packed(size));
if FAILED(hr) return .FAILED, hresult_text("the frame pool could not be made again", hr);
if cast(s32) s.box.right > size.width || cast(s32) s.box.bottom > size.height
return .FAILED, tprint("the screen is now % x %, and the region is not on it", size.width, size.height);
return .NONE, "";
}
surface: *IInspectable;
hr := frame.vtable.get_Surface(frame, *surface);
if FAILED(hr) return .FAILED, hresult_text("the frame's surface", hr);
defer com_release(*surface);
access: *Dxgi_Interface_Access;
hr = surface.vtable.QueryInterface(surface, *IID_DXGI_INTERFACE_ACCESS, xx *access);
if FAILED(hr) return .FAILED, hresult_text("the surface's IDirect3DDxgiInterfaceAccess", hr);
defer com_release(*access);
texture: *ID3D11Texture2D;
hr = access.vtable.GetInterface(access, *uid(ID3D11Texture2D_UUID), xx *texture);
if FAILED(hr) return .FAILED, hresult_text("the surface's texture", hr);
gpu_copy_region(s, texture);
IUnknown_Release(texture);
return .IMAGE, "";
}
wgc_close :: (s: *Wgc_Source) {
if s.pool && s.handler_token s.pool.vtable.remove_FrameArrived(s.pool, s.handler_token);
if s.session close_and_release(s.session);
if s.pool close_and_release(s.pool);
s.session, s.pool = null, null;
com_release(*s.item);
com_release(*s.winrt_device);
if s.handler.event CloseHandle(s.handler.event);
if s.polling timeEndPeriod(1);
gpu_close(s);
s.* = .{};
}
// The thread's apartment: WinRT on this thread from here on.
wgc_thread_start :: () -> ok: bool {
if !winrt_load() return false;
return SUCCEEDED(winrt.RoInitialize(RO_INIT_MULTITHREADED));
}
wgc_thread_end :: () {
if winrt.RoUninitialize winrt.RoUninitialize();
}
#scope_file
POOL_BUFFERS :: 2;
PIXEL_FORMAT_B8G8R8A8 :: 87; // DirectXPixelFormat.B8G8R8A8UIntNormalized
RO_INIT_MULTITHREADED :: 1;
// The pool's newest frame, the older ones let go; null when there is none.
wgc_take_newest :: (s: *Wgc_Source) -> *Capture_Frame {
newest: *Capture_Frame;
while true {
frame: *Capture_Frame;
if FAILED(s.pool.vtable.TryGetNextFrame(s.pool, *frame)) || !frame break;
if newest close_and_release(newest);
newest = frame;
}
return newest;
}
// IClosable.Close, which hands a frame back to the pool at once, then Release.
close_and_release :: (object: *$T) {
closable: *Closable;
if SUCCEEDED(object.vtable.QueryInterface(object, *IID_CLOSABLE, xx *closable)) {
closable.vtable.Close(closable);
closable.vtable.Release(closable);
}
object.vtable.Release(object);
}
activation_factory :: (class_name: string, iid: GUID, factory: **void) -> HRESULT {
wide := utf8_to_wide(class_name,, temp);
length: u32;
while wide[length] length += 1;
name: HSTRING;
if !winrt.loaded return cast,no_check(HRESULT) 0x80004001; // E_NOTIMPL: no WinRT on this system
hr := winrt.WindowsCreateString(wide, length, *name);
if FAILED(hr) return hr;
defer winrt.WindowsDeleteString(name);
iid_copy := iid;
return winrt.RoGetActivationFactory(name, *iid_copy, factory);
}
size_packed :: (size: Size_Int32) -> u64 { // SizeInt32 by value: 8 bytes, in one register
width := cast(u64) cast,no_check(u32) size.width;
height := cast(u64) cast,no_check(u32) size.height;
return width | (height << 32);
}
// ---------------------------------------------------------------- the FrameArrived handler
// TypedEventHandler<Direct3D11CaptureFramePool, Object>: a COM object of our own whose Invoke sets an
// event. It lives in the Wgc_Source and is never freed by its count; it is unregistered first.
Frame_Handler :: struct {
vtable: *Frame_Handler_VTable;
event: HANDLE;
}
Frame_Handler_VTable :: struct {
QueryInterface: (this: *Frame_Handler, iid: *GUID, out: **void) -> HRESULT #c_call;
AddRef: (this: *Frame_Handler) -> u32 #c_call;
Release: (this: *Frame_Handler) -> u32 #c_call;
Invoke: (this: *Frame_Handler, sender: *void, args: *void) -> HRESULT #c_call;
}
frame_handler_vtable := Frame_Handler_VTable.{handler_query, handler_add_ref, handler_release, handler_invoke};
handler_query :: (this: *Frame_Handler, iid: *GUID, out: **void) -> HRESULT #c_call {
if guid_equal(iid, *IID_UNKNOWN) || guid_equal(iid, *IID_AGILE_OBJECT) || guid_equal(iid, *IID_FRAME_ARRIVED_HANDLER) {
out.* = this;
return 0;
}
out.* = null;
return cast,no_check(HRESULT) 0x80004002; // E_NOINTERFACE
}
handler_add_ref :: (this: *Frame_Handler) -> u32 #c_call { return 1; }
handler_release :: (this: *Frame_Handler) -> u32 #c_call { return 1; }
handler_invoke :: (this: *Frame_Handler, sender: *void, args: *void) -> HRESULT #c_call {
SetEvent(this.event);
return 0;
}
guid_equal :: (a: *GUID, b: *GUID) -> bool #c_call {
if a.Data1 != b.Data1 || a.Data2 != b.Data2 || a.Data3 != b.Data3 return false;
for 0..7 if a.Data4[it] != b.Data4[it] return false;
return true;
}
// ---------------------------------------------------------------- bindings
d3d11_dll :: #library,system "d3d11";
kernel32_dll :: #library,system "kernel32";
HSTRING :: *void;
// WinRT's own calls, from combase.dll at run time: its import library is runtimeobject.lib, which
// names no DLL of its own for #library to find.
winrt: struct {
loaded: bool;
RoInitialize: (init_type: s32) -> HRESULT #c_call;
RoUninitialize: () #c_call;
RoGetActivationFactory: (class_id: HSTRING, iid: *GUID, factory: **void) -> HRESULT #c_call;
WindowsCreateString: (source: *u16, length: u32, out: *HSTRING) -> HRESULT #c_call;
WindowsDeleteString: (text: HSTRING) -> HRESULT #c_call;
}
winrt_load :: () -> bool {
if winrt.loaded return true;
library := LoadLibraryW(utf8_to_wide("combase.dll",, temp));
if !library return false;
winrt.RoInitialize = xx GetProcAddress(library, "RoInitialize");
winrt.RoUninitialize = xx GetProcAddress(library, "RoUninitialize");
winrt.RoGetActivationFactory = xx GetProcAddress(library, "RoGetActivationFactory");
winrt.WindowsCreateString = xx GetProcAddress(library, "WindowsCreateString");
winrt.WindowsDeleteString = xx GetProcAddress(library, "WindowsDeleteString");
winrt.loaded = winrt.RoInitialize && winrt.RoUninitialize && winrt.RoGetActivationFactory && winrt.WindowsCreateString && winrt.WindowsDeleteString;
return winrt.loaded;
}
CreateDirect3D11DeviceFromDXGIDevice :: (device: *IDXGIDevice, out: **IInspectable) -> HRESULT #foreign d3d11_dll;
SetEvent :: (event: HANDLE) -> s32 #foreign kernel32_dll;
Size_Int32 :: struct { width, height: s32; }
IID_UNKNOWN := #run guid("00000000-0000-0000-C000-000000000046");
IID_AGILE_OBJECT := #run guid("94ea2b94-e9cc-49e0-c0ff-ee64ca8f5b90");
IID_FRAME_ARRIVED_HANDLER := #run guid("51a947f7-79cf-5a3e-a3a5-1289cfa6dfe8"); // TypedEventHandler<Direct3D11CaptureFramePool, Object>
IID_ITEM_INTEROP := #run guid("3628E81B-3CAC-4C60-B7F4-23CE0E0C3356");
IID_ITEM := #run guid("79C3F95B-31F7-4EC2-A464-632EF5D30760");
IID_SESSION_STATICS := #run guid("2224A540-5974-49AA-B232-0882536F4CB5");
IID_SESSION2 := #run guid("2C39AE40-7D2E-5044-804E-8B6799D4CF9E");
IID_SESSION3 := #run guid("F2CDD966-22AE-5EA1-9596-3A289344C3BE");
IID_POOL_STATICS2 := #run guid("589B103F-6BBC-5DF5-A991-02E28B3B66D5");
IID_DIRECT3D_DEVICE := #run guid("A37624AB-8D5F-4650-9D3E-9EAE3D9BC670");
IID_DXGI_INTERFACE_ACCESS := #run guid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1");
IID_CLOSABLE := #run guid("30D5A829-7FA4-4026-83BB-D75BAE4EA99E");
guid :: (text: string) -> GUID {
hex :: (s: string) -> u64 {
value: u64;
for 0..s.count - 1 {
c := s[it];
digit: u64 = ifx c >= #char "a" then cast(u64) (c - #char "a" + 10) else ifx c >= #char "A" then cast(u64) (c - #char "A" + 10) else cast(u64) (c - #char "0");
value = value * 16 + digit;
}
return value;
}
g: GUID;
g.Data1 = cast(u32) hex(slice(text, 0, 8));
g.Data2 = cast(u16) hex(slice(text, 9, 4));
g.Data3 = cast(u16) hex(slice(text, 14, 4));
for 0..1 g.Data4[it] = cast(u8) hex(slice(text, 19 + it * 2, 2));
for 0..5 g.Data4[it + 2] = cast(u8) hex(slice(text, 24 + it * 2, 2));
return g;
}
// IUnknown, then IInspectable's three: every WinRT interface starts so.
Inspectable_Methods :: struct {
QueryInterface: (this: *void, iid: *GUID, out: **void) -> HRESULT #c_call;
AddRef: (this: *void) -> u32 #c_call;
Release: (this: *void) -> u32 #c_call;
GetIids: *void;
GetRuntimeClassName: *void;
GetTrustLevel: *void;
}
IInspectable :: struct { vtable: *Inspectable_Methods; }
Closable :: struct { vtable: *struct { using base: Inspectable_Methods; Close: (this: *Closable) -> HRESULT #c_call; }; }
Session_Statics :: struct { vtable: *struct { using base: Inspectable_Methods; IsSupported: (this: *Session_Statics, out: *u8) -> HRESULT #c_call; }; }
Capture_Item_Interop :: struct { vtable: *struct {
QueryInterface: (this: *void, iid: *GUID, out: **void) -> HRESULT #c_call;
AddRef: (this: *void) -> u32 #c_call;
Release: (this: *void) -> u32 #c_call;
CreateForWindow: (this: *Capture_Item_Interop, window: HWND, iid: *GUID, out: **void) -> HRESULT #c_call;
CreateForMonitor: (this: *Capture_Item_Interop, monitor: HMONITOR, iid: *GUID, out: **void) -> HRESULT #c_call;
}; }
Capture_Item :: struct { vtable: *struct {
using base: Inspectable_Methods;
get_DisplayName: *void;
get_Size: (this: *Capture_Item, out: *Size_Int32) -> HRESULT #c_call;
add_Closed: *void;
remove_Closed: *void;
}; }
Frame_Pool_Statics2 :: struct { vtable: *struct {
using base: Inspectable_Methods;
CreateFreeThreaded: (this: *Frame_Pool_Statics2, device: *IInspectable, format: s32, buffers: s32, size: u64, out: **Frame_Pool) -> HRESULT #c_call;
}; }
Frame_Pool :: struct { vtable: *struct {
using base: Inspectable_Methods;
Recreate: (this: *Frame_Pool, device: *IInspectable, format: s32, buffers: s32, size: u64) -> HRESULT #c_call;
TryGetNextFrame: (this: *Frame_Pool, out: **Capture_Frame) -> HRESULT #c_call;
add_FrameArrived: (this: *Frame_Pool, handler: *Frame_Handler, token: *s64) -> HRESULT #c_call;
remove_FrameArrived: (this: *Frame_Pool, token: s64) -> HRESULT #c_call;
CreateCaptureSession: (this: *Frame_Pool, item: *Capture_Item, out: **Capture_Session) -> HRESULT #c_call;
get_DispatcherQueue: *void;
}; }
Capture_Frame :: struct { vtable: *struct {
using base: Inspectable_Methods;
get_Surface: (this: *Capture_Frame, out: **IInspectable) -> HRESULT #c_call;
get_SystemRelativeTime: (this: *Capture_Frame, out: *s64) -> HRESULT #c_call;
get_ContentSize: (this: *Capture_Frame, out: *Size_Int32) -> HRESULT #c_call;
}; }
Capture_Session :: struct { vtable: *struct {
using base: Inspectable_Methods;
StartCapture: (this: *Capture_Session) -> HRESULT #c_call;
}; }
Capture_Session2 :: struct { vtable: *struct {
using base: Inspectable_Methods;
get_IsCursorCaptureEnabled: (this: *Capture_Session2, out: *u8) -> HRESULT #c_call;
put_IsCursorCaptureEnabled: (this: *Capture_Session2, value: u8) -> HRESULT #c_call;
}; }
Capture_Session3 :: struct { vtable: *struct {
using base: Inspectable_Methods;
get_IsBorderRequired: (this: *Capture_Session3, out: *u8) -> HRESULT #c_call;
put_IsBorderRequired: (this: *Capture_Session3, value: u8) -> HRESULT #c_call;
}; }
Dxgi_Interface_Access :: struct { vtable: *struct {
QueryInterface: (this: *void, iid: *GUID, out: **void) -> HRESULT #c_call;
AddRef: (this: *void) -> u32 #c_call;
Release: (this: *void) -> u32 #c_call;
GetInterface: (this: *Dxgi_Interface_Access, iid: *GUID, out: **void) -> HRESULT #c_call;
}; }