Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions electron/native/wgc-capture/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,102 @@ std::string jsonEscape(const std::string& value) {
return result;
}

// Reports which GPU the capture device landed on, and which one actually drives
// the monitor being captured.
//
// WgcSession creates its device with D3D11CreateDevice(nullptr, ...) -- the
// default adapter -- and nothing anywhere asks whether that is the adapter that
// owns the target display. On a single-GPU machine the question does not arise.
// On a hybrid laptop, a machine with a discrete card, or one with virtual
// display adapters, the two can differ, and then every frame WGC delivers has
// crossed an adapter boundary before the caller ever touches it. That crossing
// is driver work on both GPUs, and it is the most plausible remaining candidate
// for the stop hangs in #252 / #327, which nobody has reproduced on hardware we
// control.
//
// This does not change behaviour, and deliberately so: it turns the next bug
// report into evidence instead of another round of guessing. Failures here are
// silent -- a diagnostic that can abort a recording is worse than no diagnostic.
void reportCaptureAdapters(ID3D11Device* device, HMONITOR targetMonitor) {
if (!device) {
return;
}

Microsoft::WRL::ComPtr<IDXGIDevice> dxgiDevice;
if (FAILED(device->QueryInterface(IID_PPV_ARGS(&dxgiDevice)))) {
return;
}
Microsoft::WRL::ComPtr<IDXGIAdapter> deviceAdapter;
if (FAILED(dxgiDevice->GetAdapter(&deviceAdapter))) {
return;
}
DXGI_ADAPTER_DESC deviceDesc{};
if (FAILED(deviceAdapter->GetDesc(&deviceDesc))) {
return;
}

// The device's own adapter knows its factory, so there is no need to create
// one (and no second code path to keep alive if that ever needs a flag).
Microsoft::WRL::ComPtr<IDXGIFactory1> factory;
if (FAILED(deviceAdapter->GetParent(IID_PPV_ARGS(&factory)))) {
return;
}

std::wstring monitorAdapterName;
bool monitorAdapterFound = false;
bool sameAdapter = false;
// Both loops end on FAILED(), not on DXGI_ERROR_NOT_FOUND specifically.
// NOT_FOUND is itself a failure code, so one test covers the normal end of
// the enumeration and every other way it can stop -- and the other ways are
// what matter here. EnumOutputs returns DXGI_ERROR_NOT_CURRENTLY_AVAILABLE
// to a process in session 0, and neither call fills its out-pointer when it
// fails. Testing only for NOT_FOUND left a null ComPtr to be dereferenced on
// the next line, which would take down a recording from inside the one
// function in this file that promises never to.
for (UINT adapterIndex = 0;; ++adapterIndex) {
Microsoft::WRL::ComPtr<IDXGIAdapter1> adapter;
if (FAILED(factory->EnumAdapters1(adapterIndex, &adapter)) || !adapter) {
break;
}
for (UINT outputIndex = 0;; ++outputIndex) {
Microsoft::WRL::ComPtr<IDXGIOutput> output;
if (FAILED(adapter->EnumOutputs(outputIndex, &output)) || !output) {
break;
}
DXGI_OUTPUT_DESC outputDesc{};
if (FAILED(output->GetDesc(&outputDesc)) || outputDesc.Monitor != targetMonitor) {
continue;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
DXGI_ADAPTER_DESC1 adapterDesc{};
if (FAILED(adapter->GetDesc1(&adapterDesc))) {
continue;
}
monitorAdapterName = adapterDesc.Description;
monitorAdapterFound = true;
// Compared by LUID rather than by description, because two adapters
// of the same model report the same string.
sameAdapter = adapterDesc.AdapterLuid.LowPart == deviceDesc.AdapterLuid.LowPart &&
adapterDesc.AdapterLuid.HighPart == deviceDesc.AdapterLuid.HighPart;
}
if (monitorAdapterFound) {
break;
}
}

std::cout << "{\"event\":\"capture-adapter\",\"schemaVersion\":2,\"deviceAdapter\":\""
<< jsonEscape(wideToUtf8(deviceDesc.Description)) << "\",\"monitorAdapter\":";
if (monitorAdapterFound) {
std::cout << "\"" << jsonEscape(wideToUtf8(monitorAdapterName)) << "\",\"sameAdapter\":"
<< (sameAdapter ? "true" : "false");
} 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 << "}" << std::endl;
}

bool hasVisibleBgraContent(const std::vector<BYTE>& frame) {
if (frame.size() < 4) {
return false;
Expand Down Expand Up @@ -489,6 +585,7 @@ int main(int argc, char* argv[]) {
std::cout << "{\"event\":\"ready\",\"schemaVersion\":2}" << std::endl;

WgcSession session;
HMONITOR capturedMonitor = nullptr;
if (config.sourceType == "display") {
HMONITOR monitor = findMonitorForCapture(
config.displayId,
Expand All @@ -497,6 +594,7 @@ int main(int argc, char* argv[]) {
std::cerr << "ERROR: Could not resolve monitor" << std::endl;
return 1;
}
capturedMonitor = monitor;
if (!session.initialize(monitor, config.fps, config.captureCursor)) {
std::cerr << "ERROR: Failed to initialize WGC display session" << std::endl;
return 1;
Expand All @@ -507,6 +605,9 @@ int main(int argc, char* argv[]) {
std::cerr << "ERROR: Native window capture requires a valid HWND" << std::endl;
return 1;
}
// A window is captured by whichever display it currently sits on, which
// is the adapter that matters for the same reason a monitor's does.
capturedMonitor = MonitorFromWindow(window, MONITOR_DEFAULTTONEAREST);
if (!session.initialize(window, config.fps, config.captureCursor)) {
std::cerr << "ERROR: Failed to initialize WGC window session" << std::endl;
return 1;
Expand All @@ -516,6 +617,8 @@ int main(int argc, char* argv[]) {
return 1;
}

reportCaptureAdapters(session.device(), capturedMonitor);

// WGC owns the captured texture size. Encoding must use that exact size
// until a dedicated GPU scaling pass is introduced; CopyResource requires
// matching resource dimensions.
Expand Down
8 changes: 8 additions & 0 deletions scripts/diagnostic-tool/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,16 @@ Flags:
- `-d, --duration <seconds>` recording length before sending stop (default 10)
- `-o, --output <path>` output JSON path (default `./openscreen-diagnostic-<timestamp>.json`)
- `--window` capture a window instead of the full display (default: display)
- `--system-audio` also capture system (loopback) audio
- `--mic` also capture the default microphone
- `-h, --help` show help

The audio flags matter for reproducing a stop hang. Audio and video writes take
the same sink-writer lock, so a run without audio has nothing to contend with
and can pass on a machine where the app hangs every time. If you are reporting a
hang that happens in the app but not here, re-run with whichever sources the
failing recording used: `--system-audio`, `--mic`, or both together.

Or use the bundled launcher:
- Windows: `diagnostic.bat`
- macOS / Linux: `./diagnostic.sh`
Expand Down
24 changes: 21 additions & 3 deletions scripts/diagnostic-tool/diagnostic.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ function parseArgs(argv) {
duration: 10_000,
output: null,
source: "display",
systemAudio: false,
mic: false,
help: false,
};
const requireNumber = (raw, flag) => {
Expand All @@ -68,6 +70,10 @@ function parseArgs(argv) {
opts.source = value;
} else if (arg === "--window") {
opts.source = "window";
} else if (arg === "--system-audio") {
opts.systemAudio = true;
} else if (arg === "--mic") {
opts.mic = true;
} else if (arg === "--help" || arg === "-h") {
opts.help = true;
} else if (arg.startsWith("--")) {
Expand All @@ -88,7 +94,15 @@ Flags:
-o, --output <path> Output JSON path (default: ./openscreen-diagnostic-<timestamp>.json)
--source <display|window> Capture source type (default: display)
--window Shortcut for --source window
--system-audio Also capture system (loopback) audio
--mic Also capture the default microphone
-h, --help Show this help

The audio flags are off by default, which is why a plain run cannot reproduce a
hang that only happens with audio: an audio write and a video write contend for
the same sink-writer lock, and with no audio there is nothing to contend with.
If a recording hangs in the app but not here, re-run with whichever sources the
failing recording used -- --system-audio, --mic, or both.
`);
}

Expand Down Expand Up @@ -135,8 +149,8 @@ function buildConfig(opts) {
displayW: 1920,
displayH: 1080,
hasDisplayBounds: true,
captureSystemAudio: false,
captureMic: false,
captureSystemAudio: opts.systemAudio,
captureMic: opts.mic,
captureCursor: false,
microphoneDeviceId: "default",
microphoneDeviceName: "",
Expand All @@ -163,7 +177,11 @@ function run(opts) {
const helper = findHelper();
console.log(`[diag] helper: ${helper.path}`);
console.log(`[diag] platform: ${process.platform}-${process.arch}`);
console.log(`[diag] duration: ${opts.duration}ms, source: ${opts.source}`);
const audioSummary =
[opts.systemAudio && "system", opts.mic && "mic"].filter(Boolean).join("+") || "none";
console.log(
`[diag] duration: ${opts.duration}ms, source: ${opts.source}, audio: ${audioSummary}`,
);

const config = buildConfig(opts);
config.outputs.screenPath = config.outputPath;
Expand Down
Loading