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,20 +2344,24 @@ fn virtual_stream_relay(
|
|||||||
let target = vout.win_capture.clone().ok_or_else(|| {
|
let target = vout.win_capture.clone().ok_or_else(|| {
|
||||||
anyhow!("SudoVDA target not yet an active display (needs a WDDM GPU to activate it)")
|
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
|
// HDR is driven by the SudoVDA monitor's ACTUAL advanced-color state, not the handshake bit
|
||||||
// helper captures it. The advanced-color state PERSISTS on the monitor across sessions, so an
|
// depth: the whole pipeline follows the monitor (WGC captures FP16 when HDR is on; NVENC forces
|
||||||
// 8-bit (SDR) session could otherwise inherit HDR left on by a prior 10-bit run (or our own
|
// Main10 + BT.2020 PQ from the 10-bit capture format regardless of the negotiated depth; the
|
||||||
// earlier toggle) → the helper captures HDR FP16 while the encoder is 8-bit SDR → broken image.
|
// client auto-detects PQ from the HEVC VUI). So:
|
||||||
// Runs on every build (initial + mode-switch + return-from-secure rebuild), keeping WGC's format
|
// - a negotiated 10-bit session PROACTIVELY enables HDR on the monitor (below), but
|
||||||
// consistent with the encoder. (HDR independent-flip on the secure desktop is handled separately
|
// - we must NEVER force HDR *off* here — that would wipe out a user's deliberate Windows HDR
|
||||||
// by dropping to SDR for the DDA leg.)
|
// 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")]
|
#[cfg(target_os = "windows")]
|
||||||
|
if bit_depth >= 10 {
|
||||||
unsafe {
|
unsafe {
|
||||||
if crate::vdisplay::sudovda::set_advanced_color(target.target_id, bit_depth >= 10) {
|
if crate::vdisplay::sudovda::set_advanced_color(target.target_id, true) {
|
||||||
// Let the colorspace change settle before WGC creates its capture item / detects HDR.
|
// Let the colorspace change settle before WGC creates its capture item / detects HDR.
|
||||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let relay = HelperRelay::spawn(
|
let relay = HelperRelay::spawn(
|
||||||
&target,
|
&target,
|
||||||
(mode.width, mode.height, effective_hz),
|
(mode.width, mode.height, effective_hz),
|
||||||
@@ -2466,6 +2470,10 @@ fn virtual_stream_relay(
|
|||||||
// decoder must resume on a keyframe — the two encoders keep independent infinite-GOP state).
|
// decoder must resume on a keyframe — the two encoders keep independent infinite-GOP state).
|
||||||
let mut dda: Option<DdaPipe> = None;
|
let mut dda: Option<DdaPipe> = None;
|
||||||
let mut on_secure = false;
|
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 next = std::time::Instant::now();
|
||||||
let mut await_idr = false;
|
let mut await_idr = false;
|
||||||
// Step 6 relaunch watchdog: how many times in a row the helper has died without producing a frame.
|
// 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 {
|
if secure {
|
||||||
// SDR-while-secure (HDR sessions ONLY): drop the SudoVDA out of HDR so the 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
|
// (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
|
// DDA storm ACCESS_LOST (black). Key off the monitor's REAL HDR state (a user may have
|
||||||
// toggling is a needless topology change AND its matching restore on the way back would
|
// toggled Windows HDR on the virtual display), not the negotiated bit depth — the pipeline
|
||||||
// force the desktop into HDR the 8-bit encoder can't take (broken image).
|
// streams HDR whenever the monitor is HDR regardless of the 8/10 handshake. For an SDR
|
||||||
if bit_depth >= 10 {
|
// 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 {
|
let toggled = unsafe {
|
||||||
crate::vdisplay::sudovda::set_advanced_color(target.target_id, false)
|
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
|
dda = None; // free the secure DDA encoder; the relay (helper) is the source again
|
||||||
while relay.try_recv().is_ok() {} // drop secure-dwell backlog
|
while relay.try_recv().is_ok() {} // drop secure-dwell backlog
|
||||||
relay.request_keyframe(); // client decoder resumes on the helper's next IDR
|
relay.request_keyframe(); // client decoder resumes on the helper's next IDR
|
||||||
if bit_depth >= 10 {
|
if dropped_hdr_for_secure {
|
||||||
// HDR session ONLY: the secure switch dropped the SudoVDA to SDR for the DDA leg, so
|
// We dropped the SudoVDA to SDR for the DDA leg → restore HDR AND rebuild the helper
|
||||||
// here we must restore HDR AND rebuild the helper so WGC re-detects the HDR
|
// so WGC re-detects the HDR colorspace. (An SDR session never changed the colorspace
|
||||||
// colorspace. An SDR session never changed the colorspace → no rebuild, no recreate.
|
// → dropped_hdr_for_secure is false → no rebuild, no recreate.)
|
||||||
|
dropped_hdr_for_secure = false;
|
||||||
unsafe {
|
unsafe {
|
||||||
crate::vdisplay::sudovda::set_advanced_color(target.target_id, true);
|
crate::vdisplay::sudovda::set_advanced_color(target.target_id, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ use windows::Win32::Devices::DeviceAndDriverInstallation::{
|
|||||||
};
|
};
|
||||||
use windows::Win32::Devices::Display::{
|
use windows::Win32::Devices::Display::{
|
||||||
DisplayConfigGetDeviceInfo, DisplayConfigSetDeviceInfo, GetDisplayConfigBufferSizes,
|
DisplayConfigGetDeviceInfo, DisplayConfigSetDeviceInfo, GetDisplayConfigBufferSizes,
|
||||||
QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
|
QueryDisplayConfig, SetDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO,
|
||||||
DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_MODE_INFO,
|
DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE,
|
||||||
DISPLAYCONFIG_PATH_INFO, DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE,
|
DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO, DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO,
|
||||||
DISPLAYCONFIG_SOURCE_DEVICE_NAME, QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY,
|
DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE, DISPLAYCONFIG_SOURCE_DEVICE_NAME,
|
||||||
SDC_USE_SUPPLIED_DISPLAY_CONFIG,
|
QDC_ONLY_ACTIVE_PATHS, SDC_ALLOW_CHANGES, SDC_APPLY, SDC_USE_SUPPLIED_DISPLAY_CONFIG,
|
||||||
};
|
};
|
||||||
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
|
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
|
||||||
use windows::Win32::Graphics::Gdi::{
|
use windows::Win32::Graphics::Gdi::{
|
||||||
@@ -276,6 +276,48 @@ pub(crate) unsafe fn set_advanced_color(target_id: u32, enable: bool) -> bool {
|
|||||||
false
|
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
|
/// 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
|
/// 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
|
/// ACTIVE mode (what DXGI Desktop Duplication captures) must be set explicitly. CDS_TEST first so a
|
||||||
|
|||||||
Reference in New Issue
Block a user