// 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 ""; }