mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-10 22:41:05 +03:00
Compare commits
9 Commits
c4df2e6da2
...
hdr-tonema
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce968c02dd | ||
|
|
89d444b950 | ||
|
|
792618e59a | ||
|
|
2f28a96f6a | ||
|
|
2f3a6789de | ||
|
|
6e502cde2e | ||
|
|
3730ab0733 | ||
|
|
696451b08b | ||
|
|
a7df138fc7 |
@@ -37,7 +37,7 @@ serde = {version="1.0", features=["derive"]}
|
||||
[dependencies.winapi]
|
||||
version = "0.3"
|
||||
default-features = true
|
||||
features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11", "winuser", "winerror", "errhandlingapi", "libloaderapi"]
|
||||
features = ["dxgi", "dxgi1_2", "dxgi1_5", "dxgi1_6", "d3d11", "winuser", "winerror", "errhandlingapi", "libloaderapi"]
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
block = "0.1"
|
||||
|
||||
629
libs/scrap/src/dxgi/hdr.rs
Normal file
629
libs/scrap/src/dxgi/hdr.rs
Normal file
@@ -0,0 +1,629 @@
|
||||
//! 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 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.
|
||||
//!
|
||||
//! Windows 11 22H2 also composes Advanced Color SDR (WCG) desktops in FP16,
|
||||
//! but there 1.0 is the display's reference white rather than 80 nits and no
|
||||
//! SDR white level applies. IDXGIOutput6 tells the two apart, and for a
|
||||
//! non-HDR output the pass only applies the scRGB -> sRGB transfer.
|
||||
//!
|
||||
//! 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, OnceLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use winapi::{
|
||||
ctypes::c_void,
|
||||
shared::{
|
||||
basetsd::SIZE_T,
|
||||
dxgi::{CreateDXGIFactory1, IDXGIFactory1, IID_IDXGIFactory1, DXGI_OUTPUT_DESC},
|
||||
dxgi1_2::IDXGIOutput1,
|
||||
dxgi1_6::{IDXGIOutput6, IID_IDXGIOutput6, DXGI_OUTPUT_DESC1},
|
||||
dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
dxgitype::{DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020, DXGI_SAMPLE_DESC},
|
||||
minwindef::{FALSE, LPCVOID, UINT, ULONG},
|
||||
ntdef::{LONG, LPCSTR, WCHAR},
|
||||
winerror::S_OK,
|
||||
},
|
||||
um::{
|
||||
d3d11::*,
|
||||
d3dcommon::{ID3DBlob, ID3DInclude, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST, D3D_SHADER_MACRO},
|
||||
libloaderapi::{GetProcAddress, LoadLibraryExW, LOAD_LIBRARY_SEARCH_SYSTEM32},
|
||||
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 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);
|
||||
return float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}";
|
||||
|
||||
const PS_SRC: &str = "\
|
||||
Texture2D<float4> 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);
|
||||
}";
|
||||
|
||||
const OUTPUT_STATE_REFRESH: Duration = Duration::from_secs(1);
|
||||
|
||||
pub struct HdrToSdr {
|
||||
device: ComPtr<ID3D11Device>,
|
||||
context: ComPtr<ID3D11DeviceContext>,
|
||||
vs: ComPtr<ID3D11VertexShader>,
|
||||
ps: ComPtr<ID3D11PixelShader>,
|
||||
params: ComPtr<ID3D11Buffer>,
|
||||
target: ComPtr<ID3D11Texture2D>,
|
||||
rtv: ComPtr<ID3D11RenderTargetView>,
|
||||
srv: ComPtr<ID3D11ShaderResourceView>,
|
||||
// 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],
|
||||
// Advanced Color state is read from `output6`, which is re-enumerated from a
|
||||
// fresh factory whenever `factory` stops being current.
|
||||
factory: ComPtr<IDXGIFactory1>,
|
||||
output6: ComPtr<IDXGIOutput6>,
|
||||
is_hdr: bool,
|
||||
// DISPLAYCONFIG units (1000 == 80 nits == scRGB 1.0). `None` when it could
|
||||
// not be read, in which case 80 nits is assumed until it can.
|
||||
sdr_white_level: Option<u32>,
|
||||
queried_at: Instant,
|
||||
}
|
||||
|
||||
impl HdrToSdr {
|
||||
pub fn new(
|
||||
device: *mut ID3D11Device,
|
||||
context: *mut ID3D11DeviceContext,
|
||||
output: *mut IDXGIOutput1,
|
||||
device_name: &[WCHAR; 32],
|
||||
) -> io::Result<Self> {
|
||||
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);
|
||||
|
||||
// Not found leaves the factory null, so the first refresh enumerates
|
||||
// again instead of trusting the capturer's possibly stale output.
|
||||
let (factory, mut output6) = enumerate_output6(device_name);
|
||||
if output6.is_null() {
|
||||
output6 = query_output6(output as *mut IUnknown);
|
||||
}
|
||||
// Float frames are only requested where IDXGIOutput6 exists, so an
|
||||
// unreadable description still comes from an HDR-capable stack.
|
||||
let is_hdr = output_is_hdr(output6.0).unwrap_or(true);
|
||||
let sdr_white_level = if is_hdr {
|
||||
query_sdr_white_level(device_name)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if is_hdr && sdr_white_level.is_none() {
|
||||
log::warn!(
|
||||
"HDR output but the SDR white level cannot be read (needs Windows 10 1709+), \
|
||||
assuming 80 nits until it can"
|
||||
);
|
||||
}
|
||||
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!(
|
||||
"scRGB desktop conversion ready, hdr {is_hdr}, 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,
|
||||
factory,
|
||||
output6,
|
||||
is_hdr,
|
||||
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_output_state();
|
||||
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(())
|
||||
}
|
||||
|
||||
// Advanced Color state is dynamic: HDR can be switched on or off, or a WCG
|
||||
// desktop can turn into an HDR one, without the duplication being lost. An
|
||||
// output's description is a snapshot, so once the factory is no longer
|
||||
// current a new factory and output are needed to see the new state, as the
|
||||
// GetDesc1 docs require.
|
||||
unsafe fn refresh_output_state(&mut self) {
|
||||
if self.queried_at.elapsed() < OUTPUT_STATE_REFRESH {
|
||||
return;
|
||||
}
|
||||
self.queried_at = Instant::now();
|
||||
if self.factory.is_null() || (*self.factory.0).IsCurrent() == FALSE {
|
||||
let (factory, output6) = enumerate_output6(&self.device_name);
|
||||
if !output6.is_null() {
|
||||
self.factory = factory;
|
||||
self.output6 = output6;
|
||||
} else {
|
||||
// Keep reading the old output, but enumerate again next time.
|
||||
self.factory = ComPtr(ptr::null_mut());
|
||||
}
|
||||
}
|
||||
let is_hdr = output_is_hdr(self.output6.0).unwrap_or(self.is_hdr);
|
||||
let level = if is_hdr {
|
||||
query_sdr_white_level(&self.device_name)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// A transiently unreadable level keeps the last known one.
|
||||
if is_hdr == self.is_hdr && (level.is_none() || level == self.sdr_white_level) {
|
||||
return;
|
||||
}
|
||||
log::info!(
|
||||
"output changed: hdr {} -> {is_hdr}, sdr white level {:?} -> {level:?}",
|
||||
self.is_hdr,
|
||||
self.sdr_white_level
|
||||
);
|
||||
if is_hdr && level.is_none() {
|
||||
log::warn!(
|
||||
"HDR output but the SDR white level cannot be read, assuming 80 nits until it can"
|
||||
);
|
||||
}
|
||||
self.is_hdr = is_hdr;
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// `None` is either a non-HDR (WCG) output, where 1.0 already is the display's
|
||||
// reference white, or an HDR output whose level is unknown; both use 1.0.
|
||||
fn params_data(sdr_white_level: Option<u32>) -> [f32; 4] {
|
||||
[
|
||||
1000.0 / sdr_white_level.unwrap_or(1000) as f32,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
]
|
||||
}
|
||||
|
||||
// FP16 desktop composition means either HDR (scene-referred, 1.0 == 80 nits)
|
||||
// or, since Windows 11 22H2, Advanced Color SDR (display-referred), and only
|
||||
// IDXGIOutput6 (Windows 10 1703) tells them apart. `None` when it cannot be
|
||||
// read right now.
|
||||
unsafe fn output_is_hdr(output6: *mut IDXGIOutput6) -> Option<bool> {
|
||||
if output6.is_null() {
|
||||
return None;
|
||||
}
|
||||
let mut desc: DXGI_OUTPUT_DESC1 = mem::zeroed();
|
||||
if (*output6).GetDesc1(&mut desc) != S_OK {
|
||||
return None;
|
||||
}
|
||||
Some(desc.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020)
|
||||
}
|
||||
|
||||
unsafe fn query_output6(object: *mut IUnknown) -> ComPtr<IDXGIOutput6> {
|
||||
let mut output6: *mut IDXGIOutput6 = ptr::null_mut();
|
||||
if !object.is_null() {
|
||||
(*object).QueryInterface(
|
||||
&IID_IDXGIOutput6,
|
||||
&mut output6 as *mut *mut _ as *mut *mut _,
|
||||
);
|
||||
}
|
||||
ComPtr(output6)
|
||||
}
|
||||
|
||||
// A fresh factory sees the current display configuration; the output is found
|
||||
// by its GDI name because the outputs of a stale factory keep stale descriptions.
|
||||
// Returns both or neither: a non-null factory guarantees the output came from
|
||||
// its topology, so a caller that sees a null factory knows to enumerate again.
|
||||
unsafe fn enumerate_output6(
|
||||
device_name: &[WCHAR; 32],
|
||||
) -> (ComPtr<IDXGIFactory1>, ComPtr<IDXGIOutput6>) {
|
||||
let mut factory: *mut c_void = ptr::null_mut();
|
||||
if CreateDXGIFactory1(&IID_IDXGIFactory1, &mut factory) != S_OK {
|
||||
return (ComPtr(ptr::null_mut()), ComPtr(ptr::null_mut()));
|
||||
}
|
||||
let factory = ComPtr(factory as *mut IDXGIFactory1);
|
||||
let mut adapter_index = 0;
|
||||
loop {
|
||||
let mut adapter = ptr::null_mut();
|
||||
if (*factory.0).EnumAdapters1(adapter_index, &mut adapter) != S_OK {
|
||||
break;
|
||||
}
|
||||
let adapter = ComPtr(adapter);
|
||||
adapter_index += 1;
|
||||
let mut output_index = 0;
|
||||
loop {
|
||||
let mut output = ptr::null_mut();
|
||||
if (*adapter.0).EnumOutputs(output_index, &mut output) != S_OK {
|
||||
break;
|
||||
}
|
||||
let output = ComPtr(output);
|
||||
output_index += 1;
|
||||
let mut desc: DXGI_OUTPUT_DESC = mem::zeroed();
|
||||
if (*output.0).GetDesc(&mut desc) == S_OK && wide_eq(&desc.DeviceName, device_name) {
|
||||
let output6 = query_output6(output.0 as *mut IUnknown);
|
||||
if output6.is_null() {
|
||||
return (ComPtr(ptr::null_mut()), ComPtr(ptr::null_mut()));
|
||||
}
|
||||
return (factory, output6);
|
||||
}
|
||||
}
|
||||
}
|
||||
(ComPtr(ptr::null_mut()), ComPtr(ptr::null_mut()))
|
||||
}
|
||||
|
||||
fn other(msg: impl Into<String>) -> 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;
|
||||
|
||||
static D3D_COMPILE: OnceLock<Result<D3DCompileFn, String>> = 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<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 module = LoadLibraryExW(name.as_ptr(), ptr::null_mut(), LOAD_LIBRARY_SEARCH_SYSTEM32);
|
||||
if module.is_null() {
|
||||
return Err("d3dcompiler_47.dll not available".into());
|
||||
}
|
||||
let f = GetProcAddress(module, b"D3DCompile\0".as_ptr() as _);
|
||||
if f.is_null() {
|
||||
return Err("D3DCompile not exported".into());
|
||||
}
|
||||
Ok(mem::transmute::<_, D3DCompileFn>(f))
|
||||
}
|
||||
|
||||
unsafe fn compile_shader(
|
||||
compile: D3DCompileFn,
|
||||
src: &str,
|
||||
target: &[u8],
|
||||
) -> io::Result<ComPtr<ID3DBlob>> {
|
||||
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(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
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). `None`
|
||||
/// when the query fails (before Windows 10 1709) or reports 0.
|
||||
fn query_sdr_white_level(device_name: &[WCHAR; 32]) -> Option<u32> {
|
||||
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<DISPLAYCONFIG_PATH_INFO> = vec![mem::zeroed(); n_paths as usize];
|
||||
let mut modes: Vec<DISPLAYCONFIG_MODE_INFO> = 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::<DISPLAYCONFIG_SOURCE_DEVICE_NAME>() 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::<DISPLAYCONFIG_SDR_WHITE_LEVEL>() as _;
|
||||
white.header.adapterId = path.targetInfo.adapterId;
|
||||
white.header.id = path.targetInfo.id;
|
||||
if DisplayConfigGetDeviceInfo(&mut white.header) == 0 && white.SDRWhiteLevel != 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)]
|
||||
}
|
||||
@@ -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_6::*,
|
||||
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<hdr::HdrToSdr>,
|
||||
}
|
||||
|
||||
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,102 @@ 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.
|
||||
// Only where IDXGIOutput6 (Windows 10 1703) exists, since that is what later
|
||||
// tells an HDR desktop from a WCG one; Microsoft's duplication sample gates
|
||||
// the float request the same way.
|
||||
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 output6: *mut IDXGIOutput6 = ptr::null_mut();
|
||||
(*display.inner.0).QueryInterface(
|
||||
&IID_IDXGIOutput6,
|
||||
&mut output6 as *mut *mut _ as *mut *mut _,
|
||||
);
|
||||
if !output6.is_null() {
|
||||
let output6 = ComPtr(output6);
|
||||
let formats = [DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_FORMAT_B8G8R8A8_UNORM];
|
||||
let hres = (*output6.0).DuplicateOutput1(
|
||||
device as *mut _,
|
||||
0,
|
||||
formats.len() as UINT,
|
||||
formats.as_ptr(),
|
||||
duplication,
|
||||
);
|
||||
if hres == S_OK {
|
||||
return hres;
|
||||
}
|
||||
hbb_common::log::warn!(
|
||||
"HDR DuplicateOutput1 failed: hr={:#x}, fallback=DuplicateOutput",
|
||||
hres as u32
|
||||
);
|
||||
}
|
||||
}
|
||||
(*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.inner.0,
|
||||
&self.display.desc.DeviceName,
|
||||
) {
|
||||
Ok(hdr) => self.hdr = Some(hdr),
|
||||
Err(err) => return self.abandon_tonemap(err),
|
||||
}
|
||||
}
|
||||
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). If re-duplication fails,
|
||||
// switch to GDI before returning. 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();
|
||||
let result = wrap_hresult(
|
||||
(*self.display.inner.0).DuplicateOutput(self.device.0 as *mut _, &mut duplication),
|
||||
);
|
||||
if let Err(err) = result {
|
||||
if self.set_gdi() {
|
||||
return Err(io::ErrorKind::WouldBlock.into());
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
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,
|
||||
@@ -330,6 +427,9 @@ impl Capturer {
|
||||
}
|
||||
|
||||
unsafe fn load_frame(&mut self, timeout: UINT) -> io::Result<(*const u8, i32)> {
|
||||
if self.duplication.0.is_null() {
|
||||
return Err(io::ErrorKind::AddrNotAvailable.into());
|
||||
}
|
||||
let mut frame = ptr::null_mut();
|
||||
#[allow(invalid_value)]
|
||||
let mut info = mem::MaybeUninit::uninit().assume_init();
|
||||
@@ -365,6 +465,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 +491,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 +598,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,
|
||||
@@ -575,6 +689,9 @@ impl Capturer {
|
||||
}
|
||||
|
||||
fn unmap(&self) {
|
||||
if self.duplication.0.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
(*self.duplication.0).ReleaseFrame();
|
||||
if self.fastlane {
|
||||
|
||||
Reference in New Issue
Block a user