55
66namespace {
77
8- // Same device shape WgcSession creates, so the encoder and the cursor sampler
9- // see the same adapter/feature level whichever backend produced the frames.
10- bool createDevice (Microsoft::WRL ::ComPtr<ID3D11Device>& device,
8+ // Same device shape WgcSession creates, but on the adapter that owns the output:
9+ // DuplicateOutput refuses a device from any other adapter, which is the normal
10+ // case for a display wired to the discrete GPU of a hybrid laptop.
11+ bool createDevice (IDXGIAdapter1* adapter, Microsoft::WRL ::ComPtr<ID3D11Device>& device,
1112 Microsoft::WRL ::ComPtr<ID3D11DeviceContext>& context) {
1213 const D3D_FEATURE_LEVEL levels[] = {
1314 D3D_FEATURE_LEVEL_11_1 ,
@@ -17,11 +18,11 @@ bool createDevice(Microsoft::WRL::ComPtr<ID3D11Device>& device,
1718 };
1819 UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT ;
1920 D3D_FEATURE_LEVEL obtained = D3D_FEATURE_LEVEL_11_0 ;
20- HRESULT hr = D3D11CreateDevice (nullptr , D3D_DRIVER_TYPE_HARDWARE , nullptr , flags, levels,
21+ HRESULT hr = D3D11CreateDevice (adapter, D3D_DRIVER_TYPE_UNKNOWN , nullptr , flags, levels,
2122 ARRAYSIZE (levels), D3D11_SDK_VERSION , device.GetAddressOf (),
2223 &obtained, context.GetAddressOf ());
2324 if (FAILED (hr)) {
24- hr = D3D11CreateDevice (nullptr , D3D_DRIVER_TYPE_HARDWARE , nullptr ,
25+ hr = D3D11CreateDevice (adapter, D3D_DRIVER_TYPE_UNKNOWN , nullptr ,
2526 flags & ~D3D11_CREATE_DEVICE_BGRA_SUPPORT , levels,
2627 ARRAYSIZE (levels), D3D11_SDK_VERSION , device.GetAddressOf (),
2728 &obtained, context.GetAddressOf ());
@@ -40,48 +41,62 @@ DxgiDuplicationSession::~DxgiDuplicationSession() {
4041 stop ();
4142}
4243
43- bool DxgiDuplicationSession::createD3DDevice () {
44- if (d3dDevice_) {
45- return true ;
46- }
47- if (!createDevice (d3dDevice_, d3dContext_)) {
48- return false ;
49- }
50- QueryPerformanceFrequency (&qpcFrequency_);
51- return true ;
52- }
53-
54- bool DxgiDuplicationSession::createDuplication (HMONITOR monitor) {
55- Microsoft::WRL ::ComPtr<IDXGIDevice> dxgiDevice;
56- if (FAILED (d3dDevice_.As (&dxgiDevice))) {
57- return false ;
58- }
59- if (FAILED (dxgiDevice->GetAdapter (adapter_.ReleaseAndGetAddressOf ()))) {
44+ bool DxgiDuplicationSession::findOutput (HMONITOR monitor) {
45+ output_.Reset ();
46+ adapter_.Reset ();
47+ Microsoft::WRL ::ComPtr<IDXGIFactory1> factory;
48+ if (FAILED (CreateDXGIFactory1 (IID_PPV_ARGS (&factory)))) {
49+ std::cerr << " ERROR: DXGI capture could not create a DXGI factory" << std::endl;
6050 return false ;
6151 }
6252
6353 // The monitor handle the caller matched with EnumDisplayMonitors is the only
6454 // thing tying the app's chosen source to an output, so match on it rather
6555 // than on index or position: a display can be rearranged between the picker
66- // and the take.
67- Microsoft::WRL ::ComPtr<IDXGIOutput> candidate;
68- for (UINT i = 0 ; adapter_->EnumOutputs (i, candidate.ReleaseAndGetAddressOf ()) == S_OK ; ++i) {
69- DXGI_OUTPUT_DESC desc{};
70- if (SUCCEEDED (candidate->GetDesc (&desc)) && desc.Monitor == monitor) {
56+ // and the take. Every adapter is searched, not just the default one.
57+ Microsoft::WRL ::ComPtr<IDXGIAdapter1> adapter;
58+ for (UINT a = 0 ; factory->EnumAdapters1 (a, adapter.ReleaseAndGetAddressOf ()) == S_OK ; ++a) {
59+ Microsoft::WRL ::ComPtr<IDXGIOutput> candidate;
60+ for (UINT i = 0 ; adapter->EnumOutputs (i, candidate.ReleaseAndGetAddressOf ()) == S_OK ; ++i) {
61+ DXGI_OUTPUT_DESC desc{};
62+ if (FAILED (candidate->GetDesc (&desc)) || desc.Monitor != monitor) {
63+ continue ;
64+ }
65+ if (desc.Rotation != DXGI_MODE_ROTATION_IDENTITY &&
66+ desc.Rotation != DXGI_MODE_ROTATION_UNSPECIFIED ) {
67+ // Desktop Duplication hands over the unrotated scan-out image, so a
68+ // portrait display would record sideways. Refuse rather than do that.
69+ std::cerr << " ERROR: DXGI capture does not support rotated displays (rotation="
70+ << desc.Rotation << " )" << std::endl;
71+ return false ;
72+ }
7173 if (FAILED (candidate.As (&output_))) {
7274 std::cerr << " ERROR: DXGI capture could not query IDXGIOutput1 for the chosen monitor"
7375 << std::endl;
7476 return false ;
7577 }
76- break ;
78+ adapter_ = adapter;
79+ desktopRect_ = desc.DesktopCoordinates ;
80+ return true ;
7781 }
7882 }
79- if (!output_) {
80- std::cerr << " ERROR: DXGI capture could not find an output for the chosen monitor"
81- << std::endl;
83+ std::cerr << " ERROR: DXGI capture could not find an output for the chosen monitor"
84+ << std::endl;
85+ return false ;
86+ }
87+
88+ bool DxgiDuplicationSession::createD3DDevice () {
89+ if (d3dDevice_) {
90+ return true ;
91+ }
92+ if (!createDevice (adapter_.Get (), d3dDevice_, d3dContext_)) {
8293 return false ;
8394 }
95+ QueryPerformanceFrequency (&qpcFrequency_);
96+ return true ;
97+ }
8498
99+ bool DxgiDuplicationSession::createDuplication () {
85100 Microsoft::WRL ::ComPtr<IDXGIOutputDuplication> duplication;
86101 HRESULT hr = output_->DuplicateOutput (d3dDevice_.Get (), duplication.GetAddressOf ());
87102 if (FAILED (hr)) {
@@ -121,6 +136,8 @@ bool DxgiDuplicationSession::ensureCopyTexture() {
121136 // released immediately and the pointer handed to the caller stays valid.
122137 // Flags match what main.cpp already does when it copies a WGC frame out:
123138 // no bind flags, no CPU access -- it is only ever a CopyResource source.
139+ // With the system cursor it must also take a GDI DC, which needs a render
140+ // target that is GDI-compatible (drawCursor).
124141 D3D11_TEXTURE2D_DESC desc{};
125142 desc.Width = static_cast <UINT >(width_);
126143 desc.Height = static_cast <UINT >(height_);
@@ -129,9 +146,9 @@ bool DxgiDuplicationSession::ensureCopyTexture() {
129146 desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM ;
130147 desc.SampleDesc .Count = 1 ;
131148 desc.Usage = D3D11_USAGE_DEFAULT ;
132- desc.BindFlags = 0 ;
149+ desc.BindFlags = captureCursor_ ? D3D11_BIND_RENDER_TARGET : 0 ;
133150 desc.CPUAccessFlags = 0 ;
134- desc.MiscFlags = 0 ;
151+ desc.MiscFlags = captureCursor_ ? D3D11_RESOURCE_MISC_GDI_COMPATIBLE : 0 ;
135152 const HRESULT hr = d3dDevice_->CreateTexture2D (&desc, nullptr , copyTexture_.GetAddressOf ());
136153 if (FAILED (hr)) {
137154 std::cerr << " ERROR: DXGI capture could not create its copy texture (hr=0x" << std::hex << hr
@@ -145,17 +162,14 @@ bool DxgiDuplicationSession::initialize(HMONITOR monitor, int fps, bool captureC
145162 if (!monitor) {
146163 return false ;
147164 }
148- if (captureCursor_) {
149- // Nothing to do yet; the warning is emitted once on the first frame.
150- }
151165 captureCursor_ = captureCursor;
152166 fps_ = std::clamp (fps, 1 , 120 );
153167 monitor_ = monitor;
154168
155- if (!createD3DDevice ()) {
169+ if (!findOutput (monitor) || ! createD3DDevice ()) {
156170 return false ;
157171 }
158- if (!createDuplication (monitor )) {
172+ if (!createDuplication ()) {
159173 return false ;
160174 }
161175 if (!ensureCopyTexture ()) {
@@ -185,45 +199,46 @@ int64_t DxgiDuplicationSession::ticksToHns(LONGLONG ticks) const {
185199 return ticks;
186200 }
187201 // 10'000'000 == one second in 100 ns units.
188- return static_cast <int64_t >((ticks * 10'000'000LL ) / qpcFrequency_.QuadPart );
202+ // Split so ticks * 10^7 never overflows (it would after ~25 h of uptime).
203+ const LONGLONG f = qpcFrequency_.QuadPart ;
204+ return static_cast <int64_t >((ticks / f) * 10'000'000LL + ((ticks % f) * 10'000'000LL ) / f);
189205}
190206
191207bool DxgiDuplicationSession::reacquire () {
192208 // DXGI_ERROR_ACCESS_LOST is normal: a UAC prompt, a desktop switch, a
193209 // resolution or refresh-rate change, or another process taking exclusive
194210 // fullscreen all invalidate the duplication. Rebuild it and let the caller
195- // keep polling -- a frame is simply skipped, never a recording ended.
196- ++reacquireCount_;
211+ // keep polling -- a frame is simply skipped, never a recording ended. While
212+ // the secure desktop is up, DuplicateOutput keeps failing, so the caller
213+ // retries about once a second until it comes back.
214+ lastReacquireAttemptMs_ = GetTickCount64 ();
197215 duplication_.Reset ();
198- output_.Reset ();
199- adapter_.Reset ();
200216 copyTexture_.Reset ();
201- if (!d3dDevice_) {
202- return false ;
203- }
204- if (!createDuplication (monitor_)) {
205- if (!warnedAccessLost_) {
206- warnedAccessLost_ = true ;
207- std::cerr << " WARNING: DXGI capture lost the desktop and could not reacquire it; "
208- " recording continues without new frames"
217+ if (!d3dDevice_ || !findOutput (monitor_) || !createDuplication () || !ensureCopyTexture ()) {
218+ duplication_.Reset ();
219+ if (!warnedReacquireFailed_) {
220+ warnedReacquireFailed_ = true ;
221+ std::cerr << " WARNING: DXGI capture lost the desktop and could not reacquire it yet; "
222+ " retrying every second"
209223 << std::endl;
210224 }
211225 return false ;
212226 }
213- if (!ensureCopyTexture ()) {
214- return false ;
215- }
216- if (!warnedAccessLost_) {
217- warnedAccessLost_ = true ;
218- std::cout << " {\" event\" :\" capture-reacquired\" ,\" backend\" :\" dxgi\" }" << std::endl;
219- }
227+ warnedReacquireFailed_ = false ;
228+ std::cout << " {\" event\" :\" capture-reacquired\" ,\" backend\" :\" dxgi\" }" << std::endl;
220229 return true ;
221230}
222231
223232bool DxgiDuplicationSession::tryGetNextFrame (ID3D11Texture2D** outTexture, int64_t * outTimestampHns) {
224- if (!outTexture || !outTimestampHns || !started_ || !duplication_ || exhausted_ ) {
233+ if (!outTexture || !outTimestampHns || !started_) {
225234 return false ;
226235 }
236+ if (!duplication_) {
237+ // A previous rebuild failed: try again, throttled, rather than give up.
238+ if (GetTickCount64 () - lastReacquireAttemptMs_ < 1000 || !reacquire ()) {
239+ return false ;
240+ }
241+ }
227242 *outTexture = nullptr ;
228243
229244 // Wait no longer than one frame interval. main.cpp polls this on its own
@@ -239,9 +254,7 @@ bool DxgiDuplicationSession::tryGetNextFrame(ID3D11Texture2D** outTexture, int64
239254 return false ;
240255 }
241256 if (hr == DXGI_ERROR_ACCESS_LOST ) {
242- if (!reacquire ()) {
243- exhausted_ = true ;
244- }
257+ reacquire ();
245258 return false ;
246259 }
247260 if (FAILED (hr)) {
@@ -273,16 +286,8 @@ bool DxgiDuplicationSession::tryGetNextFrame(ID3D11Texture2D** outTexture, int64
273286 return false ;
274287 }
275288
276- if (captureCursor_ && !warnedCursorIgnored_) {
277- warnedCursorIgnored_ = true ;
278- // Desktop Duplication hands over the desktop image without the pointer;
279- // drawing it is the caller's job (DXGI_OUTDUPL_FRAME_INFO carries the
280- // shape and position). The app's default editable-overlay mode does not
281- // ask for it -- it samples the cursor separately and composites its own
282- // -- so this only affects a take explicitly set to the system cursor.
283- std::cerr << " WARNING: DXGI capture does not composite the system cursor; use the "
284- " editable cursor mode for this machine"
285- << std::endl;
289+ if (captureCursor_) {
290+ drawCursor ();
286291 }
287292
288293 // LastPresentTime shares the QPC clock. A zero means the frame carried no
@@ -299,6 +304,39 @@ bool DxgiDuplicationSession::tryGetNextFrame(ID3D11Texture2D** outTexture, int64
299304 return true ;
300305}
301306
307+ void DxgiDuplicationSession::drawCursor () {
308+ CURSORINFO ci{};
309+ ci.cbSize = sizeof (ci);
310+ if (!GetCursorInfo (&ci) || !(ci.flags & CURSOR_SHOWING ) || !ci.hCursor ) {
311+ return ;
312+ }
313+ ICONINFO icon{};
314+ if (!GetIconInfo (ci.hCursor , &icon)) {
315+ return ;
316+ }
317+ // GetIconInfo hands over copies of the bitmaps, which the caller owns.
318+ if (icon.hbmMask ) {
319+ DeleteObject (icon.hbmMask );
320+ }
321+ if (icon.hbmColor ) {
322+ DeleteObject (icon.hbmColor );
323+ }
324+
325+ Microsoft::WRL ::ComPtr<IDXGISurface1> surface;
326+ HDC dc = nullptr ;
327+ if (FAILED (copyTexture_.As (&surface)) || FAILED (surface->GetDC (FALSE , &dc))) {
328+ return ;
329+ }
330+ // The helper is per-monitor DPI aware, so the cursor position and the
331+ // output's desktop rectangle are both in physical pixels. DrawIconEx with
332+ // DI_NORMAL applies the AND/XOR masks, so monochrome and inverting cursors
333+ // look the way they do on screen.
334+ DrawIconEx (dc, ci.ptScreenPos .x - desktopRect_.left - static_cast <int >(icon.xHotspot ),
335+ ci.ptScreenPos .y - desktopRect_.top - static_cast <int >(icon.yHotspot ), ci.hCursor , 0 ,
336+ 0 , 0 , nullptr , DI_NORMAL | DI_DEFAULTSIZE );
337+ surface->ReleaseDC (nullptr );
338+ }
339+
302340void DxgiDuplicationSession::setFrameCallback (FrameCallback callback) {
303341 (void )callback;
304342 // Deliberately inert: Desktop Duplication is pull-only, and rebuilding the
0 commit comments