#528

perf(wgc): validate display state off the capture thread

OpenRamazanKarawants to merge intofrom

co-written by Claude Opus 5.5

Summary

Since 2.0, WGC capture re-enumerates DXGI once per second to catch HDR transitions that a stale factory can hide. The check runs on the capture thread at the top of the pacing loop, right before the sleep until the next frame slot. This PR runs the same probe on a background thread. The capture loop only reads a latched "structural change" flag.

Why

  • On an idle RX 7900 XT desktop, the same factory creation, adapter/output enumeration and HDR query measured 0.61 ms median, 1.67 ms p95 and 1.73 ms max (25 samples at 1 s cadence). It can take longer while the display configuration is changing.
  • If the probe pushes the wake past its slot, the pacing group busts and re-anchors through a blocking snapshot. That can happen once per second.
  • In #461, a tester reported that a 2.0.0-beta.3-based build is less smooth than 1.19.0-beta.3. This check is new in the 2.0 line and runs in every WGC session, which is the default capture method on 23H2 and later. It is a candidate cause, not a confirmed one.

Changes

  • The enumeration logic moves from refresh_output_after_nonstructural_change() into a free function, probe_output_after_nonstructural_change(). It works on a value snapshot of the captured output: adapter LUID, output description, input geometry and HDR state. The member function calls it and still swaps factory/adapter/output on the capture thread, so the stale-factory continuation path is unchanged.
  • New capture_policy::background_output_validator_t, a std::jthread wrapper. It probes immediately, then once per period. On a structural change it latches the result and exits. Stopping is prompt through its stop token.
  • The periodic check no longer replaces output every second. This also removes a race with is_hdr() / get_hdr_metadata() readers on other threads.
  • Behavior is preserved: the first probe runs at capture start, the period is 1 s, and a structural change still triggers reinit with the same log line. Continuation refreshes keep the compared geometry and HDR state, so the snapshot stays valid for the whole capture session.
  • Probes slower than 2 ms are logged at debug level.

Validation

  • New component test test_component_capture_output_validation checks three things:
    • the probe runs on a different thread, latches a change and then stops probing;
    • the first probe runs immediately;
    • teardown does not wait out the period.
  • It passes under Linux GCC with AddressSanitizer/UndefinedBehaviorSanitizer (20 repeats) and ThreadSanitizer (10 repeats), and under MinGW GCC 16 on Windows (20 repeats).
  • display_base.cpp passes a MinGW GCC 16 syntax check on this branch.
  • The same change on the native-AMF test branch (2.0.0-beta.3 base, identical display_base.cpp) passed a full Windows CI build and packaging: https://github.com/RamazanKara/Vibepollo/actions/runs/36472734373. That build is installed and running on an RX 7900 XT host.
  • There is no stream measurement yet. No smoothness or latency improvement is claimed until someone compares it against a comparable stream.

Activity

4
Nonary1mo ago

Thanks for the PR! After reviewing this a second time, I feel making the HDR/display-state validation event-based, with background polling as a fallback, would be the better option.

On supported Windows versions, we could subscribe to the captured monitor's DisplayInformation.AdvancedColorInfoChanged event through IDisplayInformationStaticsInterop::GetForMonitor, then request capture reinitialization when the HDR state actually changes. If event registration is unavailable or fails, we can fall back to the background validation loop proposed here.

That would keep the validation work off the capture thread while avoiding continuous display enumeration when notifications are available.

RamazanKara1mo ago

Done. On Windows 11 22H2 and later, the validator now subscribes to the captured monitor's AdvancedColorInfoChanged through IDisplayInformationStaticsInterop::GetForMonitor and only enumerates DXGI around those notifications: right away, then once per second for a 5 s settle window, since DXGI can report the new colorspace late. If the interface or the registration isn't available, it keeps the once-per-second background loop.

One detail: registering the event fails unless the thread that created the DisplayInformation has a DispatcherQueue, so a small STA thread owns the queue and its message loop. The host now links windowsapp for it, like the WGC helper.

On Windows 11 build 26200, registration succeeds and 20 start/stop cycles are clean, and the validator tests cover the notification mode. I haven't toggled HDR during a stream yet to confirm the reinit end to end.