From a7df138fc729590276c36e573a6839c0fe375d8c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 2 Sep 2026 11:50:53 +0800 Subject: [PATCH] scrap: tone-map HDR desktops to SDR in the Windows capturer With HDR enabled, Windows composes the desktop as linear scRGB in R16G16B16A16_FLOAT and places SDR white at the user's "SDR content brightness" (DISPLAYCONFIG_SDR_WHITE_LEVEL / 1000, e.g. 2.5 for 200 nits) rather than at 1.0. The legacy IDXGIOutput1::DuplicateOutput we used always converts that surface to BGRA8, and it does so by clipping, so everything above ~40% linear brightness lands on white. That is the washed-out / overexposed / high-contrast picture reported for HDR hosts in #5368, #9951, #12707 and discussion #7652. Ask for the desktop with IDXGIOutput5::DuplicateOutput1 and the format list [R16G16B16A16_FLOAT, B8G8R8A8_UNORM]. An SDR desktop still yields BGRA8 and takes the unchanged path. A float frame is converted on the GPU by a small pixel shader: divide by the SDR white level, clamp, apply the sRGB transfer. SDR content round-trips exactly, HDR highlights clip at white, the same result the local user sees. The white level is read per output through DisplayConfigGetDeviceInfo and refreshed once a second, so dragging the Windows slider tracks remotely. The converted BGRA8 texture feeds both the staging-copy (CPU) path and the vram (hwcodec texture) path; rotation is unchanged. Toggling HDR mid-session invalidates the duplication as before, and the recreated capturer re-detects the format. The shaders are compiled at runtime from a dynamically loaded d3dcompiler_47.dll so no new import is added to the binary. If the DLL, the compile or any D3D object creation fails, a process-wide flag makes later capturers fall back to the legacy DuplicateOutput, i.e. today's behaviour. This deliberately stops at SDR on the controlled side, with no option and no protocol change, which is how OBS, Sunshine (client without HDR) and macOS screen capture handle it. Real HDR pass-through would need 10-bit capture, Main10/AV1-10 encoders in the hwcodec fork, colour metadata on the wire and, decisively, an HDR output path on the controller: Flutter's desktop external textures are BGRA8888/RGBA8888 only on every platform, so nothing would be visible. When that changes it should follow the Sunshine/Moonlight pattern: an hdr capability bit advertised by the controller behind an explicit user toggle, negotiated like i444. Type-checked against x86_64-pc-windows-msvc (with and without the vram feature); not yet run on an HDR machine, which needs Windows 10 1703+ with HDR on. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F (cherry picked from commit ca3c2f8ac7d1549a40855411b0539a69c82d7223) --- libs/scrap/src/dxgi/hdr.rs | 464 +++++++++++++++++++++++++++++++++++++ libs/scrap/src/dxgi/mod.rs | 81 ++++++- 2 files changed, 541 insertions(+), 4 deletions(-) create mode 100644 libs/scrap/src/dxgi/hdr.rs 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,