Skip to content

Commit e30d8a1

Browse files
committed
fix(win): harden the DXGI fallback (reacquire, adapter, cursor, rotation, resize)
- Retry a failed duplication rebuild about once a second instead of freezing the take after a UAC prompt or a secure-desktop switch; log every recovery. - Search the output across all adapters and create the D3D11 device on the one that owns it, so hybrid and multi-GPU machines can use the fallback. - Draw the system cursor over the copied frame through GDI when the take asks for it, since Desktop Duplication never includes the pointer. - Refuse rotated displays with a clear error rather than record sideways. - End the take with an error when the capture size changes mid-recording, instead of copying between mismatched textures. - Keep ticksToHns from overflowing after ~25 h of QPC uptime.
1 parent 660a7e0 commit e30d8a1

4 files changed

Lines changed: 137 additions & 78 deletions

File tree

‎electron/native/wgc-capture/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ target_link_libraries(wgc-capture PRIVATE
8181
avrt
8282
d3d11
8383
dxgi
84+
gdi32
8485
mf
8586
mfplat
8687
mfreadwrite

‎electron/native/wgc-capture/src/dxgi_duplication_session.cpp‎

Lines changed: 110 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66
namespace {
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

191207
bool 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

223232
bool 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+
302340
void DxgiDuplicationSession::setFrameCallback(FrameCallback callback) {
303341
(void)callback;
304342
// Deliberately inert: Desktop Duplication is pull-only, and rebuilding the

‎electron/native/wgc-capture/src/dxgi_duplication_session.h‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,14 @@ class DxgiDuplicationSession {
6363
ID3D11DeviceContext* context() const { return d3dContext_.Get(); }
6464

6565
private:
66+
bool findOutput(HMONITOR monitor);
6667
bool createD3DDevice();
67-
bool createDuplication(HMONITOR monitor);
68+
bool createDuplication();
6869
bool ensureCopyTexture();
6970
bool reacquire();
71+
// WGC draws the pointer itself; Desktop Duplication never does, so with the
72+
// system cursor mode it is drawn here, over the private copy, through GDI.
73+
void drawCursor();
7074
// LastPresentTime and QueryPerformanceCounter share the QPC clock, which is
7175
// the same clock WGC's SystemRelativeTime is derived from -- and main.cpp
7276
// only ever uses the value as a delta from the first frame, so a monotonic
@@ -75,7 +79,7 @@ class DxgiDuplicationSession {
7579

7680
Microsoft::WRL::ComPtr<ID3D11Device> d3dDevice_;
7781
Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3dContext_;
78-
Microsoft::WRL::ComPtr<IDXGIAdapter> adapter_;
82+
Microsoft::WRL::ComPtr<IDXGIAdapter1> adapter_;
7983
Microsoft::WRL::ComPtr<IDXGIOutput1> output_;
8084
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
8185
Microsoft::WRL::ComPtr<ID3D11Texture2D> copyTexture_;
@@ -87,8 +91,7 @@ class DxgiDuplicationSession {
8791
int fps_ = 60;
8892
bool captureCursor_ = false;
8993
bool started_ = false;
90-
bool warnedCursorIgnored_ = false;
91-
bool warnedAccessLost_ = false;
92-
bool exhausted_ = false;
93-
uint64_t reacquireCount_ = 0;
94+
RECT desktopRect_{};
95+
bool warnedReacquireFailed_ = false;
96+
ULONGLONG lastReacquireAttemptMs_ = 0;
9497
};

‎electron/native/wgc-capture/src/main.cpp‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,23 @@ int wmain(int argc, wchar_t* argv[]) {
11661166
break;
11671167
}
11681168
}
1169+
// A display mode change mid-take (resolution, scale)
1170+
// hands over frames of a new size. The encoder was set
1171+
// up for the first one, and CopyResource between
1172+
// mismatched textures is undefined, so end the take
1173+
// with a clear error instead of recording garbage.
1174+
D3D11_TEXTURE2D_DESC frameDesc{};
1175+
D3D11_TEXTURE2D_DESC latestDesc{};
1176+
wgcTexture->GetDesc(&frameDesc);
1177+
latestFrameTexture->GetDesc(&latestDesc);
1178+
if (frameDesc.Width != latestDesc.Width || frameDesc.Height != latestDesc.Height) {
1179+
std::cerr << "ERROR: Capture size changed mid-recording (" << latestDesc.Width
1180+
<< "x" << latestDesc.Height << " -> " << frameDesc.Width << "x"
1181+
<< frameDesc.Height << "); ending the take" << std::endl;
1182+
encodeFailed = true;
1183+
control.requestStop();
1184+
break;
1185+
}
11691186
// The wedge risk this class exists to avoid: this call
11701187
// can block inside the display driver and never return
11711188
// (#252, still true of PR #305's DXGI path on some

0 commit comments

Comments
 (0)