Skip to content

fix(wgc): report adapter LUIDs, because the descriptions can be identical - #342

Open
EtienneLescot wants to merge 1 commit into
mainfrom
fix/adapter-luid
Open

fix(wgc): report adapter LUIDs, because the descriptions can be identical#342
EtienneLescot wants to merge 1 commit into
mainfrom
fix/adapter-luid

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

The capture-adapter diagnostic from #332 compared adapters by LUID and printed only their descriptions. On the exact configuration it exists to catch, those two descriptions are the same string.

Measured on a rented multi-adapter VM:

adapter[0]  NVIDIA Quadro RTX 4000   LUID 0:24084      -> \.\DISPLAY1
adapter[1]  NVIDIA Quadro RTX 4000   LUID 0:12889146   -> the Parsec IDD

An IddCx virtual display driver renders through the physical GPU and inherits its description string, while being a separate DXGI adapter with its own LUID. QueryDisplayConfig confirmed the virtual monitor sits under 0:12889146.

So a machine that actually has the divergence would have printed:

{"deviceAdapter":"NVIDIA Quadro RTX 4000","monitorAdapter":"NVIDIA Quadro RTX 4000","sameAdapter":false}

Two identical names next to sameAdapter:false — which reads as a bug in the reporting rather than as the finding it is, and would most likely have been dismissed as one. The comparison was never wrong. The output was unreadable at the only moment it matters, and we were days away from asking four reporters to run it.

What changed

  • deviceLuid and monitorLuid on the event, formatted HighPart:LowPart to match QueryDisplayConfig traces so a LUID from a bug report greps straight against one
  • one [adapters] line per enumerated adapter on stderr, with its LUID and its outputs, marking the captured one

The stderr dump matters for the negative case too: an adapter with no output is how an inactive virtual display presents, and that is exactly why the rented box reported sameAdapter:true despite having a virtual display driver installed. Having one is not enough — it has to be driving the captured display.

Not in scope

Making createD3DDevice select the adapter that owns the captured HMONITOR instead of taking the default. That is a real correctness gap — nothing in DXGI promises adapter 0 drives the display, and nothing in the code checks — but it is a behaviour change and belongs in its own PR. This one only makes the existing diagnostic legible.

Refs #252, #292, #327.

Summary by CodeRabbit

  • New Features

    • Added enhanced Windows capture diagnostics, including capture and display GPU identifiers.
    • Startup logs now list all detected graphics adapters and their connected displays.
    • Added indicators for the captured monitor and adapters without outputs.
    • Added cross-adapter detection when graphics adapters have identical descriptions.
    • Updated diagnostic event data to version 2 and preserved empty monitor values when no match is found.
  • Documentation

    • Updated Windows helper documentation to explain the new adapter diagnostics and cross-adapter detection.

…ical

The `capture-adapter` diagnostic compared adapters by LUID and printed only
their descriptions. On the exact configuration it exists to catch, those two
descriptions are the same string.

An IddCx virtual display driver renders through the physical GPU and inherits
its description while being a separate DXGI adapter with its own LUID. Measured
on a rented multi-adapter box:

    adapter[0]  NVIDIA Quadro RTX 4000   LUID 0:24084      -> \.\DISPLAY1
    adapter[1]  NVIDIA Quadro RTX 4000   LUID 0:12889146   -> the Parsec IDD

So a machine with the divergence would have reported two identical names beside
`sameAdapter:false` — which reads as a bug in the reporting rather than as the
finding it is, and would most likely have been dismissed as one. The comparison
was never wrong; the output was unreadable at the only moment it matters.

Adds `deviceLuid` and `monitorLuid` to the event, and one `[adapters]` line per
enumerated adapter on stderr with its LUID and its outputs, marking the captured
one. Two adapters sharing a description is something a reader has to see before
they will believe `sameAdapter` over the names, and an adapter with no output at
all is how an inactive virtual display presents -- which is why the rented box
reported `sameAdapter:true` despite having a virtual display driver installed.

Formatted HighPart:LowPart to match QueryDisplayConfig traces, so a LUID from a
bug report can be grepped straight against one.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The Windows capture helper now reports capture and monitor adapter LUIDs, enumerates DXGI adapters and outputs at startup, and documents the new diagnostics and cross-adapter detection.

Changes

GPU adapter diagnostics

Layer / File(s) Summary
Adapter identity reporting
electron/native/wgc-capture/src/main.cpp, electron/native/README.md
The helper formats adapter LUIDs, adds device and monitor LUIDs to the version 2 capture-adapter output, enumerates adapters and outputs on stderr, and documents the diagnostics.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: my-denia

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: reporting adapter LUIDs when descriptions can match.
Description check ✅ Passed The description clearly explains the problem, implementation, testing evidence, related issues, and out-of-scope behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/adapter-luid

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/native/wgc-capture/src/main.cpp`:
- Around line 307-311: Update the DXGI monitor/output enumeration loops in main
so they stop only when the enumeration call returns DXGI_ERROR_NOT_FOUND; treat
DXGI_ERROR_NOT_CURRENTLY_AVAILABLE as an indeterminate enumeration failure and
report that distinct status instead of emitting monitorLuid:null or
outputs=none. Preserve the existing no-output handling for genuinely completed
enumeration, and run the WGC smoke test on a real Windows system.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 472a463e-0f04-4098-8c6a-86f2b70a5cc8

📥 Commits

Reviewing files that changed from the base of the PR and between a6795d2 and ab650e1.

📒 Files selected for processing (2)
  • electron/native/README.md
  • electron/native/wgc-capture/src/main.cpp

Comment on lines 307 to +311
} else {
// No output claims this monitor: it is driven by something DXGI does not
// enumerate, which on the machines in #252 means a virtual display
// adapter. Worth seeing in a report in its own right.
std::cout << "null,\"sameAdapter\":null";
std::cout << "null,\"monitorLuid\":null,\"sameAdapter\":null";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- file outline ---'
ast-grep outline electron/native/wgc-capture/src/main.cpp
printf '%s\n' '--- target section ---'
sed -n '240,370p' electron/native/wgc-capture/src/main.cpp
printf '%s\n' '--- HRESULT and enumeration references ---'
rg -n -C 4 'EnumAdapters|EnumOutputs|DXGI_ERROR_NOT_FOUND|DXGI_ERROR_NOT_CURRENTLY_AVAILABLE|outputs=|monitorLuid|sameAdapter' electron/native/wgc-capture/src/main.cpp electron/native

Repository: getopenscreen/openscreen

Length of output: 25062


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- function context and callers ---'
sed -n '190,355p' electron/native/wgc-capture/src/main.cpp
rg -n -C 3 'reportCaptureAdapters|capture-adapter|schemaVersion.*2|monitorAdapter|outputs=none' --glob '!node_modules/**' .
printf '%s\n' '--- relevant project guidance and native test references ---'
rg -n -C 3 'manual smoke|smoke test|wgc-capture|Windows|macOS|native capture' README.md CONTRIBUTING.md docs electron .github 2>/dev/null || true
printf '%s\n' '--- change summary ---'
git diff --stat
git status --short

Repository: getopenscreen/openscreen

Length of output: 50381


🌐 Web query:

Microsoft DXGI IDXGIAdapter1::EnumAdapters1 IDXGIAdapter::EnumOutputs documentation DXGI_ERROR_NOT_FOUND DXGI_ERROR_NOT_CURRENTLY_AVAILABLE

💡 Result:

In the Microsoft DirectX Graphics Infrastructure (DXGI) API, these methods and error codes are used for enumerating graphics hardware and displays. IDXGIFactory1::EnumAdapters1 This method is used to enumerate available graphics adapters (video cards) on the system [1][2]. It is part of the IDXGIFactory1 interface, which provides improvements over the original DXGI 1.0 IDXGIFactory by explicitly supporting the enumeration of adapters that do not have monitors attached [1][2]. - Usage: You pass an index (UINT) to the method to retrieve the corresponding IDXGIAdapter1 interface [1][3]. - Enumeration Order: It first returns the adapter associated with the desktop primary display, followed by other adapters with outputs, and finally adapters without outputs [1][4]. IDXGIAdapter::EnumOutputs This method is used to enumerate the outputs (monitors/displays) connected to a specific graphics adapter [5][6]. - Usage: You pass an index (UINT) to the method to retrieve the corresponding IDXGIOutput interface [5][7]. DXGI_ERROR_NOT_FOUND This error code indicates that the requested index is out of range [8]. - For IDXGIFactory1::EnumAdapters1: It is returned when the provided index is greater than or equal to the total number of adapters in the system [1][3]. - For IDXGIAdapter::EnumOutputs: It is returned when the index is greater than the number of available outputs [5][7]. It is also returned if the adapter is a WARP (Windows Advanced Rasterization Platform) software adapter, as these have no physical outputs [5][7]. DXGI_ERROR_NOT_CURRENTLY_AVAILABLE This error code indicates that the requested resource or operation is currently unavailable, though it may become available later [8][9]. - Specifically for IDXGIAdapter::EnumOutputs: This error is frequently returned if the method is called from a Session 0 process (such as a Windows service), where interactive display access is restricted [5][7]. Summary of Return Values - IDXGIFactory1::EnumAdapters1: Returns S_OK on success, or DXGI_ERROR_NOT_FOUND if the index is invalid [1][3]. - IDXGIAdapter::EnumOutputs: Returns S_OK on success, DXGI_ERROR_NOT_FOUND if the output index is out of range, or DXGI_ERROR_NOT_CURRENTLY_AVAILABLE if called from an inappropriate context like Session 0 [5][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re

path = Path("electron/native/wgc-capture/src/main.cpp")
text = path.read_text()

# Read-only structural verifier for the two enumeration loops.
patterns = [
    ("monitor lookup", r"for \(UINT adapterIndex = 0;; \+\+adapterIndex\).*?monitorAdapterFound = true;",
     r"EnumAdapters1\(adapterIndex, &adapter\).*?FAILED\(\).*?break;",
     r"EnumOutputs\(outputIndex, &output\).*?FAILED\(\).*?break;"),
    ("full diagnostic", r"for \(UINT adapterIndex = 0;; \+\+adapterIndex\).*?std::cerr << std::endl;",
     r"EnumAdapters1\(adapterIndex, &adapter\).*?FAILED\(\).*?break;",
     r"EnumOutputs\(outputIndex, &output\).*?FAILED\(\).*?break;"),
]
for name, scope_pattern, adapter_pattern, output_pattern in patterns:
    scope = re.search(scope_pattern, text, re.S)
    if not scope:
        raise SystemExit(f"{name}: scope not found")
    body = scope.group(0)
    print(f"{name}:")
    print(f"  adapter_failed_hresult_break = {bool(re.search(adapter_pattern, body, re.S))}")
    print(f"  output_failed_hresult_break = {bool(re.search(output_pattern, body, re.S))}")

# Verify the state assignments made when no matching output is found.
json_else = re.search(
    r"if \(monitorAdapterFound\).*?else \{.*?std::cout << (.*?);",
    text, re.S)
if not json_else:
    raise SystemExit("JSON fallback not found")
print("JSON fallback:")
print("  monitorAdapterFound_false_emits_null_monitorLuid = "
      + str('"monitorLuid":null' in json_else.group(1)))
print("  monitorAdapterFound_false_emits_null_sameAdapter = "
      + str('"sameAdapter":null' in json_else.group(1)))

stderr_scope = text[text.index("    // The full enumeration, to stderr"):text.index("\n}\n\nbool hasVisibleBgraContent")]
print("stderr fallback:")
print("  outputCount_zero_emits_outputs_none = "
      + str('if (outputCount == 0)' in stderr_scope and 'outputs=none' in stderr_scope))
print("  outputCount_increments_only_after_success = "
      + str(stderr_scope.index("++outputCount") > stderr_scope.index("EnumOutputs")))
PY
printf '%s\n' '--- narrow contract references ---'
rg -n -C 2 'capture-adapter|monitorLuid|sameAdapter|outputs=none|schemaVersion' \
  electron/native electron/native-bridge scripts .github/workflows \
  -g '*.ts' -g '*.js' -g '*.mjs' -g '*.json' -g '*.md' 2>/dev/null | head -n 240
printf '%s\n' '--- main call site ---'
sed -n '600,710p' electron/native/wgc-capture/src/main.cpp

Repository: getopenscreen/openscreen

Length of output: 11660


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re

source = Path("electron/native/wgc-capture/src/main.cpp").read_text()

# Extract the two relevant regions by their stable comments.
lookup_start = source.index("    // Both loops end on FAILED()")
lookup_end = source.index("    // The LUIDs are reported", lookup_start)
lookup = source[lookup_start:lookup_end]

diagnostic_start = source.index("    // The full enumeration, to stderr")
diagnostic_end = source.index("\n}\n\nbool hasVisibleBgraContent", diagnostic_start)
diagnostic = source[diagnostic_start:diagnostic_end]

for name, region in (("monitor lookup", lookup), ("full diagnostic", diagnostic)):
    adapter_guard = re.search(
        r"EnumAdapters1\s*\([^;]+;\s*if\s*\(FAILED\s*\([^)]*\)\s*\|\|\s*!adapter\)\s*\{\s*break;",
        region, re.S)
    output_guard = re.search(
        r"EnumOutputs\s*\([^;]+;\s*if\s*\(FAILED\s*\([^)]*\)\s*\|\|\s*!output\)\s*\{\s*break;",
        region, re.S)
    print(f"{name}: adapter_guard={bool(adapter_guard)} output_guard={bool(output_guard)}")

print("monitor lookup fallback:")
print("  monitorAdapterFound_false_branch =",
      "if (monitorAdapterFound)" in source and "} else {" in source[lookup_end:source.index("    std::cout << \"}\"", lookup_end)])
json_tail = source[source.index('std::cout << "null', lookup_end):source.index('std::cout << "null', lookup_end)+100]
print("  emits_monitorLuid_null =", '\\"monitorLuid\\":null' in json_tail)
print("  emits_sameAdapter_null =", '\\"sameAdapter\\":null' in json_tail)

print("full diagnostic fallback:")
print("  outputCount_starts_at_zero =", "UINT outputCount = 0;" in diagnostic)
print("  outputCount_increments_after_enum_success =",
      diagnostic.index("++outputCount") > diagnostic.index("EnumOutputs"))
print("  zero_count_emits_outputs_none =",
      "if (outputCount == 0)" in diagnostic and 'outputs=none' in diagnostic)

# Behavioral state model: the current code treats every FAILED HRESULT as the
# same loop terminator, so NOT_CURRENTLY_AVAILABLE produces the zero/unknown
# fallbacks instead of a distinct indeterminate state.
HRESULTS = {
    "S_OK": 0x00000000,
    "DXGI_ERROR_NOT_FOUND": 0x887A0002,
    "DXGI_ERROR_NOT_CURRENTLY_AVAILABLE": 0x887A0022,
}
def failed(hr):
    return bool(hr & 0x80000000)

for name, hr in HRESULTS.items():
    print(f"FAILED({name})={failed(hr)}")
print("non-terminal failure is distinct from normal end marker =",
      HRESULTS["DXGI_ERROR_NOT_CURRENTLY_AVAILABLE"] != HRESULTS["DXGI_ERROR_NOT_FOUND"])
PY
printf '%s\n' '--- exact README diagnostic contract ---'
sed -n '45,105p' electron/native/README.md

Repository: getopenscreen/openscreen

Length of output: 9414


Distinguish DXGI_ERROR_NOT_FOUND from enumeration failures.

Stop only on DXGI_ERROR_NOT_FOUND. Report DXGI_ERROR_NOT_CURRENTLY_AVAILABLE as an indeterminate enumeration failure. The current loops emit monitorLuid:null and outputs=none for both cases. Run the WGC smoke test on a real Windows system because CI does not exercise native capture.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/main.cpp` around lines 307 - 311, Update the
DXGI monitor/output enumeration loops in main so they stop only when the
enumeration call returns DXGI_ERROR_NOT_FOUND; treat
DXGI_ERROR_NOT_CURRENTLY_AVAILABLE as an indeterminate enumeration failure and
report that distinct status instead of emitting monitorLuid:null or
outputs=none. Preserve the existing no-output handling for genuinely completed
enumeration, and run the WGC smoke test on a real Windows system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant