diff --git a/libs/scrap/src/dxgi/hdr.rs b/libs/scrap/src/dxgi/hdr.rs new file mode 100644 index 000000000..8a5df5129 --- /dev/null +++ b/libs/scrap/src/dxgi/hdr.rs @@ -0,0 +1,464 @@ +//! HDR desktop -> SDR frame tone-map for Desktop Duplication. +//! +//! 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. +//! +//! The conversion is automatic and stays on the controlled side on purpose: +//! the controller renders through Flutter external textures, which are 8-bit +//! on every desktop platform, so there is nothing to gain from sending HDR. +//! Real HDR pass-through, if the renderer ever supports it, should follow the +//! Sunshine/Moonlight pattern instead: an `hdr` capability bit advertised by +//! the controller behind an explicit user toggle, negotiated like i444. + +use super::ComPtr; +use hbb_common::log; +use std::{ + io, mem, ptr, + sync::atomic::AtomicBool, + time::{Duration, Instant}, +}; +use winapi::{ + shared::{ + basetsd::SIZE_T, + dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM, + dxgitype::DXGI_SAMPLE_DESC, + minwindef::{LPCVOID, UINT, ULONG}, + ntdef::{LONG, LPCSTR, WCHAR}, + winerror::S_OK, + }, + um::{ + d3d11::*, + d3dcommon::{ID3DBlob, ID3DInclude, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, D3D_SHADER_MACRO}, + libloaderapi::{GetProcAddress, LoadLibraryW}, + unknwnbase::IUnknown, + wingdi::{ + DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_HEADER, + DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO, DISPLAYCONFIG_SOURCE_DEVICE_NAME, + DISPLAYCONFIG_TOPOLOGY_ID, + }, + winnt::HRESULT, + }, +}; + +/// 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. +pub static UNAVAILABLE: AtomicBool = AtomicBool::new(false); + +const VS_SRC: &str = "\ +float4 main(uint id : SV_VertexID) : SV_Position { + float2 uv = float2((id << 1) & 2, id & 2); + return float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); +}"; + +const PS_SRC: &str = "\ +Texture2D src : register(t0); +cbuffer Params : register(b0) { float inv_sdr_white; float3 pad; }; +float4 main(float4 pos : SV_Position) : SV_Target { + float3 lin = saturate(src.Load(int3(pos.xy, 0)).rgb * inv_sdr_white); + float3 lo = lin * 12.92; + float3 hi = 1.055 * pow(lin, 1.0 / 2.4) - 0.055; + return float4(lerp(hi, lo, step(lin, 0.0031308)), 1.0); +}"; + +/// DISPLAYCONFIG units: 1000 == 80 nits == scRGB 1.0. +const DEFAULT_SDR_WHITE_LEVEL: u32 = 1000; +const SDR_WHITE_LEVEL_REFRESH: Duration = Duration::from_secs(1); + +pub struct HdrToSdr { + device: ComPtr, + context: ComPtr, + vs: ComPtr, + ps: ComPtr, + params: ComPtr, + target: ComPtr, + rtv: ComPtr, + srv: ComPtr, + // Texture `srv` was created for. The view keeps it alive, so the address + // cannot be recycled behind our back. + srv_source: *mut ID3D11Texture2D, + width: u32, + height: u32, + device_name: [WCHAR; 32], + sdr_white_level: u32, + queried_at: Instant, +} + +impl HdrToSdr { + pub fn new( + device: *mut ID3D11Device, + context: *mut ID3D11DeviceContext, + device_name: &[WCHAR; 32], + ) -> io::Result { + unsafe { + if device.is_null() || context.is_null() { + return Err(other("no d3d11 device")); + } + (*device).AddRef(); + let device = ComPtr(device); + (*context).AddRef(); + let context = ComPtr(context); + + let compile = load_d3d_compile()?; + let vs_code = compile_shader(compile, VS_SRC, b"vs_4_0\0")?; + let ps_code = compile_shader(compile, PS_SRC, b"ps_4_0\0")?; + let mut vs = ptr::null_mut(); + check( + (*device.0).CreateVertexShader( + (*vs_code.0).GetBufferPointer(), + (*vs_code.0).GetBufferSize(), + ptr::null_mut(), + &mut vs, + ), + "CreateVertexShader", + )?; + let vs = ComPtr(vs); + let mut ps = ptr::null_mut(); + check( + (*device.0).CreatePixelShader( + (*ps_code.0).GetBufferPointer(), + (*ps_code.0).GetBufferSize(), + ptr::null_mut(), + &mut ps, + ), + "CreatePixelShader", + )?; + let ps = ComPtr(ps); + + let sdr_white_level = + query_sdr_white_level(device_name).unwrap_or(DEFAULT_SDR_WHITE_LEVEL); + let init = params_data(sdr_white_level); + let desc = D3D11_BUFFER_DESC { + ByteWidth: mem::size_of_val(&init) as _, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: D3D11_BIND_CONSTANT_BUFFER, + CPUAccessFlags: 0, + MiscFlags: 0, + StructureByteStride: 0, + }; + let data = D3D11_SUBRESOURCE_DATA { + pSysMem: init.as_ptr() as _, + SysMemPitch: 0, + SysMemSlicePitch: 0, + }; + let mut params = ptr::null_mut(); + check( + (*device.0).CreateBuffer(&desc, &data, &mut params), + "CreateBuffer", + )?; + let params = ComPtr(params); + log::info!("HDR tone-map ready, sdr white level {sdr_white_level}"); + + Ok(Self { + device, + context, + vs, + ps, + params, + target: ComPtr(ptr::null_mut()), + rtv: ComPtr(ptr::null_mut()), + srv: ComPtr(ptr::null_mut()), + srv_source: ptr::null_mut(), + width: 0, + height: 0, + device_name: *device_name, + sdr_white_level, + queried_at: Instant::now(), + }) + } + } + + /// Renders `source` (R16G16B16A16_FLOAT) into an owned B8G8R8A8_UNORM + /// texture of the same size and returns it. The texture stays valid until + /// the next call. + pub fn convert( + &mut self, + source: *mut ID3D11Texture2D, + desc: &D3D11_TEXTURE2D_DESC, + ) -> io::Result<*mut ID3D11Texture2D> { + unsafe { + self.refresh_sdr_white_level(); + self.ensure_target(desc.Width, desc.Height)?; + self.ensure_source_view(source)?; + + let ctx = self.context.0; + let rtv = self.rtv.0; + let srv = self.srv.0; + let params = self.params.0; + let viewport = D3D11_VIEWPORT { + TopLeftX: 0.0, + TopLeftY: 0.0, + Width: self.width as f32, + Height: self.height as f32, + MinDepth: 0.0, + MaxDepth: 1.0, + }; + (*ctx).OMSetRenderTargets(1, &rtv, ptr::null_mut()); + (*ctx).OMSetBlendState(ptr::null_mut(), &[0.0; 4], 0xffff_ffff); + (*ctx).OMSetDepthStencilState(ptr::null_mut(), 0); + (*ctx).RSSetState(ptr::null_mut()); + (*ctx).RSSetViewports(1, &viewport); + (*ctx).IASetInputLayout(ptr::null_mut()); + (*ctx).IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + (*ctx).VSSetShader(self.vs.0, ptr::null(), 0); + (*ctx).PSSetShader(self.ps.0, ptr::null(), 0); + (*ctx).PSSetConstantBuffers(0, 1, ¶ms); + (*ctx).PSSetShaderResources(0, 1, &srv); + (*ctx).Draw(3, 0); + // Unbind so the next frame's copy and the encoder never see the + // target as a live render target or the desktop image as a bound + // shader input. + let no_srv: *mut ID3D11ShaderResourceView = ptr::null_mut(); + (*ctx).PSSetShaderResources(0, 1, &no_srv); + (*ctx).OMSetRenderTargets(0, ptr::null(), ptr::null_mut()); + Ok(self.target.0) + } + } + + unsafe fn ensure_target(&mut self, width: u32, height: u32) -> io::Result<()> { + if !self.target.is_null() && self.width == width && self.height == height { + return Ok(()); + } + let desc = D3D11_TEXTURE2D_DESC { + Width: width, + Height: height, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_B8G8R8A8_UNORM, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE, + CPUAccessFlags: 0, + MiscFlags: D3D11_RESOURCE_MISC_SHARED, + }; + let mut target = ptr::null_mut(); + check( + (*self.device.0).CreateTexture2D(&desc, ptr::null(), &mut target), + "CreateTexture2D", + )?; + let target = ComPtr(target); + let mut rtv = ptr::null_mut(); + check( + (*self.device.0).CreateRenderTargetView(target.0 as *mut _, ptr::null(), &mut rtv), + "CreateRenderTargetView", + )?; + self.rtv = ComPtr(rtv); + self.target = target; + self.width = width; + self.height = height; + Ok(()) + } + + unsafe fn ensure_source_view(&mut self, source: *mut ID3D11Texture2D) -> io::Result<()> { + if !self.srv.is_null() && self.srv_source == source { + return Ok(()); + } + let mut srv = ptr::null_mut(); + check( + (*self.device.0).CreateShaderResourceView(source as *mut _, ptr::null(), &mut srv), + "CreateShaderResourceView", + )?; + self.srv = ComPtr(srv); + self.srv_source = source; + Ok(()) + } + + unsafe fn refresh_sdr_white_level(&mut self) { + if self.queried_at.elapsed() < SDR_WHITE_LEVEL_REFRESH { + return; + } + self.queried_at = Instant::now(); + let Some(level) = query_sdr_white_level(&self.device_name) else { + return; + }; + if level == self.sdr_white_level || level == 0 { + return; + } + log::info!( + "sdr white level changed {} -> {level}", + self.sdr_white_level + ); + self.sdr_white_level = level; + let data = params_data(level); + (*self.context.0).UpdateSubresource( + self.params.0 as *mut _, + 0, + ptr::null(), + data.as_ptr() as _, + 0, + 0, + ); + } +} + +fn params_data(sdr_white_level: u32) -> [f32; 4] { + [1000.0 / sdr_white_level.max(1) as f32, 0.0, 0.0, 0.0] +} + +fn other(msg: impl Into) -> io::Error { + io::Error::new(io::ErrorKind::Other, msg.into()) +} + +fn check(hr: HRESULT, what: &str) -> io::Result<()> { + if hr == S_OK { + Ok(()) + } else { + Err(other(format!("{what} failed: {hr:#x}"))) + } +} + +// D3DCompile(pSrcData, SrcDataSize, pSourceName, pDefines, pInclude, +// pEntrypoint, pTarget, Flags1, Flags2, ppCode, ppErrorMsgs) +type D3DCompileFn = unsafe extern "system" fn( + LPCVOID, + SIZE_T, + LPCSTR, + *const D3D_SHADER_MACRO, + *mut ID3DInclude, + LPCSTR, + LPCSTR, + UINT, + UINT, + *mut *mut ID3DBlob, + *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 { + 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")); + } + let f = GetProcAddress(module, b"D3DCompile\0".as_ptr() as _); + if f.is_null() { + return Err(other("D3DCompile not exported")); + } + Ok(mem::transmute::<_, D3DCompileFn>(f)) +} + +unsafe fn compile_shader( + compile: D3DCompileFn, + src: &str, + target: &[u8], +) -> io::Result> { + let mut code = ptr::null_mut(); + let mut errors = ptr::null_mut(); + let hr = compile( + src.as_ptr() as _, + src.len(), + ptr::null(), + ptr::null(), + ptr::null_mut(), + b"main\0".as_ptr() as _, + target.as_ptr() as _, + 0, + 0, + &mut code, + &mut errors, + ); + let errors = ComPtr(errors); + if hr != S_OK || code.is_null() { + let msg = if errors.is_null() { + String::new() + } else { + let bytes = std::slice::from_raw_parts( + (*errors.0).GetBufferPointer() as *const u8, + (*errors.0).GetBufferSize(), + ); + String::from_utf8_lossy(bytes).into_owned() + }; + if !code.is_null() { + (*(code as *mut IUnknown)).Release(); + } + return Err(other(format!("D3DCompile failed: {hr:#x} {msg}"))); + } + Ok(ComPtr(code)) +} + +const DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL: u32 = 11; +const QDC_ONLY_ACTIVE_PATHS: u32 = 2; + +#[repr(C)] +#[allow(non_snake_case)] +struct DISPLAYCONFIG_SDR_WHITE_LEVEL { + header: DISPLAYCONFIG_DEVICE_INFO_HEADER, + SDRWhiteLevel: ULONG, +} + +#[link(name = "user32")] +extern "system" { + fn GetDisplayConfigBufferSizes( + flags: u32, + numPathArrayElements: *mut u32, + numModeInfoArrayElements: *mut u32, + ) -> LONG; + fn QueryDisplayConfig( + flags: u32, + numPathArrayElements: *mut u32, + pathArray: *mut DISPLAYCONFIG_PATH_INFO, + numModeInfoArrayElements: *mut u32, + modeInfoArray: *mut DISPLAYCONFIG_MODE_INFO, + currentTopologyId: *mut DISPLAYCONFIG_TOPOLOGY_ID, + ) -> LONG; + fn DisplayConfigGetDeviceInfo(requestPacket: *mut DISPLAYCONFIG_DEVICE_INFO_HEADER) -> LONG; +} + +/// SDR white level of the output whose GDI name is `device_name` +/// (e.g. `\\.\DISPLAY1`), in DISPLAYCONFIG units (1000 == 80 nits). +fn query_sdr_white_level(device_name: &[WCHAR; 32]) -> Option { + unsafe { + let mut n_paths = 0u32; + let mut n_modes = 0u32; + if GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut n_paths, &mut n_modes) != 0 { + return None; + } + let mut paths: Vec = vec![mem::zeroed(); n_paths as usize]; + let mut modes: Vec = vec![mem::zeroed(); n_modes as usize]; + if QueryDisplayConfig( + QDC_ONLY_ACTIVE_PATHS, + &mut n_paths, + paths.as_mut_ptr(), + &mut n_modes, + modes.as_mut_ptr(), + ptr::null_mut(), + ) != 0 + { + return None; + } + for path in &paths[..n_paths as usize] { + let mut source: DISPLAYCONFIG_SOURCE_DEVICE_NAME = mem::zeroed(); + source.header._type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + source.header.size = mem::size_of::() as _; + source.header.adapterId = path.sourceInfo.adapterId; + source.header.id = path.sourceInfo.id; + if DisplayConfigGetDeviceInfo(&mut source.header) != 0 + || !wide_eq(&source.viewGdiDeviceName, device_name) + { + continue; + } + let mut white: DISPLAYCONFIG_SDR_WHITE_LEVEL = mem::zeroed(); + white.header._type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL; + white.header.size = mem::size_of::() as _; + white.header.adapterId = path.targetInfo.adapterId; + white.header.id = path.targetInfo.id; + if DisplayConfigGetDeviceInfo(&mut white.header) == 0 { + return Some(white.SDRWhiteLevel); + } + } + None + } +} + +fn wide_eq(a: &[WCHAR], b: &[WCHAR]) -> bool { + let end = |s: &[WCHAR]| s.iter().position(|&c| c == 0).unwrap_or(s.len()); + a[..end(a)] == b[..end(b)] +} diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 1f5296954..f75055c19 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -1,18 +1,20 @@ use std::{io, mem, ptr, slice}; pub mod gdi; pub use gdi::CapturerGDI; +pub mod hdr; pub mod mag; use winapi::{ shared::{ dxgi::*, dxgi1_2::*, + dxgi1_5::*, + dxgiformat::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT}, dxgitype::*, minwindef::{DWORD, FALSE, TRUE, UINT}, ntdef::LONG, windef::{HMONITOR, RECT}, winerror::*, - // dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE}, }, um::{ d3d11::*, d3dcommon::D3D_DRIVER_TYPE_UNKNOWN, unknwnbase::IUnknown, wingdi::*, @@ -58,6 +60,7 @@ pub struct Capturer { output_texture: bool, adapter_desc1: DXGI_ADAPTER_DESC1, rotate: Rotate, + hdr: Option, } impl Capturer { @@ -105,7 +108,7 @@ impl Capturer { } } else { res = wrap_hresult(unsafe { - let hres = (*display.inner.0).DuplicateOutput(device.0 as *mut _, &mut duplication); + let hres = Self::duplicate_output(&display, device.0, &mut duplication); if hres != S_OK { gdi_capturer = display.create_gdi(); println!("Fallback to GDI"); @@ -161,7 +164,8 @@ impl Capturer { device, context, duplication: ComPtr(duplication), - fastlane: desc.DesktopImageInSystemMemory == TRUE, + fastlane: desc.DesktopImageInSystemMemory == TRUE + && desc.ModeDesc.Format != DXGI_FORMAT_R16G16B16A16_FLOAT, surface: ComPtr(ptr::null_mut()), texture: ComPtr(ptr::null_mut()), width: display.width() as usize, @@ -174,9 +178,64 @@ impl Capturer { output_texture: false, adapter_desc1, rotate, + hdr: None, }) } + // Asks for the float desktop that HDR mode composes so it can be tone-mapped; + // the legacy call would hand back DXGI's clipped BGRA8 conversion instead. + unsafe fn duplicate_output( + display: &Display, + device: *mut ID3D11Device, + duplication: &mut *mut IDXGIOutputDuplication, + ) -> HRESULT { + if !hdr::UNAVAILABLE.load(std::sync::atomic::Ordering::Relaxed) { + let mut output5: *mut IDXGIOutput5 = ptr::null_mut(); + (*display.inner.0).QueryInterface( + &IID_IDXGIOutput5, + &mut output5 as *mut *mut _ as *mut *mut _, + ); + if !output5.is_null() { + let output5 = ComPtr(output5); + let formats = [DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_FORMAT_B8G8R8A8_UNORM]; + let hres = (*output5.0).DuplicateOutput1( + device as *mut _, + 0, + formats.len() as UINT, + formats.as_ptr(), + duplication, + ); + if hres == S_OK { + return hres; + } + } + } + (*display.inner.0).DuplicateOutput(device as *mut _, duplication) + } + + unsafe fn tonemap( + &mut self, + source: *mut ID3D11Texture2D, + desc: &D3D11_TEXTURE2D_DESC, + ) -> io::Result<*mut ID3D11Texture2D> { + 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); + } + } + } + match self.hdr.as_mut() { + Some(hdr) => hdr.convert(source, desc), + None => Err(io::Error::new(io::ErrorKind::Other, "no tone-map")), + } + } + fn create_rotations( device: *mut ID3D11Device, context: *mut ID3D11DeviceContext, @@ -365,6 +424,12 @@ impl Capturer { let mut texture_desc = mem::MaybeUninit::uninit().assume_init(); (*texture.0).GetDesc(&mut texture_desc); + let mut source = texture.0; + if texture_desc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT { + source = self.tonemap(texture.0, &texture_desc)?; + (*source).GetDesc(&mut texture_desc); + } + texture_desc.Usage = D3D11_USAGE_STAGING; texture_desc.BindFlags = 0; texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; @@ -385,7 +450,7 @@ impl Capturer { &mut surface as *mut *mut _ as *mut *mut _, ); - (*self.context.0).CopyResource(readable.0 as *mut _, texture.0 as *mut _); + (*self.context.0).CopyResource(readable.0 as *mut _, source as *mut _); Ok(surface) } @@ -492,6 +557,14 @@ impl Capturer { let texture = ComPtr(texture); self.texture = texture; + let mut frame_desc: D3D11_TEXTURE2D_DESC = mem::zeroed(); + (*self.texture.0).GetDesc(&mut frame_desc); + if frame_desc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT { + let converted = self.tonemap(self.texture.0, &frame_desc)?; + (*converted).AddRef(); + self.texture = ComPtr(converted); + } + let mut final_texture = self.texture.0 as *mut c_void; let mut rotation = match self.display.rotation() { DXGI_MODE_ROTATION_ROTATE90 => 90,