- 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.
165 lines
7.4 KiB
Plaintext
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 = .{};
|
|
}
|