From af6787c0bd8a70500fe38ccc0670b8364161a7b0 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Tue, 16 Jun 2026 21:37:04 +0000 Subject: [PATCH] fix(host/windows): honor the SudoVDA's real HDR state (stop wiping the user's HDR toggle) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/punktfunk-host/src/m3.rs | 50 +++++++++++------- crates/punktfunk-host/src/vdisplay/sudovda.rs | 52 +++++++++++++++++-- 2 files changed, 78 insertions(+), 24 deletions(-) diff --git a/crates/punktfunk-host/src/m3.rs b/crates/punktfunk-host/src/m3.rs index 36d4aa1..bab4992 100644 --- a/crates/punktfunk-host/src/m3.rs +++ b/crates/punktfunk-host/src/m3.rs @@ -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 = 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); } diff --git a/crates/punktfunk-host/src/vdisplay/sudovda.rs b/crates/punktfunk-host/src/vdisplay/sudovda.rs index 4f4db87..fb1472e 100644 --- a/crates/punktfunk-host/src/vdisplay/sudovda.rs +++ b/crates/punktfunk-host/src/vdisplay/sudovda.rs @@ -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::() 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