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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user