- modules/Screen_Record: a region of one screen to H.264 in a fragmented MP4. A capture thread duplicates the screen (DuplicateOutput1, else DuplicateOutput) on a D3D11 device of its own on the screen's adapter, copies the region on the GPU, reads it back through three staging textures and draws the cursor in (colour, masked colour and monochrome shapes). An encoder thread feeds FFmpeg: h264_nvenc, else h264_amf, h264_qsv, h264_mf, libx264, each tried in turn; BGRA straight to NVENC, swscale to NV12 or YUV 4:2:0 (BT.709) for the rest. A frame is sent when the screen or cursor changed and its turn on an fps schedule is due, with a repeat each second of an unchanged screen; the encoder falling 8 frames behind drops the oldest. A lost duplication (mode change, secure desktop) is made again and recording goes on. record_start says fall_back when duplication cannot run (unsupported, too many, rotated screen). Regions under 240 x 180 are refused, then sizes are rounded down to even; a process whose view of the desktop is scaled is refused. - tools/record_probe.jai (jai first.jai - record_probe): lists the screens, records a screen or region and reports source, encoder, frames captured, sent, written and dropped, time per stage and file size; -legacy, -cuda, -exclude-test, -animate, -snapshot DIR, -check FILE. - On the RTX 5080 laptop: 60 s of 4K at 30 fps, 1802 frames, none dropped, 18 MB a minute; every change kept at 60 fps; plain DuplicateOutput works when DPI aware; a CUDA context changes nothing; WDA_EXCLUDEFROMCAPTURE is respected; QSV, Media Foundation and libx264 keep up too; a recording killed mid-way still plays. The two-GPU failure was not reproduced: the screens are on the NVIDIA GPU in this mode. The plan records the results.
239 lines
11 KiB
Plaintext
239 lines
11 KiB
Plaintext
// DXGI Desktop Duplication: the screens as the GPU composes them, a frame each time one changes.
|
|
//
|
|
// It runs on a D3D11 device of its own, made on the adapter the screen is attached to: 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.
|
|
//
|
|
// 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. legacy forces the plain one, for
|
|
// record-probe to check that.
|
|
|
|
Dxgi_Source :: struct {
|
|
adapter: *IDXGIAdapter1;
|
|
output: *IDXGIOutput1;
|
|
output5: *IDXGIOutput5; // null before Windows 10 1703
|
|
device: *ID3D11Device;
|
|
device_context: *ID3D11DeviceContext;
|
|
duplication: *IDXGIOutputDuplication;
|
|
legacy: bool;
|
|
method: string; // which duplication call worked, for the log
|
|
|
|
box: D3D11_BOX; // the region, in the output's pixels
|
|
latest: *ID3D11Texture2D; // the region as last seen
|
|
staging: [RECORD_STAGING] *ID3D11Texture2D;
|
|
|
|
copy_ticks: s64; // spent taking images out of frames: QueryInterface and the copy's submission
|
|
pointer_position: POINT; // the cursor's top left, in the output's pixels
|
|
pointer_visible: bool;
|
|
shape: Cursor_Shape;
|
|
}
|
|
|
|
RECORD_STAGING :: 3; // staging textures in turn: the one read is two frames behind the one copied
|
|
|
|
// 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});
|
|
}
|
|
output_index += 1;
|
|
}
|
|
adapter_index += 1;
|
|
}
|
|
return screens;
|
|
}
|
|
|
|
// Makes the device on the screen's adapter, and 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;
|
|
|
|
factory: *IDXGIFactory1;
|
|
hr := CreateDXGIFactory1(*uid(IDXGIFactory1_UUID), xx *factory);
|
|
if FAILED(hr) return false, hresult_text("CreateDXGIFactory1", hr), false;
|
|
defer IUnknown_Release(factory);
|
|
hr = IDXGIFactory1_EnumAdapters1(factory, screen.adapter_index, *s.adapter);
|
|
if FAILED(hr) return false, hresult_text("the screen's adapter is gone", hr), 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
|
|
|
|
levels := D3D_FEATURE_LEVEL.[._11_1, ._11_0, ._10_1, ._10_0];
|
|
level: D3D_FEATURE_LEVEL;
|
|
hr = D3D11CreateDevice(s.adapter, .UNKNOWN, null, .BGRA_SUPPORT, levels.data, levels.count, D3D11_SDK_VERSION, *s.device, *level, *s.device_context);
|
|
if FAILED(hr) return false, hresult_text("D3D11CreateDevice on the screen's adapter", hr), true;
|
|
|
|
s.box = .{left = xx region.left, top = xx region.top, right = xx region.right, bottom = xx region.bottom, front = 0, back = 1};
|
|
width := cast(u32) (region.right - region.left);
|
|
height := cast(u32) (region.bottom - region.top);
|
|
desc: D3D11_TEXTURE2D_DESC;
|
|
desc.Width = width;
|
|
desc.Height = height;
|
|
desc.MipLevels = 1;
|
|
desc.ArraySize = 1;
|
|
desc.Format = .B8G8R8A8_UNORM;
|
|
desc.SampleDesc.Count = 1;
|
|
desc.Usage = .DEFAULT;
|
|
hr = ID3D11Device_CreateTexture2D(s.device, *desc, null, *s.latest);
|
|
if FAILED(hr) return false, hresult_text("a texture for the region", hr), false;
|
|
desc.Usage = .STAGING;
|
|
desc.CPUAccessFlags = .READ;
|
|
for * s.staging {
|
|
hr = ID3D11Device_CreateTexture2D(s.device, *desc, null, it);
|
|
if FAILED(hr) return false, hresult_text("a staging texture", hr), false;
|
|
}
|
|
|
|
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 {
|
|
if s.duplication { IUnknown_Release(s.duplication); s.duplication = null; }
|
|
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);
|
|
}
|
|
|
|
Dxgi_Next :: enum u8 { NONE; IMAGE; POINTER; IMAGE_AND_POINTER; LOST; FAILED; }
|
|
|
|
// Waits up to timeout_ms for the screen to change. A new image is copied into s.latest; a moved or
|
|
// changed cursor updates s.pointer_*. On LOST the duplication is gone: call dxgi_duplicate.
|
|
dxgi_next :: (s: *Dxgi_Source, timeout_ms: u32) -> Dxgi_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 {
|
|
start: LARGE_INTEGER;
|
|
QueryPerformanceCounter(*start);
|
|
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);
|
|
ID3D11DeviceContext_CopySubresourceRegion(s.device_context, s.latest, 0, 0, 0, 0, texture, 0, *s.box);
|
|
IUnknown_Release(texture);
|
|
end: LARGE_INTEGER;
|
|
QueryPerformanceCounter(*end);
|
|
s.copy_ticks += end.QuadPart - start.QuadPart;
|
|
}
|
|
|
|
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) {
|
|
release :: (p: *$T) { if p.* { IUnknown_Release(p.*); p.* = null; } }
|
|
release(*s.duplication);
|
|
for * s.staging release(it);
|
|
release(*s.latest);
|
|
release(*s.device_context);
|
|
release(*s.device);
|
|
release(*s.output5);
|
|
release(*s.output);
|
|
release(*s.adapter);
|
|
array_free(s.shape.data);
|
|
s.shape = .{};
|
|
}
|
|
|
|
#scope_module
|
|
|
|
hresult_is :: (hr: HRESULT, code: u32) -> bool {
|
|
return cast,no_check(u32) hr == code;
|
|
}
|
|
|
|
// "what failed: 0x887A0004 (DXGI_ERROR_UNSUPPORTED)", temp.
|
|
hresult_text :: (what: string, hr: HRESULT) -> string {
|
|
code := cast,no_check(u32) hr;
|
|
name := "";
|
|
if code == {
|
|
case DXGI_ERROR_UNSUPPORTED; name = " (DXGI_ERROR_UNSUPPORTED)";
|
|
case DXGI_ERROR_NOT_CURRENTLY_AVAILABLE; name = " (DXGI_ERROR_NOT_CURRENTLY_AVAILABLE)";
|
|
case DXGI_ERROR_ACCESS_LOST; name = " (DXGI_ERROR_ACCESS_LOST)";
|
|
case 0x80070005; name = " (E_ACCESSDENIED)";
|
|
case 0x80004002; name = " (E_NOINTERFACE)";
|
|
case 0x80070057; name = " (E_INVALIDARG)";
|
|
}
|
|
return tprint("%: 0x%%", what, formatInt(code, base = 16, minimum_digits = 8), name);
|
|
}
|
|
|
|
wide_text :: (wide: *s16) -> string {
|
|
text, ok := wide_to_utf8(cast(*u16) wide,, temp);
|
|
return ifx ok then text else "";
|
|
}
|