mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-09 22:11:02 +03:00
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F
This commit is contained in:
@@ -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
|
//! With HDR enabled Windows composes the desktop as linear scRGB in
|
||||||
//! R16G16B16A16_FLOAT, and SDR "white" sits at the user's SDR content
|
//! R16G16B16A16_FLOAT, and SDR "white" sits at the user's SDR content
|
||||||
//! brightness (DISPLAYCONFIG_SDR_WHITE_LEVEL) rather than at 1.0. The legacy
|
//! brightness (DISPLAYCONFIG_SDR_WHITE_LEVEL) rather than at 1.0. The legacy
|
||||||
//! DuplicateOutput converts that to BGRA8 by clipping, which is the washed-out
|
//! 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,
|
//! 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
|
//! clamps, and applies the sRGB transfer, so SDR content comes out exactly as
|
||||||
//! and HDR highlights clip at white.
|
//! 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 conversion is automatic and stays on the controlled side on purpose:
|
||||||
//! the controller renders through Flutter external textures, which are 8-bit
|
//! the controller renders through Flutter external textures, which are 8-bit
|
||||||
@@ -19,7 +25,7 @@ use super::ComPtr;
|
|||||||
use hbb_common::log;
|
use hbb_common::log;
|
||||||
use std::{
|
use std::{
|
||||||
io, mem, ptr,
|
io, mem, ptr,
|
||||||
sync::atomic::AtomicBool,
|
sync::{atomic::AtomicBool, OnceLock},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use winapi::{
|
use winapi::{
|
||||||
@@ -45,10 +51,18 @@ use winapi::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Set once the tone-map cannot be built on this machine (no d3dcompiler, shader
|
/// Set once the tone-map can never work in this process (no d3dcompiler, the
|
||||||
/// creation failed, ...). The capturer then stops asking DXGI for float frames.
|
/// 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);
|
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 = "\
|
const VS_SRC: &str = "\
|
||||||
float4 main(uint id : SV_VertexID) : SV_Position {
|
float4 main(uint id : SV_VertexID) : SV_Position {
|
||||||
float2 uv = float2((id << 1) & 2, id & 2);
|
float2 uv = float2((id << 1) & 2, id & 2);
|
||||||
@@ -330,17 +344,26 @@ type D3DCompileFn = unsafe extern "system" fn(
|
|||||||
*mut *mut ID3DBlob,
|
*mut *mut ID3DBlob,
|
||||||
) -> HRESULT;
|
) -> HRESULT;
|
||||||
|
|
||||||
// Loaded on demand: the compiler DLL is only needed on HDR desktops, and an
|
static D3D_COMPILE: OnceLock<Result<D3DCompileFn, String>> = OnceLock::new();
|
||||||
// import-time link would make every install depend on it.
|
|
||||||
unsafe fn load_d3d_compile() -> io::Result<D3DCompileFn> {
|
// 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<D3DCompileFn> {
|
||||||
|
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<D3DCompileFn, String> {
|
||||||
let name: Vec<u16> = "d3dcompiler_47.dll\0".encode_utf16().collect();
|
let name: Vec<u16> = "d3dcompiler_47.dll\0".encode_utf16().collect();
|
||||||
let module = LoadLibraryW(name.as_ptr());
|
let module = LoadLibraryW(name.as_ptr());
|
||||||
if module.is_null() {
|
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 _);
|
let f = GetProcAddress(module, b"D3DCompile\0".as_ptr() as _);
|
||||||
if f.is_null() {
|
if f.is_null() {
|
||||||
return Err(other("D3DCompile not exported"));
|
return Err("D3DCompile not exported".into());
|
||||||
}
|
}
|
||||||
Ok(mem::transmute::<_, D3DCompileFn>(f))
|
Ok(mem::transmute::<_, D3DCompileFn>(f))
|
||||||
}
|
}
|
||||||
@@ -379,7 +402,10 @@ unsafe fn compile_shader(
|
|||||||
if !code.is_null() {
|
if !code.is_null() {
|
||||||
(*(code as *mut IUnknown)).Release();
|
(*(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))
|
Ok(ComPtr(code))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,21 +221,42 @@ impl Capturer {
|
|||||||
if self.hdr.is_none() {
|
if self.hdr.is_none() {
|
||||||
match hdr::HdrToSdr::new(self.device.0, self.context.0, &self.display.desc.DeviceName) {
|
match hdr::HdrToSdr::new(self.device.0, self.context.0, &self.display.desc.DeviceName) {
|
||||||
Ok(hdr) => self.hdr = Some(hdr),
|
Ok(hdr) => self.hdr = Some(hdr),
|
||||||
Err(err) => {
|
Err(err) => return self.abandon_tonemap(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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match self.hdr.as_mut() {
|
let converted = match self.hdr.as_mut() {
|
||||||
Some(hdr) => hdr.convert(source, desc),
|
Some(hdr) => hdr.convert(source, desc),
|
||||||
None => Err(io::Error::new(io::ErrorKind::Other, "no tone-map")),
|
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<T>(&mut self, err: io::Error) -> io::Result<T> {
|
||||||
|
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(
|
fn create_rotations(
|
||||||
device: *mut ID3D11Device,
|
device: *mut ID3D11Device,
|
||||||
context: *mut ID3D11DeviceContext,
|
context: *mut ID3D11DeviceContext,
|
||||||
|
|||||||
Reference in New Issue
Block a user