mcap/modules/Screen_Record/dxgi_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

165 lines
7.4 KiB
Plaintext

// DXGI Desktop Duplication: the screens as the GPU composes them, a frame each time one changes,
// with the cursor apart (cursor.jai draws it in).
//
// It runs on a device made on the adapter the screen is attached to (capture_gpu.jai): duplicating
// with a device on another adapter fails. On a laptop with two GPUs, where the screen hangs off one
// and the process may run on the other, DuplicateOutput1 can fail with DXGI_ERROR_UNSUPPORTED all the
// same; that, too many duplications of the screen at once, and a rotated screen are what dxgi_open
// returns fall_back for, and Windows.Graphics.Capture (wgc_source.jai) takes over.
//
// DuplicateOutput1 (IDXGIOutput5) is used where there is one: the plain DuplicateOutput is documented
// to fail for a process aware of each monitor's DPI, as mcap is (it did not on an RTX 5080 laptop).
// legacy forces the plain one, for record-probe.
Dxgi_Source :: struct {
#as using gpu: Capture_Gpu;
output: *IDXGIOutput1;
output5: *IDXGIOutput5; // null before Windows 10 1703
duplication: *IDXGIOutputDuplication;
legacy: bool;
method: string; // which duplication call worked, for the log
pointer_position: POINT; // the cursor's top left, in the screen's pixels
pointer_visible: bool;
shape: Cursor_Shape;
}
// Every screen attached to the desktop, on every adapter, in DXGI's order. Temp.
record_list_screens :: () -> [] Record_Screen {
screens: [..] Record_Screen;
screens.allocator = temp;
factory: *IDXGIFactory1;
if FAILED(CreateDXGIFactory1(*uid(IDXGIFactory1_UUID), xx *factory)) return screens;
defer IUnknown_Release(factory);
adapter_index: u32;
while true {
adapter: *IDXGIAdapter1;
if FAILED(IDXGIFactory1_EnumAdapters1(factory, adapter_index, *adapter)) break;
defer IUnknown_Release(adapter);
adapter_desc: DXGI_ADAPTER_DESC1;
IDXGIAdapter1_GetDesc1(adapter, *adapter_desc);
adapter_name := wide_text(adapter_desc.Description.data);
output_index: u32;
while true {
output: *IDXGIOutput;
if FAILED(IDXGIAdapter_EnumOutputs(adapter, output_index, *output)) break;
desc: DXGI_OUTPUT_DESC;
IDXGIOutput_GetDesc(output, *desc);
IUnknown_Release(output);
if desc.AttachedToDesktop != .FALSE {
array_add(*screens, .{adapter_index = adapter_index, output_index = output_index, adapter = adapter_name,
name = wide_text(desc.DeviceName.data), desktop = desc.DesktopCoordinates,
rotation = desc.Rotation, monitor = desc.Monitor});
}
output_index += 1;
}
adapter_index += 1;
}
return screens;
}
// Duplicates the screen. region is in the screen's pixels and lies on it.
dxgi_open :: (s: *Dxgi_Source, screen: Record_Screen, region: RECT, legacy: bool) -> ok: bool, error: string, fall_back: bool {
s.legacy = legacy;
if screen.rotation != .IDENTITY && screen.rotation != .UNSPECIFIED return false, "the screen is rotated", true;
ok, error := gpu_open(s, screen, region);
if !ok return false, error, false;
output: *IDXGIOutput;
hr := IDXGIAdapter_EnumOutputs(s.adapter, screen.output_index, *output);
if FAILED(hr) return false, hresult_text("the screen is gone", hr), false;
defer IUnknown_Release(output);
hr = IUnknown_QueryInterface(output, *uid(IDXGIOutput1_UUID), xx *s.output);
if FAILED(hr) return false, hresult_text("the screen cannot be duplicated (no IDXGIOutput1)", hr), true;
IUnknown_QueryInterface(output, *uid(IDXGIOutput5_UUID), xx *s.output5); // optional
hr = dxgi_duplicate(s);
if FAILED(hr) {
// Unsupported: two GPUs, the device on the wrong one. Not currently available: too many duplications.
fall_back := hresult_is(hr, DXGI_ERROR_UNSUPPORTED) || hresult_is(hr, DXGI_ERROR_NOT_CURRENTLY_AVAILABLE);
return false, hresult_text(tprint("Desktop Duplication (%)", s.method), hr), fall_back;
}
// A process not aware of each monitor's DPI is told the desktop in scaled pixels, while the frames
// come in the screen's own: the region would be the wrong part of the screen.
duplication_desc: DXGI_OUTDUPL_DESC;
IDXGIOutputDuplication_GetDesc(s.duplication, *duplication_desc);
desktop_width := screen.desktop.right - screen.desktop.left;
desktop_height := screen.desktop.bottom - screen.desktop.top;
if duplication_desc.ModeDesc.Width != cast(u32) desktop_width || duplication_desc.ModeDesc.Height != cast(u32) 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",
duplication_desc.ModeDesc.Width, duplication_desc.ModeDesc.Height, desktop_width, desktop_height), false;
}
return true, "", false;
}
// Duplicates the screen, again after the duplication was lost.
dxgi_duplicate :: (s: *Dxgi_Source) -> HRESULT {
com_release(*s.duplication);
if s.output5 && !s.legacy {
s.method = "DuplicateOutput1";
formats := DXGI_FORMAT.[.B8G8R8A8_UNORM];
return IDXGIOutput5_DuplicateOutput1(s.output5, s.device, 0, formats.count, formats.data, *s.duplication);
}
s.method = "DuplicateOutput";
return IDXGIOutput1_DuplicateOutput(s.output, s.device, *s.duplication);
}
// Waits up to timeout_ms for the screen to change. A new image's region goes into s.latest; a moved
// or changed cursor updates s.pointer_* and s.shape. On LOST the duplication is gone: dxgi_duplicate.
dxgi_next :: (s: *Dxgi_Source, timeout_ms: u32) -> Capture_Next, error: string {
info: DXGI_OUTDUPL_FRAME_INFO;
resource: *IDXGIResource;
hr := IDXGIOutputDuplication_AcquireNextFrame(s.duplication, timeout_ms, *info, *resource);
if hresult_is(hr, DXGI_ERROR_WAIT_TIMEOUT) return .NONE, "";
if hresult_is(hr, DXGI_ERROR_ACCESS_LOST) return .LOST, "";
if FAILED(hr) return .FAILED, hresult_text("AcquireNextFrame", hr);
defer IDXGIOutputDuplication_ReleaseFrame(s.duplication); // held, it stalls the desktop
defer IUnknown_Release(resource);
image := info.LastPresentTime.QuadPart != 0;
if image {
texture: *ID3D11Texture2D;
hr = IUnknown_QueryInterface(resource, *uid(ID3D11Texture2D_UUID), xx *texture);
if FAILED(hr) return .FAILED, hresult_text("the frame is not a texture", hr);
gpu_copy_region(s, texture);
IUnknown_Release(texture);
}
pointer := info.LastMouseUpdateTime.QuadPart != 0;
if pointer {
s.pointer_position = info.PointerPosition.Position;
s.pointer_visible = info.PointerPosition.Visible != .FALSE;
if info.PointerShapeBufferSize {
array_resize(*s.shape.data, info.PointerShapeBufferSize, initialize = false);
needed: u32;
shape_info: DXGI_OUTDUPL_POINTER_SHAPE_INFO;
hr = IDXGIOutputDuplication_GetFramePointerShape(s.duplication, cast(u32) s.shape.data.count, s.shape.data.data, *needed, *shape_info);
if SUCCEEDED(hr) {
s.shape.type = cast(DXGI_OUTDUPL_POINTER_SHAPE_TYPE) shape_info.Type;
s.shape.width = cast(s32) shape_info.Width;
s.shape.height = cast(s32) shape_info.Height;
s.shape.pitch = cast(s32) shape_info.Pitch;
} else {
array_reset_keeping_memory(*s.shape.data);
}
}
}
if image && pointer return .IMAGE_AND_POINTER, "";
if image return .IMAGE, "";
if pointer return .POINTER, "";
return .NONE, "";
}
dxgi_close :: (s: *Dxgi_Source) {
com_release(*s.duplication);
com_release(*s.output5);
com_release(*s.output);
gpu_close(s);
array_free(s.shape.data);
s.shape = .{};
}