fix(host/windows): honor the SudoVDA's real HDR state (stop wiping the user's HDR toggle)

HDR streamed nothing and "didn't persist" because build() forced the SudoVDA's
advanced-color state to match the handshake bit_depth on every build — with an
8-bit-negotiated session (the common case: clients advertise no 10-bit cap) that
meant set_advanced_color(false) on every connect, wiping a user's deliberate
Windows HDR toggle on the virtual display.

But the whole pipeline already follows the monitor's REAL HDR state: WGC captures
FP16 when HDR is on, NVENC forces Main10 + BT.2020 PQ from the 10-bit capture
format regardless of the negotiated depth (encode/nvenc.rs), and the client
auto-detects PQ from the HEVC VUI. So the negotiated bit_depth must NOT drive the
monitor's colorspace.

- build(): only ever ENABLE HDR (proactively, for a negotiated 10-bit session);
  never force it off. A user-enabled HDR session now persists and flows end-to-end.
- secure-desktop mux: gate the HDR→SDR drop (for the DDA leg) on the monitor's
  ACTUAL advanced-color state at switch time, not bit_depth — so an HDR session
  with an 8-bit handshake still drops correctly for Winlogon and restores after.
- sudovda: add advanced_color_enabled() reader (DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 21:37:04 +00:00
parent aabf9fbc83
commit af6787c0bd
2 changed files with 78 additions and 24 deletions
+31 -19
View File
@@ -2344,18 +2344,22 @@ fn virtual_stream_relay(
let target = vout.win_capture.clone().ok_or_else(|| {
anyhow!("SudoVDA target not yet an active display (needs a WDDM GPU to activate it)")
})?;
// Force the SudoVDA's advanced-color (HDR) state to MATCH the session bit depth BEFORE the WGC
// helper captures it. The advanced-color state PERSISTS on the monitor across sessions, so an
// 8-bit (SDR) session could otherwise inherit HDR left on by a prior 10-bit run (or our own
// earlier toggle) → the helper captures HDR FP16 while the encoder is 8-bit SDR → broken image.
// Runs on every build (initial + mode-switch + return-from-secure rebuild), keeping WGC's format
// consistent with the encoder. (HDR independent-flip on the secure desktop is handled separately
// by dropping to SDR for the DDA leg.)
// HDR is driven by the SudoVDA monitor's ACTUAL advanced-color state, not the handshake bit
// depth: the whole pipeline follows the monitor (WGC captures FP16 when HDR is on; NVENC forces
// Main10 + BT.2020 PQ from the 10-bit capture format regardless of the negotiated depth; the
// client auto-detects PQ from the HEVC VUI). So:
// - a negotiated 10-bit session PROACTIVELY enables HDR on the monitor (below), but
// - we must NEVER force HDR *off* here — that would wipe out a user's deliberate Windows HDR
// toggle on the virtual display on every build (the "HDR doesn't persist" bug). Leaving the
// monitor's state alone lets a user-enabled HDR session flow through end-to-end.
// The secure-desktop HDR drop (for the DDA leg) keys off the monitor's real state in the mux loop.
#[cfg(target_os = "windows")]
unsafe {
if crate::vdisplay::sudovda::set_advanced_color(target.target_id, bit_depth >= 10) {
// Let the colorspace change settle before WGC creates its capture item / detects HDR.
std::thread::sleep(std::time::Duration::from_millis(250));
if bit_depth >= 10 {
unsafe {
if crate::vdisplay::sudovda::set_advanced_color(target.target_id, true) {
// Let the colorspace change settle before WGC creates its capture item / detects HDR.
std::thread::sleep(std::time::Duration::from_millis(250));
}
}
}
let relay = HelperRelay::spawn(
@@ -2466,6 +2470,10 @@ fn virtual_stream_relay(
// decoder must resume on a keyframe — the two encoders keep independent infinite-GOP state).
let mut dda: Option<DdaPipe> = None;
let mut on_secure = false;
// Whether we dropped the SudoVDA out of HDR for the secure (DDA) leg, so we know to restore it on
// the way back. Keyed off the monitor's REAL HDR state at the moment of the switch (a user can
// toggle Windows HDR mid-session), not the handshake bit depth.
let mut dropped_hdr_for_secure = false;
let mut next = std::time::Instant::now();
let mut await_idr = false;
// Step 6 relaunch watchdog: how many times in a row the helper has died without producing a frame.
@@ -2557,10 +2565,13 @@ fn virtual_stream_relay(
if secure {
// SDR-while-secure (HDR sessions ONLY): drop the SudoVDA out of HDR so the secure
// (Winlogon) desktop renders SDR/composed — HDR fullscreen independent-flip is what made
// DDA storm ACCESS_LOST (black). For an SDR (8-bit) session the output is already SDR, so
// toggling is a needless topology change AND its matching restore on the way back would
// force the desktop into HDR the 8-bit encoder can't take (broken image).
if bit_depth >= 10 {
// DDA storm ACCESS_LOST (black). Key off the monitor's REAL HDR state (a user may have
// toggled Windows HDR on the virtual display), not the negotiated bit depth — the pipeline
// streams HDR whenever the monitor is HDR regardless of the 8/10 handshake. For an SDR
// monitor this is a no-op (no needless topology change, nothing to restore).
dropped_hdr_for_secure =
unsafe { crate::vdisplay::sudovda::advanced_color_enabled(target.target_id) };
if dropped_hdr_for_secure {
let toggled = unsafe {
crate::vdisplay::sudovda::set_advanced_color(target.target_id, false)
};
@@ -2590,10 +2601,11 @@ fn virtual_stream_relay(
dda = None; // free the secure DDA encoder; the relay (helper) is the source again
while relay.try_recv().is_ok() {} // drop secure-dwell backlog
relay.request_keyframe(); // client decoder resumes on the helper's next IDR
if bit_depth >= 10 {
// HDR session ONLY: the secure switch dropped the SudoVDA to SDR for the DDA leg, so
// here we must restore HDR AND rebuild the helper so WGC re-detects the HDR
// colorspace. An SDR session never changed the colorspace → no rebuild, no recreate.
if dropped_hdr_for_secure {
// We dropped the SudoVDA to SDR for the DDA leg → restore HDR AND rebuild the helper
// so WGC re-detects the HDR colorspace. (An SDR session never changed the colorspace
// → dropped_hdr_for_secure is false → no rebuild, no recreate.)
dropped_hdr_for_secure = false;
unsafe {
crate::vdisplay::sudovda::set_advanced_color(target.target_id, true);
}
+47 -5
View File
@@ -23,11 +23,11 @@ use windows::Win32::Devices::DeviceAndDriverInstallation::{
};
use windows::Win32::Devices::Display::{
DisplayConfigGetDeviceInfo, DisplayConfigSetDeviceInfo, GetDisplayConfigBufferSizes,
QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_MODE_INFO,
DISPLAYCONFIG_PATH_INFO, DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE,
DISPLAYCONFIG_SOURCE_DEVICE_NAME, QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY,
SDC_USE_SUPPLIED_DISPLAY_CONFIG,
QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO,
DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE,
DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO, DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO,
DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME,
QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY, SDC_USE_SUPPLIED_DISPLAY_CONFIG,
};
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
use windows::Win32::Graphics::Gdi::{
@@ -276,6 +276,48 @@ pub(crate) unsafe fn set_advanced_color(target_id: u32, enable: bool) -> bool {
false
}
/// Read the SudoVDA target's CURRENT advanced-color (HDR) state via the CCD API — i.e. whether HDR is
/// actually ON for the virtual display right now (e.g. because the user toggled it in Windows display
/// settings). The capture/encode pipeline follows the monitor's real colorspace (WGC → FP16 → NVENC
/// Main10 BT.2020 PQ), so this is the authoritative "is this an HDR session" signal — NOT the
/// handshake-negotiated bit depth. Returns false if the target isn't found / the query fails.
pub(crate) unsafe fn advanced_color_enabled(target_id: u32) -> bool {
let mut np = 0u32;
let mut nm = 0u32;
if GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut np, &mut nm).is_err() {
return false;
}
let mut paths = vec![DISPLAYCONFIG_PATH_INFO::default(); np as usize];
let mut modes = vec![DISPLAYCONFIG_MODE_INFO::default(); nm as usize];
if QueryDisplayConfig(
QDC_ONLY_ACTIVE_PATHS,
&mut np,
paths.as_mut_ptr(),
&mut nm,
modes.as_mut_ptr(),
None,
)
.is_err()
{
return false;
}
for p in paths.iter().take(np as usize) {
if p.targetInfo.id == target_id {
let mut info = DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO::default();
info.header.r#type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
info.header.size = size_of::<DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO>() as u32;
info.header.adapterId = p.targetInfo.adapterId;
info.header.id = p.targetInfo.id;
if DisplayConfigGetDeviceInfo(&mut info.header) == 0 {
// value bit 1 = advancedColorEnabled (bit 0 = advancedColorSupported).
return (info.Anonymous.value & 0x2) != 0;
}
return false;
}
}
false
}
/// Force the freshly-added SudoVDA monitor to the client's exact `WxH@Hz`. The ADD IOCTL only
/// ADVERTISES the mode; Windows otherwise activates an IDD target at a 1280x720 default, so the
/// ACTIVE mode (what DXGI Desktop Duplication captures) must be set explicitly. CDS_TEST first so a