Compare commits

...

9 Commits

Author SHA1 Message Date
21pages
ce968c02dd scrap: load the D3D compiler securely from System32
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-09-03 10:36:12 +08:00
21pages
89d444b950 scrap: avoid null duplication after HDR fallback failure
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-09-02 23:08:34 +08:00
21pages
792618e59a scrap: log DuplicateOutput1 failures before fallback
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-09-02 21:02:09 +08:00
rustdesk
2f28a96f6a scrap: keep enumerate_output6's both-or-neither promise when IDXGIOutput6 is missing
Review nit: a matched output whose IDXGIOutput6 query fails returned the
fresh factory next to a null output, which let the constructor pair a
current factory with the capturer's fallback output. Return neither in
that case so the next refresh enumerates again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F
2026-09-02 16:14:22 +08:00
rustdesk
2f3a6789de scrap: replace the DXGI factory and output together when re-reading HDR state
Review follow-up: when the fresh factory did not find the output by name,
the refresh kept the new, current factory next to the old output, and
because IsCurrent then reported TRUE it never enumerated again, so the
stale output could stay forever. enumerate_output6 now returns both or
neither, and the refresh only replaces the pair together; a null factory
forces another enumeration on the next refresh while the old output is
still read in the meantime. The constructor gets the same guarantee for
free, since a failed lookup leaves the factory null there too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F
2026-09-02 16:00:47 +08:00
rustdesk
6e502cde2e scrap: re-read the Advanced Color state while capturing, gate FP16 on IDXGIOutput6
Review follow-up:

- The HDR/WCG decision was taken once when the conversion was created.
  HDR can be switched on or off, and a WCG desktop can turn into an HDR
  one, without the duplication being invalidated, so the choice between
  "divide by the SDR white level" and "no scaling" could go stale. The
  once-a-second refresh now re-reads it: it keeps an IDXGIFactory1,
  and when IsCurrent reports FALSE it creates a fresh factory and
  re-finds the output by GDI name, since a stale factory's outputs keep
  stale descriptions (this is the procedure the GetDesc1 docs require).
  On a change it switches modes, re-reads the white level and updates
  the shader constant; an unreadable level keeps the last known one.

- Float frames are only requested when IDXGIOutput6 exists, as
  Microsoft's duplication sample does. That interface is also what tells
  HDR from WCG, so there is no longer a state where FP16 is requested
  without being able to interpret it. IDXGIOutput6 dates from Windows 10
  1703, not 1803 as the previous commit said; 1803 added IDXGIFactory6.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F
2026-09-02 15:44:23 +08:00
rustdesk
3730ab0733 scrap: only apply the SDR white level to outputs DXGI reports as HDR
Review follow-up:

- An FP16 desktop is not necessarily HDR. Since Windows 11 22H2 an
  Advanced Color (WCG) SDR display is composed in FP16 scRGB too, and
  there 1.0 is the display's reference white, not 80 nits, so the
  SDRWhiteLevel scaling does not apply (Microsoft: "Reference white
  applies only to HDR displays"). Query IDXGIOutput6::GetDesc1 and treat
  the output as HDR only for DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020,
  the documented Win32 check; a non-HDR FP16 frame now gets just the
  scRGB -> sRGB transfer. Without IDXGIOutput6 (before Windows 10 1803)
  FP16 can only mean HDR.

- The SDR white level is now Option: a failed query or a reported 0 is
  "unknown" on both the initial and the refresh path, an HDR output with
  an unknown level logs a warning and keeps the 80-nit assumption until
  the periodic re-query succeeds. DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL
  needs Windows 10 1709, not 1703 as an earlier message said.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F
2026-09-02 15:02:57 +08:00
rustdesk
696451b08b 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
2026-09-02 13:04:23 +08:00
rustdesk
a7df138fc7 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F
(cherry picked from commit ca3c2f8ac7d1549a40855411b0539a69c82d7223)
2026-09-02 12:36:48 +08:00
3 changed files with 751 additions and 5 deletions

View File

@@ -37,7 +37,7 @@ serde = {version="1.0", features=["derive"]}
[dependencies.winapi] [dependencies.winapi]
version = "0.3" version = "0.3"
default-features = true 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] [target.'cfg(target_os = "macos")'.dependencies]
block = "0.1" block = "0.1"

629
libs/scrap/src/dxgi/hdr.rs Normal file
View 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, &params);
(*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)]
}

View File

@@ -1,18 +1,20 @@
use std::{io, mem, ptr, slice}; use std::{io, mem, ptr, slice};
pub mod gdi; pub mod gdi;
pub use gdi::CapturerGDI; pub use gdi::CapturerGDI;
pub mod hdr;
pub mod mag; pub mod mag;
use winapi::{ use winapi::{
shared::{ shared::{
dxgi::*, dxgi::*,
dxgi1_2::*, dxgi1_2::*,
dxgi1_6::*,
dxgiformat::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT},
dxgitype::*, dxgitype::*,
minwindef::{DWORD, FALSE, TRUE, UINT}, minwindef::{DWORD, FALSE, TRUE, UINT},
ntdef::LONG, ntdef::LONG,
windef::{HMONITOR, RECT}, windef::{HMONITOR, RECT},
winerror::*, winerror::*,
// dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE},
}, },
um::{ um::{
d3d11::*, d3dcommon::D3D_DRIVER_TYPE_UNKNOWN, unknwnbase::IUnknown, wingdi::*, d3d11::*, d3dcommon::D3D_DRIVER_TYPE_UNKNOWN, unknwnbase::IUnknown, wingdi::*,
@@ -58,6 +60,7 @@ pub struct Capturer {
output_texture: bool, output_texture: bool,
adapter_desc1: DXGI_ADAPTER_DESC1, adapter_desc1: DXGI_ADAPTER_DESC1,
rotate: Rotate, rotate: Rotate,
hdr: Option<hdr::HdrToSdr>,
} }
impl Capturer { impl Capturer {
@@ -105,7 +108,7 @@ impl Capturer {
} }
} else { } else {
res = wrap_hresult(unsafe { 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 { if hres != S_OK {
gdi_capturer = display.create_gdi(); gdi_capturer = display.create_gdi();
println!("Fallback to GDI"); println!("Fallback to GDI");
@@ -161,7 +164,8 @@ impl Capturer {
device, device,
context, context,
duplication: ComPtr(duplication), duplication: ComPtr(duplication),
fastlane: desc.DesktopImageInSystemMemory == TRUE, fastlane: desc.DesktopImageInSystemMemory == TRUE
&& desc.ModeDesc.Format != DXGI_FORMAT_R16G16B16A16_FLOAT,
surface: ComPtr(ptr::null_mut()), surface: ComPtr(ptr::null_mut()),
texture: ComPtr(ptr::null_mut()), texture: ComPtr(ptr::null_mut()),
width: display.width() as usize, width: display.width() as usize,
@@ -174,9 +178,102 @@ impl Capturer {
output_texture: false, output_texture: false,
adapter_desc1, adapter_desc1,
rotate, 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( fn create_rotations(
device: *mut ID3D11Device, device: *mut ID3D11Device,
context: *mut ID3D11DeviceContext, context: *mut ID3D11DeviceContext,
@@ -330,6 +427,9 @@ impl Capturer {
} }
unsafe fn load_frame(&mut self, timeout: UINT) -> io::Result<(*const u8, i32)> { 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(); let mut frame = ptr::null_mut();
#[allow(invalid_value)] #[allow(invalid_value)]
let mut info = mem::MaybeUninit::uninit().assume_init(); let mut info = mem::MaybeUninit::uninit().assume_init();
@@ -365,6 +465,12 @@ impl Capturer {
let mut texture_desc = mem::MaybeUninit::uninit().assume_init(); let mut texture_desc = mem::MaybeUninit::uninit().assume_init();
(*texture.0).GetDesc(&mut texture_desc); (*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.Usage = D3D11_USAGE_STAGING;
texture_desc.BindFlags = 0; texture_desc.BindFlags = 0;
texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
@@ -385,7 +491,7 @@ impl Capturer {
&mut surface as *mut *mut _ as *mut *mut _, &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) Ok(surface)
} }
@@ -492,6 +598,14 @@ impl Capturer {
let texture = ComPtr(texture); let texture = ComPtr(texture);
self.texture = 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 final_texture = self.texture.0 as *mut c_void;
let mut rotation = match self.display.rotation() { let mut rotation = match self.display.rotation() {
DXGI_MODE_ROTATION_ROTATE90 => 90, DXGI_MODE_ROTATION_ROTATE90 => 90,
@@ -575,6 +689,9 @@ impl Capturer {
} }
fn unmap(&self) { fn unmap(&self) {
if self.duplication.0.is_null() {
return;
}
unsafe { unsafe {
(*self.duplication.0).ReleaseFrame(); (*self.duplication.0).ReleaseFrame();
if self.fastlane { if self.fastlane {