Skip to content

Commit 7d9e5d5

Browse files
committed
fix(wgc): tell "no outputs" apart from "outputs could not be read"
DXGI_ERROR_NOT_CURRENTLY_AVAILABLE is not the end of an enumeration. It is what EnumOutputs answers a process in session 0, and it means the outputs were never inspected rather than that the adapter has none. Both were being folded into the same FAILED() test, so a machine we could not look at reported exactly what a machine with an active virtual display reports: monitorAdapter null. That value is one this diagnostic tells its readers to act on -- the briefing sent to an operator said null meant a purely virtual display. It would have sent them down the wrong path, and it is the same class of defect this PR was opened to fix for the descriptions: an ambiguity that reads as a finding. The event now carries monitorLookup: ok, no-output-claims-it, or unavailable, so the absence of a monitor adapter is never reported without saying whether anyone actually looked. The stderr dump says outputs=unavailable rather than outputs=none for the same reason. Unreachable from the app, which spawns the helper from the user's session -- but the helper is run standalone by hand during exactly this investigation, and that is where session 0 gets reached.
1 parent 0a070f4 commit 7d9e5d5

2 files changed

Lines changed: 47 additions & 15 deletions

File tree

electron/native/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Frame input path: the helper feeds the encoder from the GPU when it can. On that
9191

9292
The helper reports the outcome through the `encoder-selection` stdout event (`video` is `default`, `software-preferred`, or `software-fallback`; `videoInput` is `dxgi-nv12` or `cpu-rgb32`; `container` is `fragmented-mp4` or `mp4`; all three report what the encoder settled on rather than what was asked for). On the GPU path the helper also prints one `[frame-drops] gpu_bridge_contended=<n>` line to stderr at stop: a frame the bridge was too busy to take is skipped rather than failing the recording, and a large count there is the first thing to look at in a report about missing frames. When the app sees `software-fallback` — the default encoder failed and the helper switched on its own — it shows a small dismissible notice in the recording HUD with a "Don't show again" option, because software encoding can raise CPU usage. An explicit `software-preferred` selection shows no notice, and the event stays available for diagnostics either way.
9393

94-
At startup the helper also emits `capture-adapter`, naming the GPU its D3D device landed on and the one actually driving the captured display, each with its LUID, plus one `[adapters]` line per enumerated adapter on stderr. `createD3DDevice` asks for the *default* adapter and nothing checks that it is the one driving the display; when they differ every frame crosses an adapter boundary before the caller touches it. The LUIDs are there because the descriptions are not enough to tell: an IddCx virtual display driver renders through the physical GPU and inherits its description string while being a separate DXGI adapter, so the configuration this diagnostic exists to catch is precisely the one where both names are identical and only the LUIDs differ (measured: `NVIDIA Quadro RTX 4000` at LUID `0:24084` driving the display, the same string at `0:12889146` for the virtual adapter).
94+
At startup the helper also emits `capture-adapter`, naming the GPU its D3D device landed on and the one actually driving the captured display, each with its LUID, plus one `[adapters]` line per enumerated adapter on stderr. `createD3DDevice` asks for the *default* adapter and nothing checks that it is the one driving the display; when they differ every frame crosses an adapter boundary before the caller touches it. The LUIDs are there because the descriptions are not enough to tell: an IddCx virtual display driver renders through the physical GPU and inherits its description string while being a separate DXGI adapter, so the configuration this diagnostic exists to catch is precisely the one where both names are identical and only the LUIDs differ (measured: `NVIDIA Quadro RTX 4000` at LUID `0:24084` driving the display, the same string at `0:12889146` for the virtual adapter). `monitorLookup` says which of three things happened: `ok`, `no-output-claims-it` (the enumeration finished and nothing owns the captured monitor, which is what an active virtual display looks like), or `unavailable` (`EnumOutputs` refused, as it does in session 0 — the outputs were never inspected, so the absence means nothing about the hardware).
9595

9696
Encoder diagnostic on final sink-writer failure: when the final sink-writer attempt fails (`MFCreateSinkWriterFromMediaSink` on the fragmented container, `MFCreateSinkWriterFromURL` on the plain one; the message names which), the helper logs the registered H.264 video encoder MFT count (via `MFTEnumEx`), the registered AAC encoder count when audio was requested, and the hex HRESULT. If no H.264 encoder is registered, it additionally emits the four-bullet actionable error (missing Media Feature Pack / GPU driver registration / empty `HKLM:\SOFTWARE\Microsoft\Windows Media Foundation\Transforms` / reboot). If an H.264 encoder IS registered but the sink writer still failed, it logs a hint pointing at invalid output path, missing MP4 mux, or GPU driver incompatibility. There is still no fail-fast pre-flight gate because `MFTEnumEx` and the sink writer can disagree about which H.264 encoders are available in non-interactive / Session 0 contexts.
9797

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

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -246,22 +246,38 @@ void reportCaptureAdapters(ID3D11Device* device, HMONITOR targetMonitor) {
246246
std::string monitorAdapterLuid;
247247
bool monitorAdapterFound = false;
248248
bool sameAdapter = false;
249-
// Both loops end on FAILED(), not on DXGI_ERROR_NOT_FOUND specifically.
249+
// Set when EnumOutputs says the outputs could not be looked at, rather than
250+
// that there are none. The two are different answers and the event reports
251+
// them differently -- see the comment on the inner loop.
252+
bool enumerationUnavailable = false;
253+
// The loops end on FAILED(), not on DXGI_ERROR_NOT_FOUND specifically.
250254
// NOT_FOUND is itself a failure code, so one test covers the normal end of
251255
// the enumeration and every other way it can stop -- and the other ways are
252-
// what matter here. EnumOutputs returns DXGI_ERROR_NOT_CURRENTLY_AVAILABLE
253-
// to a process in session 0, and neither call fills its out-pointer when it
254-
// fails. Testing only for NOT_FOUND left a null ComPtr to be dereferenced on
255-
// the next line, which would take down a recording from inside the one
256-
// function in this file that promises never to.
256+
// what matter here: neither call fills its out-pointer when it fails, so
257+
// testing only for NOT_FOUND left a null ComPtr to be dereferenced on the
258+
// next line, taking down a recording from inside the one function in this
259+
// file that promises never to.
257260
for (UINT adapterIndex = 0;; ++adapterIndex) {
258261
Microsoft::WRL::ComPtr<IDXGIAdapter1> adapter;
259262
if (FAILED(factory->EnumAdapters1(adapterIndex, &adapter)) || !adapter) {
260263
break;
261264
}
262265
for (UINT outputIndex = 0;; ++outputIndex) {
263266
Microsoft::WRL::ComPtr<IDXGIOutput> output;
264-
if (FAILED(adapter->EnumOutputs(outputIndex, &output)) || !output) {
267+
// NOT_CURRENTLY_AVAILABLE is the exception to the rule above, and it
268+
// has to be told apart: it is what EnumOutputs answers a process in
269+
// session 0, and it means the outputs could not be inspected rather
270+
// than that the adapter has none. Collapsing the two would report
271+
// "no adapter claims this monitor" for a machine we never got to
272+
// look at -- and that is a value this diagnostic tells its readers
273+
// to interpret as an active virtual display. Same class of lie as
274+
// the identical descriptions this event was just fixed for.
275+
const HRESULT outputHr = adapter->EnumOutputs(outputIndex, &output);
276+
if (outputHr == DXGI_ERROR_NOT_CURRENTLY_AVAILABLE) {
277+
enumerationUnavailable = true;
278+
break;
279+
}
280+
if (FAILED(outputHr) || !output) {
265281
break;
266282
}
267283
DXGI_OUTPUT_DESC outputDesc{};
@@ -303,12 +319,19 @@ void reportCaptureAdapters(ID3D11Device* device, HMONITOR targetMonitor) {
303319
<< formatLuid(deviceDesc.AdapterLuid) << "\",\"monitorAdapter\":";
304320
if (monitorAdapterFound) {
305321
std::cout << "\"" << jsonEscape(wideToUtf8(monitorAdapterName)) << "\",\"monitorLuid\":\""
306-
<< monitorAdapterLuid << "\",\"sameAdapter\":" << (sameAdapter ? "true" : "false");
322+
<< monitorAdapterLuid << "\",\"monitorLookup\":\"ok\",\"sameAdapter\":"
323+
<< (sameAdapter ? "true" : "false");
324+
} else if (enumerationUnavailable) {
325+
// Session 0: the outputs were never inspected. Reported as its own
326+
// state so nobody reads it as a finding about the hardware.
327+
std::cout << "null,\"monitorLuid\":null,\"monitorLookup\":\"unavailable\",\"sameAdapter\":null";
307328
} else {
308-
// No output claims this monitor: it is driven by something DXGI does not
309-
// enumerate, which on the machines in #252 means a virtual display
310-
// adapter. Worth seeing in a report in its own right.
311-
std::cout << "null,\"monitorLuid\":null,\"sameAdapter\":null";
329+
// The enumeration completed and no output claims this monitor: it is
330+
// driven by something DXGI does not enumerate, which on the machines in
331+
// #252 would mean a virtual display adapter. Worth seeing in its own
332+
// right -- but only distinguishable from the case above because that
333+
// one is now labelled.
334+
std::cout << "null,\"monitorLuid\":null,\"monitorLookup\":\"no-output-claims-it\",\"sameAdapter\":null";
312335
}
313336
std::cout << "}" << std::endl;
314337

@@ -328,9 +351,15 @@ void reportCaptureAdapters(ID3D11Device* device, HMONITOR targetMonitor) {
328351
std::cerr << "[adapters] " << adapterIndex << " luid=" << formatLuid(desc.AdapterLuid)
329352
<< " \"" << wideToUtf8(desc.Description) << "\"";
330353
UINT outputCount = 0;
354+
bool outputsUnavailable = false;
331355
for (UINT outputIndex = 0;; ++outputIndex) {
332356
Microsoft::WRL::ComPtr<IDXGIOutput> output;
333-
if (FAILED(adapter->EnumOutputs(outputIndex, &output)) || !output) {
357+
const HRESULT outputHr = adapter->EnumOutputs(outputIndex, &output);
358+
if (outputHr == DXGI_ERROR_NOT_CURRENTLY_AVAILABLE) {
359+
outputsUnavailable = true;
360+
break;
361+
}
362+
if (FAILED(outputHr) || !output) {
334363
break;
335364
}
336365
DXGI_OUTPUT_DESC outputDesc{};
@@ -340,7 +369,10 @@ void reportCaptureAdapters(ID3D11Device* device, HMONITOR targetMonitor) {
340369
}
341370
++outputCount;
342371
}
343-
if (outputCount == 0) {
372+
if (outputsUnavailable) {
373+
// Not the same as none: session 0 refuses the question entirely.
374+
std::cerr << " outputs=unavailable";
375+
} else if (outputCount == 0) {
344376
std::cerr << " outputs=none";
345377
}
346378
std::cerr << std::endl;

0 commit comments

Comments
 (0)