From 696451b08b2ac3be8da8a97ebf770ab77c1ca067 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 2 Sep 2026 13:04:23 +0800 Subject: [PATCH] scrap: stay on DXGI when the HDR conversion fails, load the compiler once Review follow-up: - A tone-map failure surfaced as a capture error, and the capture loop answers any DXGI error by switching the capturer to GDI for the rest of its life. The capturer now drops the tone-map, re-creates the duplication with the legacy DuplicateOutput (DXGI's clipped BGRA8, the pre-HDR behaviour) and returns WouldBlock, so the session stays on DXGI and simply fetches the next frame. - Only failures that cannot succeed anywhere in the process (no d3dcompiler_47.dll, shaders that do not compile) set the global UNAVAILABLE flag; D3D object creation failures stay with the capturer that hit them, so another adapter or a recreated capturer tries again. - D3DCompile is resolved once through a OnceLock instead of a LoadLibrary per capturer that was never freed. - The module doc now states what this pass is: normalization of an HDR desktop to SDR, with everything above SDR white clipping, not a tone map, and why a roll-off is deliberately not applied. The earlier claim that clipping matches what the local user sees was wrong for HDR content on an HDR display. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F --- libs/scrap/src/dxgi/hdr.rs | 50 +++++++++++++++++++++++++++++--------- libs/scrap/src/dxgi/mod.rs | 37 ++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/libs/scrap/src/dxgi/hdr.rs b/libs/scrap/src/dxgi/hdr.rs index 8a5df5129..5957ab602 100644 --- a/libs/scrap/src/dxgi/hdr.rs +++ b/libs/scrap/src/dxgi/hdr.rs @@ -1,12 +1,18 @@ -//! HDR desktop -> SDR frame tone-map for Desktop Duplication. +//! HDR desktop -> SDR normalization for Desktop Duplication frames. //! //! With HDR enabled Windows composes the desktop as linear scRGB in //! R16G16B16A16_FLOAT, and SDR "white" sits at the user's SDR content //! brightness (DISPLAYCONFIG_SDR_WHITE_LEVEL) rather than at 1.0. The legacy //! DuplicateOutput converts that to BGRA8 by clipping, which is the washed-out //! picture reported for HDR hosts. This pass divides by the SDR white level, -//! clamps, and applies the sRGB transfer, so SDR content round-trips exactly -//! and HDR highlights clip at white. +//! clamps, and applies the sRGB transfer, so SDR content comes out exactly as +//! it would from an SDR desktop. +//! +//! It is a normalization, not a tone map: anything brighter than SDR white +//! (HDR video, HDR games) clips to white on the SDR viewer, where the local +//! HDR display would show it brighter than white. A roll-off would have to +//! move SDR white below 1.0 to make headroom, trading the accuracy of the SDR +//! content this pass exists for, so it is deliberately not done. //! //! The conversion is automatic and stays on the controlled side on purpose: //! the controller renders through Flutter external textures, which are 8-bit @@ -19,7 +25,7 @@ use super::ComPtr; use hbb_common::log; use std::{ io, mem, ptr, - sync::atomic::AtomicBool, + sync::{atomic::AtomicBool, OnceLock}, time::{Duration, Instant}, }; use winapi::{ @@ -45,10 +51,18 @@ use winapi::{ }, }; -/// Set once the tone-map cannot be built on this machine (no d3dcompiler, shader -/// creation failed, ...). The capturer then stops asking DXGI for float frames. +/// Set once the tone-map can never work in this process (no d3dcompiler, the +/// shaders do not compile). Capturers then stop asking DXGI for float frames. +/// Device-specific failures are not recorded here; the capturer that hit one +/// re-duplicates without the tone-map on its own. pub static UNAVAILABLE: AtomicBool = AtomicBool::new(false); +/// Failures no capturer on this machine can recover from, as opposed to +/// device-specific ones that a recreated capturer may not hit again. +pub fn is_permanent(err: &io::Error) -> bool { + err.kind() == io::ErrorKind::Unsupported +} + const VS_SRC: &str = "\ float4 main(uint id : SV_VertexID) : SV_Position { float2 uv = float2((id << 1) & 2, id & 2); @@ -330,17 +344,26 @@ type D3DCompileFn = unsafe extern "system" fn( *mut *mut ID3DBlob, ) -> HRESULT; -// Loaded on demand: the compiler DLL is only needed on HDR desktops, and an -// import-time link would make every install depend on it. -unsafe fn load_d3d_compile() -> io::Result { +static D3D_COMPILE: OnceLock> = OnceLock::new(); + +// Loaded once per process and kept: the compiler DLL is only needed on HDR +// desktops, and an import-time link would make every install depend on it. +fn load_d3d_compile() -> io::Result { + D3D_COMPILE + .get_or_init(|| unsafe { find_d3d_compile() }) + .clone() + .map_err(|e| io::Error::new(io::ErrorKind::Unsupported, e)) +} + +unsafe fn find_d3d_compile() -> Result { let name: Vec = "d3dcompiler_47.dll\0".encode_utf16().collect(); let module = LoadLibraryW(name.as_ptr()); if module.is_null() { - return Err(other("d3dcompiler_47.dll not available")); + return Err("d3dcompiler_47.dll not available".into()); } let f = GetProcAddress(module, b"D3DCompile\0".as_ptr() as _); if f.is_null() { - return Err(other("D3DCompile not exported")); + return Err("D3DCompile not exported".into()); } Ok(mem::transmute::<_, D3DCompileFn>(f)) } @@ -379,7 +402,10 @@ unsafe fn compile_shader( if !code.is_null() { (*(code as *mut IUnknown)).Release(); } - return Err(other(format!("D3DCompile failed: {hr:#x} {msg}"))); + return Err(io::Error::new( + io::ErrorKind::Unsupported, + format!("D3DCompile failed: {hr:#x} {msg}"), + )); } Ok(ComPtr(code)) } diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index f75055c19..5c78664c2 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -221,21 +221,42 @@ impl Capturer { if self.hdr.is_none() { match hdr::HdrToSdr::new(self.device.0, self.context.0, &self.display.desc.DeviceName) { Ok(hdr) => self.hdr = Some(hdr), - Err(err) => { - hdr::UNAVAILABLE.store(true, std::sync::atomic::Ordering::Relaxed); - hbb_common::log::error!( - "HDR tone-map unavailable, next capturer falls back to clipped BGRA: {err}" - ); - return Err(err); - } + Err(err) => return self.abandon_tonemap(err), } } - match self.hdr.as_mut() { + let converted = match self.hdr.as_mut() { Some(hdr) => hdr.convert(source, desc), None => Err(io::Error::new(io::ErrorKind::Other, "no tone-map")), + }; + match converted { + Ok(texture) => Ok(texture), + Err(err) => self.abandon_tonemap(err), } } + // Drops the tone-map and re-duplicates the output the legacy way, so DXGI + // hands over clipped BGRA8 (the pre-HDR behaviour) and the session stays on + // DXGI instead of being switched to GDI by the capture loop, which treats + // any other error that way. The caller sees WouldBlock and asks again. + unsafe fn abandon_tonemap(&mut self, err: io::Error) -> io::Result { + if hdr::is_permanent(&err) { + hdr::UNAVAILABLE.store(true, std::sync::atomic::Ordering::Relaxed); + } + hbb_common::log::error!("HDR tone-map failed, re-duplicating without it: {err}"); + self.hdr = None; + (*self.duplication.0).ReleaseFrame(); + self.duplication = ComPtr(ptr::null_mut()); + let mut duplication = ptr::null_mut(); + wrap_hresult( + (*self.display.inner.0).DuplicateOutput(self.device.0 as *mut _, &mut duplication), + )?; + self.duplication = ComPtr(duplication); + let mut desc: DXGI_OUTDUPL_DESC = mem::zeroed(); + (*duplication).GetDesc(&mut desc); + self.fastlane = desc.DesktopImageInSystemMemory == TRUE; + Err(io::ErrorKind::WouldBlock.into()) + } + fn create_rotations( device: *mut ID3D11Device, context: *mut ID3D11DeviceContext,