chore(pf-capture): make the crate verifiable — Windows CI lint + two denies (sweep Phase 0)
Gate for the rest of design/pf-capture-sweep.md: today half this crate is compiled by no CI job
at all, so the Windows-side findings can't even be type-checked.
0.1 (X1) — windows-host.yml lints `-p pf-capture --all-targets`. The host lint above it builds
pf-capture only as a DEPENDENCY, so its `#[cfg(test)]` modules were never compiled anywhere: the
5 StallWatch tests, the DXGI HDR self-tests and the cursor-conversion tables were dead code in
CI. The workflow's own comment already diagnosed this blind spot and fixed it for pf-encode;
pf-capture never got the same treatment. No features on this crate, so no feature juggling and
no extra dep tree.
0.2 (X2) — `#![deny(unsafe_op_in_unsafe_fn)]` beside the existing
`deny(clippy::undocumented_unsafe_blocks)`. The crate declares "every unsafe block carries a
SAFETY proof; enforce it" in ten file headers, but in edition 2021 that lint has nothing to fire
on inside an `unsafe fn` — so the hardest FFI in the crate was exempt from its own program: the
ring/slot construction, the fence signal, the blend scratch, and every D3D converter ctor/convert.
119 operations across 16 functions now carry a proof. `shared_object_sa` loses its `unsafe`
marker instead (it states no precondition — only its RESULT is a raw pointer, which is the
caller's business).
0.3 (X4) — drop the crate-wide `#![allow(dead_code)]`. Verified: zero warnings on either target
without it, so it was hiding nothing the lint can see (W16's dead items are `pub`/written-once,
so they need Phase 1.7's deletion instead).
No behaviour change: only lint attributes, `unsafe { }` scoping and comments.
Linux `cargo test -p pf-capture` 6/6, clippy --all-targets clean on both targets (Windows
verified by cross-checking x86_64-pc-windows-msvc locally).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -172,12 +172,15 @@ jobs:
|
|||||||
# openh264 rebuild), and keeps everything in one C:\t\release tree. Same reason
|
# openh264 rebuild), and keeps everything in one C:\t\release tree. Same reason
|
||||||
# pf-vkhdr-layer's clippy below runs --release.
|
# pf-vkhdr-layer's clippy below runs --release.
|
||||||
#
|
#
|
||||||
# pf-encode is linted SEPARATELY with --all-targets so its Windows `#[cfg(test)]` modules
|
# pf-encode and pf-capture are linted SEPARATELY with --all-targets so their Windows
|
||||||
# are type-checked — the AMF C-ABI layout assertions (`variant_layout_matches_c` and
|
# `#[cfg(test)]` modules are type-checked — pf-encode's AMF C-ABI layout assertions
|
||||||
# friends, which are the only guard on a hand-mirrored vtable ABI), the QSV tests, and the
|
# (`variant_layout_matches_c` and friends, which are the only guard on a hand-mirrored
|
||||||
# PyroWave-Windows smoke test. The host lint above cannot cover them: `-p punktfunk-host`
|
# vtable ABI), the QSV tests, the PyroWave-Windows smoke test; pf-capture's `StallWatch`
|
||||||
# only builds pf-encode as a dependency, so its test targets are never compiled, and that
|
# tests, the DXGI HDR self-tests and the cursor-conversion tables. The host lint above
|
||||||
# blind spot is what let the Linux twin's tests rot to the wrong arity unnoticed.
|
# cannot cover them: `-p punktfunk-host` only builds those crates as dependencies, so their
|
||||||
|
# test targets are never compiled anywhere, and that blind spot is what let the Linux twin's
|
||||||
|
# tests rot to the wrong arity unnoticed. pf-capture has no cargo features, so it needs no
|
||||||
|
# feature juggling and pulls in no extra dep tree.
|
||||||
# NOTE: clippy (a check, no link step) is deliberately the vehicle here — `cargo test`
|
# NOTE: clippy (a check, no link step) is deliberately the vehicle here — `cargo test`
|
||||||
# with `nvenc` cannot LINK on MSVC: nvidia-video-codec-sdk link-imports
|
# with `nvenc` cannot LINK on MSVC: nvidia-video-codec-sdk link-imports
|
||||||
# NvEncodeAPICreateInstance / NvEncodeAPIGetMaxSupportedVersion, which resolve only against
|
# NvEncodeAPICreateInstance / NvEncodeAPIGetMaxSupportedVersion, which resolve only against
|
||||||
@@ -188,6 +191,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cargo clippy --release -p punktfunk-host --features nvenc,amf-qsv,qsv -- -D warnings; if ($LASTEXITCODE) { throw "host clippy" }
|
cargo clippy --release -p punktfunk-host --features nvenc,amf-qsv,qsv -- -D warnings; if ($LASTEXITCODE) { throw "host clippy" }
|
||||||
cargo clippy --release -p pf-encode --all-targets --features nvenc,amf-qsv,qsv -- -D warnings; if ($LASTEXITCODE) { throw "pf-encode clippy" }
|
cargo clippy --release -p pf-encode --all-targets --features nvenc,amf-qsv,qsv -- -D warnings; if ($LASTEXITCODE) { throw "pf-encode clippy" }
|
||||||
|
cargo clippy --release -p pf-capture --all-targets -- -D warnings; if ($LASTEXITCODE) { throw "pf-capture clippy" }
|
||||||
cargo clippy --release -p punktfunk-tray -- -D warnings; if ($LASTEXITCODE) { throw "tray clippy" }
|
cargo clippy --release -p punktfunk-tray -- -D warnings; if ($LASTEXITCODE) { throw "tray clippy" }
|
||||||
|
|
||||||
- name: Build + lint the HDR Vulkan layer (pf-vkhdr-layer)
|
- name: Build + lint the HDR Vulkan layer (pf-vkhdr-layer)
|
||||||
|
|||||||
@@ -7,10 +7,12 @@
|
|||||||
//! [`FrameChannelSender`] closure, so this crate reaches neither the encoder nor the host
|
//! [`FrameChannelSender`] closure, so this crate reaches neither the encoder nor the host
|
||||||
//! orchestrator).
|
//! orchestrator).
|
||||||
|
|
||||||
// Scaffold: trait defaults + synthetic sources are defined ahead of the backends that use them.
|
|
||||||
#![allow(dead_code)]
|
|
||||||
// Every unsafe block in this crate carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
// Every unsafe block in this crate carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
// …and that program only covers a whole `unsafe fn` body once the body needs its own block: in
|
||||||
|
// edition 2021 `unsafe_op_in_unsafe_fn` is allow-by-default, which exempted the crate's hardest FFI
|
||||||
|
// (the ring/slot construction, the channel broker, every D3D converter ctor) from the deny above.
|
||||||
|
#![deny(unsafe_op_in_unsafe_fn)]
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
|
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ unsafe extern "system" fn hybrid_query_hook(gpu_preference: *mut u32) -> i32 {
|
|||||||
if gpu_preference.is_null() {
|
if gpu_preference.is_null() {
|
||||||
return 0xC000_000Du32 as i32; // STATUS_INVALID_PARAMETER
|
return 0xC000_000Du32 as i32; // STATUS_INVALID_PARAMETER
|
||||||
}
|
}
|
||||||
*gpu_preference = 3; // D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED
|
// SAFETY: win32u's contract for this export — the caller (DXGI) passes a writable `*mut u32`
|
||||||
|
// out-param — and the null case has just been rejected above, so this is an in-bounds,
|
||||||
|
// 4-aligned single-word store into the caller's live local.
|
||||||
|
unsafe { *gpu_preference = 3 }; // D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED
|
||||||
0 // STATUS_SUCCESS
|
0 // STATUS_SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,36 +164,49 @@ pub fn install_gpu_pref_hook() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compile one HLSL entry point. Returns the bytecode blob's bytes.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `entry` and `target` must be valid NUL-terminated ASCII pointers (an `s!()` literal at every
|
||||||
|
/// call site); `src` is a plain Rust `&str`, read only for the duration of the call.
|
||||||
pub(crate) unsafe fn compile_shader(src: &str, entry: PCSTR, target: PCSTR) -> Result<Vec<u8>> {
|
pub(crate) unsafe fn compile_shader(src: &str, entry: PCSTR, target: PCSTR) -> Result<Vec<u8>> {
|
||||||
let mut blob: Option<ID3DBlob> = None;
|
// SAFETY: `D3DCompile` reads `src.as_ptr()` for exactly `src.len()` bytes (a live `&str` that
|
||||||
let mut errs: Option<ID3DBlob> = None;
|
// outlives the synchronous call) plus the two caller-supplied NUL-terminated `PCSTR`s (per the
|
||||||
let r = D3DCompile(
|
// contract above); `&mut blob` / `Some(&mut errs)` are live out-params. Both
|
||||||
src.as_ptr() as *const c_void,
|
// `slice::from_raw_parts` calls pair a blob's OWN `GetBufferPointer` with its OWN
|
||||||
src.len(),
|
// `GetBufferSize` while that blob is still alive, and the slice is copied
|
||||||
PCSTR::null(),
|
// (`to_string` / `to_vec`) before it goes out of scope.
|
||||||
None,
|
unsafe {
|
||||||
None,
|
let mut blob: Option<ID3DBlob> = None;
|
||||||
entry,
|
let mut errs: Option<ID3DBlob> = None;
|
||||||
target,
|
let r = D3DCompile(
|
||||||
0,
|
src.as_ptr() as *const c_void,
|
||||||
0,
|
src.len(),
|
||||||
&mut blob,
|
PCSTR::null(),
|
||||||
Some(&mut errs),
|
None,
|
||||||
);
|
None,
|
||||||
if r.is_err() {
|
entry,
|
||||||
let msg = errs
|
target,
|
||||||
.as_ref()
|
0,
|
||||||
.map(|e| {
|
0,
|
||||||
let p = e.GetBufferPointer() as *const u8;
|
&mut blob,
|
||||||
String::from_utf8_lossy(std::slice::from_raw_parts(p, e.GetBufferSize()))
|
Some(&mut errs),
|
||||||
.to_string()
|
);
|
||||||
})
|
if r.is_err() {
|
||||||
.unwrap_or_default();
|
let msg = errs
|
||||||
bail!("D3DCompile failed: {msg}");
|
.as_ref()
|
||||||
|
.map(|e| {
|
||||||
|
let p = e.GetBufferPointer() as *const u8;
|
||||||
|
String::from_utf8_lossy(std::slice::from_raw_parts(p, e.GetBufferSize()))
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
bail!("D3DCompile failed: {msg}");
|
||||||
|
}
|
||||||
|
let blob = blob.context("no shader blob")?;
|
||||||
|
let p = blob.GetBufferPointer() as *const u8;
|
||||||
|
Ok(std::slice::from_raw_parts(p, blob.GetBufferSize()).to_vec())
|
||||||
}
|
}
|
||||||
let blob = blob.context("no shader blob")?;
|
|
||||||
let p = blob.GetBufferPointer() as *const u8;
|
|
||||||
Ok(std::slice::from_raw_parts(p, blob.GetBufferSize()).to_vec())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fullscreen-triangle vertex shader for the HDR conversion pass (3 verts, no input layout).
|
/// Fullscreen-triangle vertex shader for the HDR conversion pass (3 verts, no input layout).
|
||||||
@@ -322,49 +338,55 @@ pub(crate) struct HdrP010Converter {
|
|||||||
|
|
||||||
impl HdrP010Converter {
|
impl HdrP010Converter {
|
||||||
pub(crate) unsafe fn new(device: &ID3D11Device) -> Result<Self> {
|
pub(crate) unsafe fn new(device: &ID3D11Device) -> Result<Self> {
|
||||||
// Inline the shared HLSL (D3DCompile has no include handler wired here). The two PS sources
|
// SAFETY: every call is a `?`-checked D3D11 method on the live `device` borrow, over
|
||||||
// carry a `#include_common` marker we substitute before compiling.
|
// fully-initialized stack descriptors and live `Option` out-params; `compile_shader` receives
|
||||||
let y_src = HDR_P010_Y_PS.replace("#include_common", HDR_P010_COMMON);
|
// `s!()` literals (its contract). Each created COM interface owns its own reference, and no
|
||||||
let uv_src = HDR_P010_UV_PS.replace("#include_common", HDR_P010_COMMON);
|
// raw pointer outlives the call that produced it.
|
||||||
let vsb = compile_shader(HDR_VS, s!("main"), s!("vs_5_0"))?;
|
unsafe {
|
||||||
let yb = compile_shader(&y_src, s!("main"), s!("ps_5_0"))?;
|
// Inline the shared HLSL (D3DCompile has no include handler wired here). The two PS sources
|
||||||
let uvb = compile_shader(&uv_src, s!("main"), s!("ps_5_0"))?;
|
// carry a `#include_common` marker we substitute before compiling.
|
||||||
let mut vs = None;
|
let y_src = HDR_P010_Y_PS.replace("#include_common", HDR_P010_COMMON);
|
||||||
device.CreateVertexShader(&vsb, None, Some(&mut vs))?;
|
let uv_src = HDR_P010_UV_PS.replace("#include_common", HDR_P010_COMMON);
|
||||||
let mut ps_y = None;
|
let vsb = compile_shader(HDR_VS, s!("main"), s!("vs_5_0"))?;
|
||||||
device.CreatePixelShader(&yb, None, Some(&mut ps_y))?;
|
let yb = compile_shader(&y_src, s!("main"), s!("ps_5_0"))?;
|
||||||
let mut ps_uv = None;
|
let uvb = compile_shader(&uv_src, s!("main"), s!("ps_5_0"))?;
|
||||||
device.CreatePixelShader(&uvb, None, Some(&mut ps_uv))?;
|
let mut vs = None;
|
||||||
let sd = D3D11_SAMPLER_DESC {
|
device.CreateVertexShader(&vsb, None, Some(&mut vs))?;
|
||||||
// POINT: the Y pass samples a single texel centre exactly, and the UV pass does its OWN
|
let mut ps_y = None;
|
||||||
// 2x2 box average via 4 explicit taps at texel centres (offset half a texel). Point
|
device.CreatePixelShader(&yb, None, Some(&mut ps_y))?;
|
||||||
// sampling keeps each tap exact; the averaging is in the shader, not the sampler.
|
let mut ps_uv = None;
|
||||||
Filter: D3D11_FILTER_MIN_MAG_MIP_POINT,
|
device.CreatePixelShader(&uvb, None, Some(&mut ps_uv))?;
|
||||||
AddressU: D3D11_TEXTURE_ADDRESS_CLAMP,
|
let sd = D3D11_SAMPLER_DESC {
|
||||||
AddressV: D3D11_TEXTURE_ADDRESS_CLAMP,
|
// POINT: the Y pass samples a single texel centre exactly, and the UV pass does its OWN
|
||||||
AddressW: D3D11_TEXTURE_ADDRESS_CLAMP,
|
// 2x2 box average via 4 explicit taps at texel centres (offset half a texel). Point
|
||||||
ComparisonFunc: D3D11_COMPARISON_NEVER,
|
// sampling keeps each tap exact; the averaging is in the shader, not the sampler.
|
||||||
MaxLOD: f32::MAX,
|
Filter: D3D11_FILTER_MIN_MAG_MIP_POINT,
|
||||||
..Default::default()
|
AddressU: D3D11_TEXTURE_ADDRESS_CLAMP,
|
||||||
};
|
AddressV: D3D11_TEXTURE_ADDRESS_CLAMP,
|
||||||
let mut sampler = None;
|
AddressW: D3D11_TEXTURE_ADDRESS_CLAMP,
|
||||||
device.CreateSamplerState(&sd, Some(&mut sampler))?;
|
ComparisonFunc: D3D11_COMPARISON_NEVER,
|
||||||
let cbd = D3D11_BUFFER_DESC {
|
MaxLOD: f32::MAX,
|
||||||
ByteWidth: 16, // float2 inv_src + float2 pad
|
..Default::default()
|
||||||
Usage: D3D11_USAGE_DYNAMIC,
|
};
|
||||||
BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32,
|
let mut sampler = None;
|
||||||
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
|
device.CreateSamplerState(&sd, Some(&mut sampler))?;
|
||||||
..Default::default()
|
let cbd = D3D11_BUFFER_DESC {
|
||||||
};
|
ByteWidth: 16, // float2 inv_src + float2 pad
|
||||||
let mut cbuf = None;
|
Usage: D3D11_USAGE_DYNAMIC,
|
||||||
device.CreateBuffer(&cbd, None, Some(&mut cbuf))?;
|
BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32,
|
||||||
Ok(Self {
|
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
|
||||||
vs: vs.context("p010 vs")?,
|
..Default::default()
|
||||||
ps_y: ps_y.context("p010 y ps")?,
|
};
|
||||||
ps_uv: ps_uv.context("p010 uv ps")?,
|
let mut cbuf = None;
|
||||||
sampler: sampler.context("p010 sampler")?,
|
device.CreateBuffer(&cbd, None, Some(&mut cbuf))?;
|
||||||
cbuf: cbuf.context("p010 cbuf")?,
|
Ok(Self {
|
||||||
})
|
vs: vs.context("p010 vs")?,
|
||||||
|
ps_y: ps_y.context("p010 y ps")?,
|
||||||
|
ps_uv: ps_uv.context("p010 uv ps")?,
|
||||||
|
sampler: sampler.context("p010 sampler")?,
|
||||||
|
cbuf: cbuf.context("p010 cbuf")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a per-plane RTV of the P010 texture `dst` with the given single-plane `format`
|
/// Create a per-plane RTV of the P010 texture `dst` with the given single-plane `format`
|
||||||
@@ -375,15 +397,19 @@ impl HdrP010Converter {
|
|||||||
dst: &ID3D11Texture2D,
|
dst: &ID3D11Texture2D,
|
||||||
format: DXGI_FORMAT,
|
format: DXGI_FORMAT,
|
||||||
) -> Result<ID3D11RenderTargetView> {
|
) -> Result<ID3D11RenderTargetView> {
|
||||||
let desc = D3D11_RENDER_TARGET_VIEW_DESC {
|
// SAFETY: one `?`-checked `CreateRenderTargetView` on the live `device` borrow, with a
|
||||||
Format: format,
|
// fully-initialized `D3D11_RENDER_TARGET_VIEW_DESC` local whose address is taken only for the
|
||||||
ViewDimension: D3D11_RTV_DIMENSION_TEXTURE2D,
|
// duration of the synchronous call, plus a live `Option` out-param.
|
||||||
Anonymous: D3D11_RENDER_TARGET_VIEW_DESC_0 {
|
unsafe {
|
||||||
Texture2D: D3D11_TEX2D_RTV { MipSlice: 0 },
|
let desc = D3D11_RENDER_TARGET_VIEW_DESC {
|
||||||
},
|
Format: format,
|
||||||
};
|
ViewDimension: D3D11_RTV_DIMENSION_TEXTURE2D,
|
||||||
let mut rtv: Option<ID3D11RenderTargetView> = None;
|
Anonymous: D3D11_RENDER_TARGET_VIEW_DESC_0 {
|
||||||
device
|
Texture2D: D3D11_TEX2D_RTV { MipSlice: 0 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut rtv: Option<ID3D11RenderTargetView> = None;
|
||||||
|
device
|
||||||
.CreateRenderTargetView(
|
.CreateRenderTargetView(
|
||||||
dst,
|
dst,
|
||||||
Some(&desc as *const D3D11_RENDER_TARGET_VIEW_DESC),
|
Some(&desc as *const D3D11_RENDER_TARGET_VIEW_DESC),
|
||||||
@@ -392,7 +418,8 @@ impl HdrP010Converter {
|
|||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!("CreateRenderTargetView(P010 plane, format={format:?}) — driver may not support planar RTVs")
|
format!("CreateRenderTargetView(P010 plane, format={format:?}) — driver may not support planar RTVs")
|
||||||
})?;
|
})?;
|
||||||
rtv.context("p010 plane rtv null")
|
rtv.context("p010 plane rtv null")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert `src_srv` (FP16 scRGB, WxH) into `dst` (a `DXGI_FORMAT_P010` texture with
|
/// Convert `src_srv` (FP16 scRGB, WxH) into `dst` (a `DXGI_FORMAT_P010` texture with
|
||||||
@@ -408,62 +435,70 @@ impl HdrP010Converter {
|
|||||||
w: u32,
|
w: u32,
|
||||||
h: u32,
|
h: u32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let y_rtv = Self::plane_rtv(device, dst, DXGI_FORMAT_R16_UNORM)?;
|
// SAFETY: all D3D11 work runs on the caller's live `device`/`ctx` borrows (`ctx` is the
|
||||||
let uv_rtv = Self::plane_rtv(device, dst, DXGI_FORMAT_R16G16_UNORM)?;
|
// owning capture thread's immediate context), and `plane_rtv` receives that same device. The
|
||||||
|
// `copy_nonoverlapping` writes `cb.len()` `f32`s into `mapped.pData` — the pointer the
|
||||||
|
// immediately preceding `Map` of `self.cbuf` returned, a 16-byte (4×`f32`) DYNAMIC constant
|
||||||
|
// buffer — inside the `is_ok()` arm only, before the paired `Unmap`. Every `OMSet*`/`PSSet*`/
|
||||||
|
// `RSSetViewports`/`Draw` takes borrowed slices of live locals or clones of live interfaces.
|
||||||
|
unsafe {
|
||||||
|
let y_rtv = Self::plane_rtv(device, dst, DXGI_FORMAT_R16_UNORM)?;
|
||||||
|
let uv_rtv = Self::plane_rtv(device, dst, DXGI_FORMAT_R16G16_UNORM)?;
|
||||||
|
|
||||||
// Update the chroma constant buffer (inverse source texel size).
|
// Update the chroma constant buffer (inverse source texel size).
|
||||||
let cb: [f32; 4] = [1.0 / w as f32, 1.0 / h as f32, 0.0, 0.0];
|
let cb: [f32; 4] = [1.0 / w as f32, 1.0 / h as f32, 0.0, 0.0];
|
||||||
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
||||||
if ctx
|
if ctx
|
||||||
.Map(&self.cbuf, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut mapped))
|
.Map(&self.cbuf, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut mapped))
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
std::ptr::copy_nonoverlapping(cb.as_ptr(), mapped.pData as *mut f32, cb.len());
|
std::ptr::copy_nonoverlapping(cb.as_ptr(), mapped.pData as *mut f32, cb.len());
|
||||||
ctx.Unmap(&self.cbuf, 0);
|
ctx.Unmap(&self.cbuf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared pipeline state.
|
||||||
|
ctx.OMSetBlendState(None, None, 0xffff_ffff); // opaque overwrite
|
||||||
|
ctx.VSSetShader(&self.vs, None);
|
||||||
|
ctx.PSSetShaderResources(0, Some(&[Some(src_srv.clone())]));
|
||||||
|
ctx.PSSetSamplers(0, Some(&[Some(self.sampler.clone())]));
|
||||||
|
ctx.IASetInputLayout(None);
|
||||||
|
ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
||||||
|
|
||||||
|
// --- LUMA pass: full-res, plane 0 ---
|
||||||
|
let vp_y = D3D11_VIEWPORT {
|
||||||
|
TopLeftX: 0.0,
|
||||||
|
TopLeftY: 0.0,
|
||||||
|
Width: w as f32,
|
||||||
|
Height: h as f32,
|
||||||
|
MinDepth: 0.0,
|
||||||
|
MaxDepth: 1.0,
|
||||||
|
};
|
||||||
|
ctx.RSSetViewports(Some(&[vp_y]));
|
||||||
|
ctx.OMSetRenderTargets(Some(&[Some(y_rtv.clone())]), None);
|
||||||
|
ctx.PSSetShader(&self.ps_y, None);
|
||||||
|
ctx.Draw(3, 0);
|
||||||
|
ctx.OMSetRenderTargets(Some(&[None]), None);
|
||||||
|
|
||||||
|
// --- CHROMA pass: half-res, plane 1 ---
|
||||||
|
let vp_uv = D3D11_VIEWPORT {
|
||||||
|
TopLeftX: 0.0,
|
||||||
|
TopLeftY: 0.0,
|
||||||
|
Width: (w / 2) as f32,
|
||||||
|
Height: (h / 2) as f32,
|
||||||
|
MinDepth: 0.0,
|
||||||
|
MaxDepth: 1.0,
|
||||||
|
};
|
||||||
|
ctx.RSSetViewports(Some(&[vp_uv]));
|
||||||
|
ctx.OMSetRenderTargets(Some(&[Some(uv_rtv.clone())]), None);
|
||||||
|
ctx.PSSetShader(&self.ps_uv, None);
|
||||||
|
ctx.PSSetConstantBuffers(0, Some(&[Some(self.cbuf.clone())]));
|
||||||
|
ctx.Draw(3, 0);
|
||||||
|
|
||||||
|
// Unbind for the next frame's re-RTV / NVENC read.
|
||||||
|
ctx.OMSetRenderTargets(Some(&[None]), None);
|
||||||
|
ctx.PSSetShaderResources(0, Some(&[None]));
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared pipeline state.
|
|
||||||
ctx.OMSetBlendState(None, None, 0xffff_ffff); // opaque overwrite
|
|
||||||
ctx.VSSetShader(&self.vs, None);
|
|
||||||
ctx.PSSetShaderResources(0, Some(&[Some(src_srv.clone())]));
|
|
||||||
ctx.PSSetSamplers(0, Some(&[Some(self.sampler.clone())]));
|
|
||||||
ctx.IASetInputLayout(None);
|
|
||||||
ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
|
||||||
|
|
||||||
// --- LUMA pass: full-res, plane 0 ---
|
|
||||||
let vp_y = D3D11_VIEWPORT {
|
|
||||||
TopLeftX: 0.0,
|
|
||||||
TopLeftY: 0.0,
|
|
||||||
Width: w as f32,
|
|
||||||
Height: h as f32,
|
|
||||||
MinDepth: 0.0,
|
|
||||||
MaxDepth: 1.0,
|
|
||||||
};
|
|
||||||
ctx.RSSetViewports(Some(&[vp_y]));
|
|
||||||
ctx.OMSetRenderTargets(Some(&[Some(y_rtv.clone())]), None);
|
|
||||||
ctx.PSSetShader(&self.ps_y, None);
|
|
||||||
ctx.Draw(3, 0);
|
|
||||||
ctx.OMSetRenderTargets(Some(&[None]), None);
|
|
||||||
|
|
||||||
// --- CHROMA pass: half-res, plane 1 ---
|
|
||||||
let vp_uv = D3D11_VIEWPORT {
|
|
||||||
TopLeftX: 0.0,
|
|
||||||
TopLeftY: 0.0,
|
|
||||||
Width: (w / 2) as f32,
|
|
||||||
Height: (h / 2) as f32,
|
|
||||||
MinDepth: 0.0,
|
|
||||||
MaxDepth: 1.0,
|
|
||||||
};
|
|
||||||
ctx.RSSetViewports(Some(&[vp_uv]));
|
|
||||||
ctx.OMSetRenderTargets(Some(&[Some(uv_rtv.clone())]), None);
|
|
||||||
ctx.PSSetShader(&self.ps_uv, None);
|
|
||||||
ctx.PSSetConstantBuffers(0, Some(&[Some(self.cbuf.clone())]));
|
|
||||||
ctx.Draw(3, 0);
|
|
||||||
|
|
||||||
// Unbind for the next frame's re-RTV / NVENC read.
|
|
||||||
ctx.OMSetRenderTargets(Some(&[None]), None);
|
|
||||||
ctx.PSSetShaderResources(0, Some(&[None]));
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,33 +639,37 @@ pub(crate) struct BgraToYuvPlanes {
|
|||||||
|
|
||||||
impl BgraToYuvPlanes {
|
impl BgraToYuvPlanes {
|
||||||
pub(crate) unsafe fn new(device: &ID3D11Device, hdr: bool, chroma444: bool) -> Result<Self> {
|
pub(crate) unsafe fn new(device: &ID3D11Device, hdr: bool, chroma444: bool) -> Result<Self> {
|
||||||
let (y_src, uv_src) = match (hdr, chroma444) {
|
// SAFETY: as `HdrP010Converter::new` — `?`-checked D3D11 shader creation on the live
|
||||||
(false, false) => (PYRO_Y_PS.to_string(), PYRO_UV_PS.to_string()),
|
// `device` borrow, with `s!()` literals into `compile_shader` and live out-params.
|
||||||
(false, true) => (PYRO_Y_PS.to_string(), PYRO_UV444_PS.to_string()),
|
unsafe {
|
||||||
(true, false) => (
|
let (y_src, uv_src) = match (hdr, chroma444) {
|
||||||
PYRO_HDR_Y_PS.replace("#include_common", PYRO_HDR_COMMON),
|
(false, false) => (PYRO_Y_PS.to_string(), PYRO_UV_PS.to_string()),
|
||||||
PYRO_HDR_UV_PS.replace("#include_common", PYRO_HDR_COMMON),
|
(false, true) => (PYRO_Y_PS.to_string(), PYRO_UV444_PS.to_string()),
|
||||||
),
|
(true, false) => (
|
||||||
(true, true) => (
|
PYRO_HDR_Y_PS.replace("#include_common", PYRO_HDR_COMMON),
|
||||||
PYRO_HDR_Y_PS.replace("#include_common", PYRO_HDR_COMMON),
|
PYRO_HDR_UV_PS.replace("#include_common", PYRO_HDR_COMMON),
|
||||||
PYRO_HDR_UV444_PS.replace("#include_common", PYRO_HDR_COMMON),
|
),
|
||||||
),
|
(true, true) => (
|
||||||
};
|
PYRO_HDR_Y_PS.replace("#include_common", PYRO_HDR_COMMON),
|
||||||
let vsb = compile_shader(HDR_VS, s!("main"), s!("vs_5_0"))?;
|
PYRO_HDR_UV444_PS.replace("#include_common", PYRO_HDR_COMMON),
|
||||||
let yb = compile_shader(&y_src, s!("main"), s!("ps_5_0"))?;
|
),
|
||||||
let uvb = compile_shader(&uv_src, s!("main"), s!("ps_5_0"))?;
|
};
|
||||||
let mut vs = None;
|
let vsb = compile_shader(HDR_VS, s!("main"), s!("vs_5_0"))?;
|
||||||
device.CreateVertexShader(&vsb, None, Some(&mut vs))?;
|
let yb = compile_shader(&y_src, s!("main"), s!("ps_5_0"))?;
|
||||||
let mut ps_y = None;
|
let uvb = compile_shader(&uv_src, s!("main"), s!("ps_5_0"))?;
|
||||||
device.CreatePixelShader(&yb, None, Some(&mut ps_y))?;
|
let mut vs = None;
|
||||||
let mut ps_uv = None;
|
device.CreateVertexShader(&vsb, None, Some(&mut vs))?;
|
||||||
device.CreatePixelShader(&uvb, None, Some(&mut ps_uv))?;
|
let mut ps_y = None;
|
||||||
Ok(Self {
|
device.CreatePixelShader(&yb, None, Some(&mut ps_y))?;
|
||||||
vs: vs.context("pyro vs")?,
|
let mut ps_uv = None;
|
||||||
ps_y: ps_y.context("pyro y ps")?,
|
device.CreatePixelShader(&uvb, None, Some(&mut ps_uv))?;
|
||||||
ps_uv: ps_uv.context("pyro uv ps")?,
|
Ok(Self {
|
||||||
chroma444,
|
vs: vs.context("pyro vs")?,
|
||||||
})
|
ps_y: ps_y.context("pyro y ps")?,
|
||||||
|
ps_uv: ps_uv.context("pyro uv ps")?,
|
||||||
|
chroma444,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert `src_srv` (BGRA slot for SDR / scRGB FP16 slot for HDR, WxH) → `y_rtv` (full-res Y
|
/// Convert `src_srv` (BGRA slot for SDR / scRGB FP16 slot for HDR, WxH) → `y_rtv` (full-res Y
|
||||||
@@ -646,47 +685,52 @@ impl BgraToYuvPlanes {
|
|||||||
w: u32,
|
w: u32,
|
||||||
h: u32,
|
h: u32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
ctx.OMSetBlendState(None, None, 0xffff_ffff); // opaque overwrite
|
// SAFETY: D3D11 state-setting plus two `Draw`s on the caller's live immediate-context
|
||||||
ctx.VSSetShader(&self.vs, None);
|
// borrow, over borrowed slices of fully-initialized locals (the viewports) and clones of the
|
||||||
ctx.PSSetShaderResources(0, Some(&[Some(src_srv.clone())]));
|
// caller's live SRV/RTVs. No raw pointers and no mapping on this path.
|
||||||
ctx.IASetInputLayout(None);
|
unsafe {
|
||||||
ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
ctx.OMSetBlendState(None, None, 0xffff_ffff); // opaque overwrite
|
||||||
|
ctx.VSSetShader(&self.vs, None);
|
||||||
|
ctx.PSSetShaderResources(0, Some(&[Some(src_srv.clone())]));
|
||||||
|
ctx.IASetInputLayout(None);
|
||||||
|
ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
||||||
|
|
||||||
// LUMA pass: full-res → the R8 Y texture.
|
// LUMA pass: full-res → the R8 Y texture.
|
||||||
ctx.RSSetViewports(Some(&[D3D11_VIEWPORT {
|
ctx.RSSetViewports(Some(&[D3D11_VIEWPORT {
|
||||||
TopLeftX: 0.0,
|
TopLeftX: 0.0,
|
||||||
TopLeftY: 0.0,
|
TopLeftY: 0.0,
|
||||||
Width: w as f32,
|
Width: w as f32,
|
||||||
Height: h as f32,
|
Height: h as f32,
|
||||||
MinDepth: 0.0,
|
MinDepth: 0.0,
|
||||||
MaxDepth: 1.0,
|
MaxDepth: 1.0,
|
||||||
}]));
|
}]));
|
||||||
ctx.OMSetRenderTargets(Some(&[Some(y_rtv.clone())]), None);
|
ctx.OMSetRenderTargets(Some(&[Some(y_rtv.clone())]), None);
|
||||||
ctx.PSSetShader(&self.ps_y, None);
|
ctx.PSSetShader(&self.ps_y, None);
|
||||||
ctx.Draw(3, 0);
|
ctx.Draw(3, 0);
|
||||||
ctx.OMSetRenderTargets(Some(&[None]), None);
|
ctx.OMSetRenderTargets(Some(&[None]), None);
|
||||||
|
|
||||||
// CHROMA pass: half-res (4:2:0) or full-res (4:4:4) → the CbCr texture.
|
// CHROMA pass: half-res (4:2:0) or full-res (4:4:4) → the CbCr texture.
|
||||||
let (cw, ch) = if self.chroma444 {
|
let (cw, ch) = if self.chroma444 {
|
||||||
(w, h)
|
(w, h)
|
||||||
} else {
|
} else {
|
||||||
(w / 2, h / 2)
|
(w / 2, h / 2)
|
||||||
};
|
};
|
||||||
ctx.RSSetViewports(Some(&[D3D11_VIEWPORT {
|
ctx.RSSetViewports(Some(&[D3D11_VIEWPORT {
|
||||||
TopLeftX: 0.0,
|
TopLeftX: 0.0,
|
||||||
TopLeftY: 0.0,
|
TopLeftY: 0.0,
|
||||||
Width: cw as f32,
|
Width: cw as f32,
|
||||||
Height: ch as f32,
|
Height: ch as f32,
|
||||||
MinDepth: 0.0,
|
MinDepth: 0.0,
|
||||||
MaxDepth: 1.0,
|
MaxDepth: 1.0,
|
||||||
}]));
|
}]));
|
||||||
ctx.OMSetRenderTargets(Some(&[Some(cbcr_rtv.clone())]), None);
|
ctx.OMSetRenderTargets(Some(&[Some(cbcr_rtv.clone())]), None);
|
||||||
ctx.PSSetShader(&self.ps_uv, None);
|
ctx.PSSetShader(&self.ps_uv, None);
|
||||||
ctx.Draw(3, 0);
|
ctx.Draw(3, 0);
|
||||||
|
|
||||||
ctx.OMSetRenderTargets(Some(&[None]), None);
|
ctx.OMSetRenderTargets(Some(&[None]), None);
|
||||||
ctx.PSSetShaderResources(0, Some(&[None]));
|
ctx.PSSetShaderResources(0, Some(&[None]));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1264,49 +1308,57 @@ impl VideoConverter {
|
|||||||
height: u32,
|
height: u32,
|
||||||
scrgb_input: bool,
|
scrgb_input: bool,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let vdev: ID3D11VideoDevice = device.cast().context("device -> ID3D11VideoDevice")?;
|
// SAFETY: the `cast()`s and the `?`-checked video-device factory calls run on the caller's
|
||||||
let vctx: ID3D11VideoContext1 = context.cast().context("context -> ID3D11VideoContext1")?;
|
// live `device`/`context` borrows; `&desc` is a fully-initialized stack
|
||||||
let rate = DXGI_RATIONAL {
|
// `D3D11_VIDEO_PROCESSOR_CONTENT_DESC` read only for the duration of the call, and the
|
||||||
Numerator: 240,
|
// colour-space/frame-format setters take the just-created processor by borrow plus plain
|
||||||
Denominator: 1,
|
// enum values.
|
||||||
};
|
unsafe {
|
||||||
let desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC {
|
let vdev: ID3D11VideoDevice = device.cast().context("device -> ID3D11VideoDevice")?;
|
||||||
InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
|
let vctx: ID3D11VideoContext1 =
|
||||||
InputFrameRate: rate,
|
context.cast().context("context -> ID3D11VideoContext1")?;
|
||||||
InputWidth: width,
|
let rate = DXGI_RATIONAL {
|
||||||
InputHeight: height,
|
Numerator: 240,
|
||||||
OutputFrameRate: rate,
|
Denominator: 1,
|
||||||
OutputWidth: width,
|
};
|
||||||
OutputHeight: height,
|
let desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC {
|
||||||
Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
|
InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
|
||||||
};
|
InputFrameRate: rate,
|
||||||
let enumr = vdev
|
InputWidth: width,
|
||||||
.CreateVideoProcessorEnumerator(&desc)
|
InputHeight: height,
|
||||||
.context("CreateVideoProcessorEnumerator")?;
|
OutputFrameRate: rate,
|
||||||
let vp = vdev
|
OutputWidth: width,
|
||||||
.CreateVideoProcessor(&enumr, 0)
|
OutputHeight: height,
|
||||||
.context("CreateVideoProcessor")?;
|
Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
|
||||||
|
};
|
||||||
|
let enumr = vdev
|
||||||
|
.CreateVideoProcessorEnumerator(&desc)
|
||||||
|
.context("CreateVideoProcessorEnumerator")?;
|
||||||
|
let vp = vdev
|
||||||
|
.CreateVideoProcessor(&enumr, 0)
|
||||||
|
.context("CreateVideoProcessor")?;
|
||||||
|
|
||||||
// Full-range RGB in → studio-range BT.709 NV12 out. Input gamma follows the ring format:
|
// Full-range RGB in → studio-range BT.709 NV12 out. Input gamma follows the ring format:
|
||||||
// scRGB linear (G10) for the FP16 HDR ring, sRGB (G22) for the 8-bit BGRA SDR ring. The
|
// scRGB linear (G10) for the FP16 HDR ring, sRGB (G22) for the 8-bit BGRA SDR ring. The
|
||||||
// output is always BT.709 SDR (the video processor tone-maps the scRGB case).
|
// output is always BT.709 SDR (the video processor tone-maps the scRGB case).
|
||||||
let in_cs = if scrgb_input {
|
let in_cs = if scrgb_input {
|
||||||
DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709
|
DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709
|
||||||
} else {
|
} else {
|
||||||
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709
|
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709
|
||||||
};
|
};
|
||||||
let out_cs = DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709;
|
let out_cs = DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709;
|
||||||
vctx.VideoProcessorSetStreamColorSpace1(&vp, 0, in_cs);
|
vctx.VideoProcessorSetStreamColorSpace1(&vp, 0, in_cs);
|
||||||
vctx.VideoProcessorSetOutputColorSpace1(&vp, out_cs);
|
vctx.VideoProcessorSetOutputColorSpace1(&vp, out_cs);
|
||||||
// One frame in, one frame out — no interpolation/auto-processing.
|
// One frame in, one frame out — no interpolation/auto-processing.
|
||||||
vctx.VideoProcessorSetStreamFrameFormat(&vp, 0, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE);
|
vctx.VideoProcessorSetStreamFrameFormat(&vp, 0, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
vdev,
|
vdev,
|
||||||
vctx,
|
vctx,
|
||||||
enumr,
|
enumr,
|
||||||
vp,
|
vp,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert `input` (BGRA or scRGB FP16) → `output` (NV12 or P010) on the video engine. Views are
|
/// Convert `input` (BGRA or scRGB FP16) → `output` (NV12 or P010) on the video engine. Views are
|
||||||
@@ -1316,47 +1368,54 @@ impl VideoConverter {
|
|||||||
input: &ID3D11Texture2D,
|
input: &ID3D11Texture2D,
|
||||||
output: &ID3D11Texture2D,
|
output: &ID3D11Texture2D,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let in_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC {
|
// SAFETY: both view creations are `?`-checked calls on `self.vdev` with fully-initialized
|
||||||
FourCC: 0,
|
// stack descriptors and live out-params. `stream.pInputSurface` is a `ManuallyDrop` of the
|
||||||
ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D,
|
// input view just created: `VideoProcessorBlt` only BORROWS it (a COM in-param never transfers
|
||||||
Anonymous: D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0 {
|
// ownership), and the explicit `into_inner` drop below releases that reference exactly once on
|
||||||
Texture2D: D3D11_TEX2D_VPIV {
|
// both the success and the failure path. `slice::from_ref(&stream)` borrows the live local.
|
||||||
MipSlice: 0,
|
unsafe {
|
||||||
ArraySlice: 0,
|
let in_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC {
|
||||||
|
FourCC: 0,
|
||||||
|
ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D,
|
||||||
|
Anonymous: D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC_0 {
|
||||||
|
Texture2D: D3D11_TEX2D_VPIV {
|
||||||
|
MipSlice: 0,
|
||||||
|
ArraySlice: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
let mut in_view: Option<ID3D11VideoProcessorInputView> = None;
|
||||||
let mut in_view: Option<ID3D11VideoProcessorInputView> = None;
|
self.vdev
|
||||||
self.vdev
|
.CreateVideoProcessorInputView(input, &self.enumr, &in_desc, Some(&mut in_view))
|
||||||
.CreateVideoProcessorInputView(input, &self.enumr, &in_desc, Some(&mut in_view))
|
.context("CreateVideoProcessorInputView")?;
|
||||||
.context("CreateVideoProcessorInputView")?;
|
|
||||||
|
|
||||||
let out_desc = D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC {
|
let out_desc = D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC {
|
||||||
ViewDimension: D3D11_VPOV_DIMENSION_TEXTURE2D,
|
ViewDimension: D3D11_VPOV_DIMENSION_TEXTURE2D,
|
||||||
Anonymous: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0 {
|
Anonymous: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC_0 {
|
||||||
Texture2D: D3D11_TEX2D_VPOV { MipSlice: 0 },
|
Texture2D: D3D11_TEX2D_VPOV { MipSlice: 0 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let mut out_view: Option<ID3D11VideoProcessorOutputView> = None;
|
let mut out_view: Option<ID3D11VideoProcessorOutputView> = None;
|
||||||
self.vdev
|
self.vdev
|
||||||
.CreateVideoProcessorOutputView(output, &self.enumr, &out_desc, Some(&mut out_view))
|
.CreateVideoProcessorOutputView(output, &self.enumr, &out_desc, Some(&mut out_view))
|
||||||
.context("CreateVideoProcessorOutputView")?;
|
.context("CreateVideoProcessorOutputView")?;
|
||||||
let out_view = out_view.context("null output view")?;
|
let out_view = out_view.context("null output view")?;
|
||||||
|
|
||||||
let stream = D3D11_VIDEO_PROCESSOR_STREAM {
|
let stream = D3D11_VIDEO_PROCESSOR_STREAM {
|
||||||
Enable: true.into(),
|
Enable: true.into(),
|
||||||
pInputSurface: std::mem::ManuallyDrop::new(in_view),
|
pInputSurface: std::mem::ManuallyDrop::new(in_view),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let blt =
|
let blt =
|
||||||
self.vctx
|
self.vctx
|
||||||
.VideoProcessorBlt(&self.vp, &out_view, 0, std::slice::from_ref(&stream));
|
.VideoProcessorBlt(&self.vp, &out_view, 0, std::slice::from_ref(&stream));
|
||||||
// COM in-params never transfer ownership: the Blt only borrowed the input view, and the
|
// COM in-params never transfer ownership: the Blt only borrowed the input view, and the
|
||||||
// struct's `ManuallyDrop` field suppressed its release — drop it by hand, success or not.
|
// struct's `ManuallyDrop` field suppressed its release — drop it by hand, success or not.
|
||||||
// (Skipping this leaked one view + its UMD allocation PER CONVERTED FRAME — the SDR hot
|
// (Skipping this leaked one view + its UMD allocation PER CONVERTED FRAME — the SDR hot
|
||||||
// path; D3D11 defers the actual destruction until the GPU is done with the blit.)
|
// path; D3D11 defers the actual destruction until the GPU is done with the blit.)
|
||||||
drop(std::mem::ManuallyDrop::into_inner(stream.pInputSurface));
|
drop(std::mem::ManuallyDrop::into_inner(stream.pInputSurface));
|
||||||
blt.context("VideoProcessorBlt")
|
blt.context("VideoProcessorBlt")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -582,21 +582,28 @@ unsafe impl Send for IddPushCapturer {}
|
|||||||
/// 2026-07-03), so it cannot dup the handles out either. History: `Global\`-named + world-openable
|
/// 2026-07-03), so it cannot dup the handles out either. History: `Global\`-named + world-openable
|
||||||
/// (`WD`, security-review 2026-06-28 #5) → SY+LS-scoped → nameless → now SY-only. `psd` must outlive
|
/// (`WD`, security-review 2026-06-28 #5) → SY+LS-scoped → nameless → now SY-only. `psd` must outlive
|
||||||
/// `sa`. See `design/idd-push-security.md`.
|
/// `sa`. See `design/idd-push-security.md`.
|
||||||
unsafe fn shared_object_sa() -> Result<(SECURITY_ATTRIBUTES, PSECURITY_DESCRIPTOR)> {
|
fn shared_object_sa() -> Result<(SECURITY_ATTRIBUTES, PSECURITY_DESCRIPTOR)> {
|
||||||
let mut psd = PSECURITY_DESCRIPTOR::default();
|
// SAFETY: `ConvertStringSecurityDescriptorToSecurityDescriptorW` reads the `w!()` literal
|
||||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
// and writes the descriptor it allocates into the live local `psd`; `?` rejects a failure
|
||||||
w!("D:P(A;;GA;;;SY)"),
|
// before `psd` is read. The `SECURITY_ATTRIBUTES` returned alongside merely CARRIES `psd.0` as
|
||||||
SDDL_REVISION_1,
|
// a raw pointer — keeping the two paired (and freeing the descriptor) is the caller's unsafe
|
||||||
&mut psd,
|
// business, so this fn itself has no precondition to state and needs no `unsafe` marker.
|
||||||
None,
|
unsafe {
|
||||||
)
|
let mut psd = PSECURITY_DESCRIPTOR::default();
|
||||||
.context("build SDDL for IDD-push shared objects")?;
|
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||||
let sa = SECURITY_ATTRIBUTES {
|
w!("D:P(A;;GA;;;SY)"),
|
||||||
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
SDDL_REVISION_1,
|
||||||
lpSecurityDescriptor: psd.0,
|
&mut psd,
|
||||||
bInheritHandle: false.into(),
|
None,
|
||||||
};
|
)
|
||||||
Ok((sa, psd))
|
.context("build SDDL for IDD-push shared objects")?;
|
||||||
|
let sa = SECURITY_ATTRIBUTES {
|
||||||
|
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||||
|
lpSecurityDescriptor: psd.0,
|
||||||
|
bInheritHandle: false.into(),
|
||||||
|
};
|
||||||
|
Ok((sa, psd))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IddPushCapturer {
|
impl IddPushCapturer {
|
||||||
@@ -610,56 +617,64 @@ impl IddPushCapturer {
|
|||||||
h: u32,
|
h: u32,
|
||||||
format: DXGI_FORMAT,
|
format: DXGI_FORMAT,
|
||||||
) -> Result<Vec<HostSlot>> {
|
) -> Result<Vec<HostSlot>> {
|
||||||
let (sa, _psd) = shared_object_sa()?;
|
// SAFETY: every D3D11/DXGI call is `?`-checked on the live `device` borrow, over
|
||||||
let mut slots = Vec::new();
|
// fully-initialized stack descriptors and live out-params; `&sa` stays valid for the whole loop
|
||||||
for _ in 0..RING_LEN {
|
// because `_psd`, the security descriptor backing it, is held in scope alongside.
|
||||||
let desc = D3D11_TEXTURE2D_DESC {
|
// `OwnedHandle::from_raw_handle` adopts the handle `CreateSharedHandle` JUST minted for this
|
||||||
Width: w,
|
// slot — a unique, still-open NT handle owned by this process — making the slot its sole owner.
|
||||||
Height: h,
|
unsafe {
|
||||||
MipLevels: 1,
|
let (sa, _psd) = shared_object_sa()?;
|
||||||
ArraySize: 1,
|
let mut slots = Vec::new();
|
||||||
// Match the OS-composed swap-chain surfaces so the driver's CopyResource into the slot +
|
for _ in 0..RING_LEN {
|
||||||
// its format-guard both succeed.
|
let desc = D3D11_TEXTURE2D_DESC {
|
||||||
Format: format,
|
Width: w,
|
||||||
SampleDesc: DXGI_SAMPLE_DESC {
|
Height: h,
|
||||||
Count: 1,
|
MipLevels: 1,
|
||||||
Quality: 0,
|
ArraySize: 1,
|
||||||
},
|
// Match the OS-composed swap-chain surfaces so the driver's CopyResource into the slot +
|
||||||
Usage: D3D11_USAGE_DEFAULT,
|
// its format-guard both succeed.
|
||||||
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
|
Format: format,
|
||||||
CPUAccessFlags: 0,
|
SampleDesc: DXGI_SAMPLE_DESC {
|
||||||
MiscFlags: (D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0
|
Count: 1,
|
||||||
| D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX.0) as u32,
|
Quality: 0,
|
||||||
};
|
},
|
||||||
let mut tex: Option<ID3D11Texture2D> = None;
|
Usage: D3D11_USAGE_DEFAULT,
|
||||||
device
|
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
|
||||||
.CreateTexture2D(&desc, None, Some(&mut tex))
|
CPUAccessFlags: 0,
|
||||||
.context("CreateTexture2D(IDD-push ring slot)")?;
|
MiscFlags: (D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0
|
||||||
let tex = tex.context("null ring texture")?;
|
| D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX.0)
|
||||||
let res1: IDXGIResource1 = tex.cast()?;
|
as u32,
|
||||||
let shared = res1
|
};
|
||||||
.CreateSharedHandle(
|
let mut tex: Option<ID3D11Texture2D> = None;
|
||||||
Some(&sa as *const SECURITY_ATTRIBUTES),
|
device
|
||||||
DXGI_SHARED_RESOURCE_RW,
|
.CreateTexture2D(&desc, None, Some(&mut tex))
|
||||||
PCWSTR::null(), // UNNAMED — reachable only through the broker's duplicate
|
.context("CreateTexture2D(IDD-push ring slot)")?;
|
||||||
)
|
let tex = tex.context("null ring texture")?;
|
||||||
.context("CreateSharedHandle(IDD-push ring slot)")?;
|
let res1: IDXGIResource1 = tex.cast()?;
|
||||||
// Own the shared handle so the slot's `Drop` closes it via RAII (was a manual `CloseHandle`).
|
let shared = res1
|
||||||
let shared = OwnedHandle::from_raw_handle(shared.0 as _);
|
.CreateSharedHandle(
|
||||||
let mutex: IDXGIKeyedMutex = tex.cast()?;
|
Some(&sa as *const SECURITY_ATTRIBUTES),
|
||||||
let mut srv: Option<ID3D11ShaderResourceView> = None;
|
DXGI_SHARED_RESOURCE_RW,
|
||||||
device
|
PCWSTR::null(), // UNNAMED — reachable only through the broker's duplicate
|
||||||
.CreateShaderResourceView(&tex, None, Some(&mut srv))
|
)
|
||||||
.context("CreateShaderResourceView(IDD-push ring slot)")?;
|
.context("CreateSharedHandle(IDD-push ring slot)")?;
|
||||||
let srv = srv.context("null slot srv")?;
|
// Own the shared handle so the slot's `Drop` closes it via RAII (was a manual `CloseHandle`).
|
||||||
slots.push(HostSlot {
|
let shared = OwnedHandle::from_raw_handle(shared.0 as _);
|
||||||
tex,
|
let mutex: IDXGIKeyedMutex = tex.cast()?;
|
||||||
mutex,
|
let mut srv: Option<ID3D11ShaderResourceView> = None;
|
||||||
shared,
|
device
|
||||||
srv,
|
.CreateShaderResourceView(&tex, None, Some(&mut srv))
|
||||||
});
|
.context("CreateShaderResourceView(IDD-push ring slot)")?;
|
||||||
|
let srv = srv.context("null slot srv")?;
|
||||||
|
slots.push(HostSlot {
|
||||||
|
tex,
|
||||||
|
mutex,
|
||||||
|
shared,
|
||||||
|
srv,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(slots)
|
||||||
}
|
}
|
||||||
Ok(slots)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open the IDD-push capturer. On success the caller's `keepalive` is attached (the capturer owns the
|
/// Open the IDD-push capturer. On success the caller's `keepalive` is attached (the capturer owns the
|
||||||
@@ -1739,46 +1754,52 @@ impl IddPushCapturer {
|
|||||||
/// Runs on the owning capture/encode thread that holds the immediate context; forms no lasting
|
/// Runs on the owning capture/encode thread that holds the immediate context; forms no lasting
|
||||||
/// borrow of `self`'s COM objects.
|
/// borrow of `self`'s COM objects.
|
||||||
unsafe fn pyro_fence_signal(&mut self) -> Result<Option<(Option<isize>, u64)>> {
|
unsafe fn pyro_fence_signal(&mut self) -> Result<Option<(Option<isize>, u64)>> {
|
||||||
if !self.pyrowave {
|
// SAFETY: per the contract above this runs on the owning capture/encode thread, which holds
|
||||||
return Ok(None);
|
// the immediate context. Every call is a `?`-checked COM method on `self`'s live device or
|
||||||
}
|
// context (or on a `cast()` of one), and `CreateSharedHandle` yields a fresh NT handle whose
|
||||||
if self.pyro_fence.is_none() {
|
// raw value is only STORED here — never dereferenced, and never closed on this path.
|
||||||
let dev5: ID3D11Device5 = self
|
unsafe {
|
||||||
.device
|
if !self.pyrowave {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
if self.pyro_fence.is_none() {
|
||||||
|
let dev5: ID3D11Device5 = self
|
||||||
|
.device
|
||||||
|
.cast()
|
||||||
|
.context("ID3D11Device -> ID3D11Device5 (shared fence)")?;
|
||||||
|
// windows-rs returns COM interfaces via an out-param (unlike the HANDLE-returning
|
||||||
|
// CreateSharedHandle below).
|
||||||
|
let mut fence_out: Option<ID3D11Fence> = None;
|
||||||
|
dev5.CreateFence(0, D3D11_FENCE_FLAG_SHARED, &mut fence_out)
|
||||||
|
.context("CreateFence(D3D11_FENCE_FLAG_SHARED)")?;
|
||||||
|
let fence = fence_out.context("null D3D11 fence")?;
|
||||||
|
// GENERIC_ALL (0x1000_0000) — the access the pyrowave interop test hands the handle.
|
||||||
|
let handle: HANDLE = fence
|
||||||
|
.CreateSharedHandle(None, 0x1000_0000, PCWSTR::null())
|
||||||
|
.context("ID3D11Fence::CreateSharedHandle")?;
|
||||||
|
self.pyro_fence = Some(fence);
|
||||||
|
self.pyro_fence_handle = Some(handle.0 as isize);
|
||||||
|
self.pyro_fence_value = 0;
|
||||||
|
}
|
||||||
|
self.pyro_fence_value += 1;
|
||||||
|
let value = self.pyro_fence_value;
|
||||||
|
let ctx4: ID3D11DeviceContext4 = self
|
||||||
|
.context
|
||||||
.cast()
|
.cast()
|
||||||
.context("ID3D11Device -> ID3D11Device5 (shared fence)")?;
|
.context("ID3D11DeviceContext -> ID3D11DeviceContext4 (fence signal)")?;
|
||||||
// windows-rs returns COM interfaces via an out-param (unlike the HANDLE-returning
|
{
|
||||||
// CreateSharedHandle below).
|
let fence = self.pyro_fence.as_ref().expect("fence just created");
|
||||||
let mut fence_out: Option<ID3D11Fence> = None;
|
ctx4.Signal(fence, value)
|
||||||
dev5.CreateFence(0, D3D11_FENCE_FLAG_SHARED, &mut fence_out)
|
.context("ID3D11 fence Signal after convert")?;
|
||||||
.context("CreateFence(D3D11_FENCE_FLAG_SHARED)")?;
|
}
|
||||||
let fence = fence_out.context("null D3D11 fence")?;
|
// Submit the queued convert + signal so the encoder's Vulkan timeline wait can resolve.
|
||||||
// GENERIC_ALL (0x1000_0000) — the access the pyrowave interop test hands the handle.
|
self.context.Flush();
|
||||||
let handle: HANDLE = fence
|
// Pass the persistent shared handle EVERY frame (not once): the encoder can be rebuilt on a
|
||||||
.CreateSharedHandle(None, 0x1000_0000, PCWSTR::null())
|
// client mode-switch, and a rebuilt encoder needs to re-import the fence into its fresh Vulkan
|
||||||
.context("ID3D11Fence::CreateSharedHandle")?;
|
// device. The encoder imports only when it has no timeline yet (and DUPLICATES the handle so
|
||||||
self.pyro_fence = Some(fence);
|
// this original stays valid for the next rebuild).
|
||||||
self.pyro_fence_handle = Some(handle.0 as isize);
|
Ok(Some((self.pyro_fence_handle, value)))
|
||||||
self.pyro_fence_value = 0;
|
|
||||||
}
|
}
|
||||||
self.pyro_fence_value += 1;
|
|
||||||
let value = self.pyro_fence_value;
|
|
||||||
let ctx4: ID3D11DeviceContext4 = self
|
|
||||||
.context
|
|
||||||
.cast()
|
|
||||||
.context("ID3D11DeviceContext -> ID3D11DeviceContext4 (fence signal)")?;
|
|
||||||
{
|
|
||||||
let fence = self.pyro_fence.as_ref().expect("fence just created");
|
|
||||||
ctx4.Signal(fence, value)
|
|
||||||
.context("ID3D11 fence Signal after convert")?;
|
|
||||||
}
|
|
||||||
// Submit the queued convert + signal so the encoder's Vulkan timeline wait can resolve.
|
|
||||||
self.context.Flush();
|
|
||||||
// Pass the persistent shared handle EVERY frame (not once): the encoder can be rebuilt on a
|
|
||||||
// client mode-switch, and a rebuilt encoder needs to re-import the fence into its fresh Vulkan
|
|
||||||
// device. The encoder imports only when it has no timeline yet (and DUPLICATES the handle so
|
|
||||||
// this original stays valid for the next rebuild).
|
|
||||||
Ok(Some((self.pyro_fence_handle, value)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The (serial, x, y, visible) of the CURRENT polled cursor — the composite-regen change
|
/// The (serial, x, y, visible) of the CURRENT polled cursor — the composite-regen change
|
||||||
@@ -1803,112 +1824,120 @@ impl IddPushCapturer {
|
|||||||
&mut self,
|
&mut self,
|
||||||
slot_tex: &ID3D11Texture2D,
|
slot_tex: &ID3D11Texture2D,
|
||||||
) -> Option<(ID3D11Texture2D, ID3D11ShaderResourceView)> {
|
) -> Option<(ID3D11Texture2D, ID3D11ShaderResourceView)> {
|
||||||
let fmt = self.ring_format();
|
// SAFETY: per the contract above, D3D11 calls on the owning thread's device + immediate
|
||||||
// (Re)build the scratch at the current ring geometry.
|
// context while the slot's keyed mutex is held. `CreateTexture2D`/`CreateShaderResourceView`
|
||||||
let stale = self
|
// take a fully-initialized stack descriptor plus live out-params and are `.ok()`-checked before
|
||||||
.blend_scratch
|
// use; `CopyResource` moves between our own scratch and the caller's live slot texture, which
|
||||||
.as_ref()
|
// share format and size by construction (the scratch is rebuilt whenever the ring geometry
|
||||||
.is_none_or(|(_, _, w, h, f)| (*w, *h, *f) != (self.width, self.height, fmt));
|
// changes). `sdr_white_level_scale` is a read-only CCD query over owned locals.
|
||||||
if stale {
|
unsafe {
|
||||||
self.blend_scratch = None;
|
let fmt = self.ring_format();
|
||||||
let desc = D3D11_TEXTURE2D_DESC {
|
// (Re)build the scratch at the current ring geometry.
|
||||||
Width: self.width,
|
let stale = self
|
||||||
Height: self.height,
|
.blend_scratch
|
||||||
MipLevels: 1,
|
.as_ref()
|
||||||
ArraySize: 1,
|
.is_none_or(|(_, _, w, h, f)| (*w, *h, *f) != (self.width, self.height, fmt));
|
||||||
Format: fmt,
|
if stale {
|
||||||
SampleDesc: DXGI_SAMPLE_DESC {
|
self.blend_scratch = None;
|
||||||
Count: 1,
|
let desc = D3D11_TEXTURE2D_DESC {
|
||||||
Quality: 0,
|
Width: self.width,
|
||||||
},
|
Height: self.height,
|
||||||
Usage: D3D11_USAGE_DEFAULT,
|
MipLevels: 1,
|
||||||
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
|
ArraySize: 1,
|
||||||
..Default::default()
|
Format: fmt,
|
||||||
};
|
SampleDesc: DXGI_SAMPLE_DESC {
|
||||||
let mut tex: Option<ID3D11Texture2D> = None;
|
Count: 1,
|
||||||
let built = self
|
Quality: 0,
|
||||||
.device
|
},
|
||||||
.CreateTexture2D(&desc, None, Some(&mut tex))
|
Usage: D3D11_USAGE_DEFAULT,
|
||||||
.ok()
|
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
|
||||||
.and(tex)
|
..Default::default()
|
||||||
.and_then(|t| {
|
|
||||||
let mut srv: Option<ID3D11ShaderResourceView> = None;
|
|
||||||
self.device
|
|
||||||
.CreateShaderResourceView(&t, None, Some(&mut srv))
|
|
||||||
.ok()
|
|
||||||
.and(srv)
|
|
||||||
.map(|v| (t, v))
|
|
||||||
});
|
|
||||||
match built {
|
|
||||||
Some((t, v)) => {
|
|
||||||
self.blend_scratch = Some((t, v, self.width, self.height, fmt));
|
|
||||||
if self.display_hdr {
|
|
||||||
// Where DWM places SDR white on this HDR desktop — the composited
|
|
||||||
// cursor must match or it reads dark (~2.5x at the Windows default).
|
|
||||||
// Queried only here: scratch rebuilds are rare, and the CCD query
|
|
||||||
// contends on the display-config lock, which must stay OFF the
|
|
||||||
// per-frame path.
|
|
||||||
// Safety: read-only CCD query over owned locals (within unsafe fn).
|
|
||||||
let queried =
|
|
||||||
pf_win_display::win_display::sdr_white_level_scale(self.target_id);
|
|
||||||
self.sdr_white_scale = queried.unwrap_or(self.sdr_white_scale);
|
|
||||||
tracing::info!(
|
|
||||||
target_id = self.target_id,
|
|
||||||
queried = ?queried,
|
|
||||||
applied = self.sdr_white_scale,
|
|
||||||
"cursor composite: HDR SDR-white scale (1.0 = 80 nits; None = \
|
|
||||||
query failed — keeping the prior value)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
if !self.cursor_blend_failed {
|
|
||||||
self.cursor_blend_failed = true;
|
|
||||||
tracing::warn!(
|
|
||||||
"cursor blend scratch creation failed — capture-model frames stay \
|
|
||||||
pointer-less this session"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let (tex, srv, ..) = self.blend_scratch.as_ref().expect("just ensured");
|
|
||||||
let (tex, srv) = (tex.clone(), srv.clone());
|
|
||||||
self.context.CopyResource(&tex, slot_tex);
|
|
||||||
// Blend the pointer (visible shapes only; hidden = the copy alone is the frame).
|
|
||||||
let overlay = self.cursor_poll.as_ref().and_then(|p| p.read());
|
|
||||||
self.last_blend_key = overlay.as_ref().map(|o| (o.serial, o.x, o.y, o.visible));
|
|
||||||
if let Some(ov) = overlay.filter(|o| o.visible) {
|
|
||||||
if self.cursor_blend.is_none() && !self.cursor_blend_failed {
|
|
||||||
match cursor_blend::CursorBlendPass::new(&self.device) {
|
|
||||||
Ok(p) => self.cursor_blend = Some(p),
|
|
||||||
Err(e) => {
|
|
||||||
self.cursor_blend_failed = true;
|
|
||||||
tracing::warn!(
|
|
||||||
"cursor blend pass build failed — capture-model frames stay \
|
|
||||||
pointer-less this session: {e:#}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(pass) = self.cursor_blend.as_mut() {
|
|
||||||
// FP16 ring = scRGB linear composition (HDR): linearize the sRGB shape and
|
|
||||||
// scale it to the target's SDR white so it matches the desktop around it.
|
|
||||||
let scale = if self.display_hdr {
|
|
||||||
self.sdr_white_scale
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
};
|
||||||
if let Err(e) = pass.blend(&self.device, &self.context, &tex, &ov, scale) {
|
let mut tex: Option<ID3D11Texture2D> = None;
|
||||||
if !self.cursor_blend_failed {
|
let built = self
|
||||||
self.cursor_blend_failed = true;
|
.device
|
||||||
tracing::warn!("cursor blend draw failed — pointer-less frames: {e:#}");
|
.CreateTexture2D(&desc, None, Some(&mut tex))
|
||||||
|
.ok()
|
||||||
|
.and(tex)
|
||||||
|
.and_then(|t| {
|
||||||
|
let mut srv: Option<ID3D11ShaderResourceView> = None;
|
||||||
|
self.device
|
||||||
|
.CreateShaderResourceView(&t, None, Some(&mut srv))
|
||||||
|
.ok()
|
||||||
|
.and(srv)
|
||||||
|
.map(|v| (t, v))
|
||||||
|
});
|
||||||
|
match built {
|
||||||
|
Some((t, v)) => {
|
||||||
|
self.blend_scratch = Some((t, v, self.width, self.height, fmt));
|
||||||
|
if self.display_hdr {
|
||||||
|
// Where DWM places SDR white on this HDR desktop — the composited
|
||||||
|
// cursor must match or it reads dark (~2.5x at the Windows default).
|
||||||
|
// Queried only here: scratch rebuilds are rare, and the CCD query
|
||||||
|
// contends on the display-config lock, which must stay OFF the
|
||||||
|
// per-frame path.
|
||||||
|
// Safety: read-only CCD query over owned locals (within unsafe fn).
|
||||||
|
let queried =
|
||||||
|
pf_win_display::win_display::sdr_white_level_scale(self.target_id);
|
||||||
|
self.sdr_white_scale = queried.unwrap_or(self.sdr_white_scale);
|
||||||
|
tracing::info!(
|
||||||
|
target_id = self.target_id,
|
||||||
|
queried = ?queried,
|
||||||
|
applied = self.sdr_white_scale,
|
||||||
|
"cursor composite: HDR SDR-white scale (1.0 = 80 nits; None = \
|
||||||
|
query failed — keeping the prior value)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if !self.cursor_blend_failed {
|
||||||
|
self.cursor_blend_failed = true;
|
||||||
|
tracing::warn!(
|
||||||
|
"cursor blend scratch creation failed — capture-model frames stay \
|
||||||
|
pointer-less this session"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let (tex, srv, ..) = self.blend_scratch.as_ref().expect("just ensured");
|
||||||
|
let (tex, srv) = (tex.clone(), srv.clone());
|
||||||
|
self.context.CopyResource(&tex, slot_tex);
|
||||||
|
// Blend the pointer (visible shapes only; hidden = the copy alone is the frame).
|
||||||
|
let overlay = self.cursor_poll.as_ref().and_then(|p| p.read());
|
||||||
|
self.last_blend_key = overlay.as_ref().map(|o| (o.serial, o.x, o.y, o.visible));
|
||||||
|
if let Some(ov) = overlay.filter(|o| o.visible) {
|
||||||
|
if self.cursor_blend.is_none() && !self.cursor_blend_failed {
|
||||||
|
match cursor_blend::CursorBlendPass::new(&self.device) {
|
||||||
|
Ok(p) => self.cursor_blend = Some(p),
|
||||||
|
Err(e) => {
|
||||||
|
self.cursor_blend_failed = true;
|
||||||
|
tracing::warn!(
|
||||||
|
"cursor blend pass build failed — capture-model frames stay \
|
||||||
|
pointer-less this session: {e:#}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(pass) = self.cursor_blend.as_mut() {
|
||||||
|
// FP16 ring = scRGB linear composition (HDR): linearize the sRGB shape and
|
||||||
|
// scale it to the target's SDR white so it matches the desktop around it.
|
||||||
|
let scale = if self.display_hdr {
|
||||||
|
self.sdr_white_scale
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
if let Err(e) = pass.blend(&self.device, &self.context, &tex, &ov, scale) {
|
||||||
|
if !self.cursor_blend_failed {
|
||||||
|
self.cursor_blend_failed = true;
|
||||||
|
tracing::warn!("cursor blend draw failed — pointer-less frames: {e:#}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((tex, srv))
|
||||||
}
|
}
|
||||||
Some((tex, srv))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The secure-desktop guard (the 0.18.0 UAC/Winlogon regression). UAC consent and Winlogon
|
/// The secure-desktop guard (the 0.18.0 UAC/Winlogon regression). UAC consent and Winlogon
|
||||||
|
|||||||
@@ -57,57 +57,62 @@ pub(super) struct CursorBlendPass {
|
|||||||
|
|
||||||
impl CursorBlendPass {
|
impl CursorBlendPass {
|
||||||
pub(super) unsafe fn new(device: &ID3D11Device) -> Result<Self> {
|
pub(super) unsafe fn new(device: &ID3D11Device) -> Result<Self> {
|
||||||
let vsb = crate::dxgi::compile_shader(crate::dxgi::HDR_VS, s!("main"), s!("vs_5_0"))?;
|
// SAFETY: `?`-checked D3D11 resource creation on the live `device` borrow, over
|
||||||
let psb = crate::dxgi::compile_shader(CURSOR_PS, s!("main"), s!("ps_5_0"))?;
|
// fully-initialized stack descriptors and live out-params; `compile_shader` receives `s!()`
|
||||||
let mut vs = None;
|
// literals (its contract).
|
||||||
device.CreateVertexShader(&vsb, None, Some(&mut vs))?;
|
unsafe {
|
||||||
let mut ps = None;
|
let vsb = crate::dxgi::compile_shader(crate::dxgi::HDR_VS, s!("main"), s!("vs_5_0"))?;
|
||||||
device.CreatePixelShader(&psb, None, Some(&mut ps))?;
|
let psb = crate::dxgi::compile_shader(CURSOR_PS, s!("main"), s!("ps_5_0"))?;
|
||||||
let sd = D3D11_SAMPLER_DESC {
|
let mut vs = None;
|
||||||
// LINEAR: the quad is drawn 1:1 in frame pixels, so this only matters at the
|
device.CreateVertexShader(&vsb, None, Some(&mut vs))?;
|
||||||
// half-texel edges; linear keeps them soft instead of ringing.
|
let mut ps = None;
|
||||||
Filter: D3D11_FILTER_MIN_MAG_MIP_LINEAR,
|
device.CreatePixelShader(&psb, None, Some(&mut ps))?;
|
||||||
AddressU: D3D11_TEXTURE_ADDRESS_CLAMP,
|
let sd = D3D11_SAMPLER_DESC {
|
||||||
AddressV: D3D11_TEXTURE_ADDRESS_CLAMP,
|
// LINEAR: the quad is drawn 1:1 in frame pixels, so this only matters at the
|
||||||
AddressW: D3D11_TEXTURE_ADDRESS_CLAMP,
|
// half-texel edges; linear keeps them soft instead of ringing.
|
||||||
ComparisonFunc: D3D11_COMPARISON_NEVER,
|
Filter: D3D11_FILTER_MIN_MAG_MIP_LINEAR,
|
||||||
MaxLOD: f32::MAX,
|
AddressU: D3D11_TEXTURE_ADDRESS_CLAMP,
|
||||||
..Default::default()
|
AddressV: D3D11_TEXTURE_ADDRESS_CLAMP,
|
||||||
};
|
AddressW: D3D11_TEXTURE_ADDRESS_CLAMP,
|
||||||
let mut sampler = None;
|
ComparisonFunc: D3D11_COMPARISON_NEVER,
|
||||||
device.CreateSamplerState(&sd, Some(&mut sampler))?;
|
MaxLOD: f32::MAX,
|
||||||
// Straight-alpha over: dst.rgb = src.rgb*a + dst.rgb*(1-a); keep dst alpha.
|
..Default::default()
|
||||||
let mut bd = D3D11_BLEND_DESC::default();
|
};
|
||||||
bd.RenderTarget[0] = D3D11_RENDER_TARGET_BLEND_DESC {
|
let mut sampler = None;
|
||||||
BlendEnable: true.into(),
|
device.CreateSamplerState(&sd, Some(&mut sampler))?;
|
||||||
SrcBlend: D3D11_BLEND_SRC_ALPHA,
|
// Straight-alpha over: dst.rgb = src.rgb*a + dst.rgb*(1-a); keep dst alpha.
|
||||||
DestBlend: D3D11_BLEND_INV_SRC_ALPHA,
|
let mut bd = D3D11_BLEND_DESC::default();
|
||||||
BlendOp: D3D11_BLEND_OP_ADD,
|
bd.RenderTarget[0] = D3D11_RENDER_TARGET_BLEND_DESC {
|
||||||
SrcBlendAlpha: D3D11_BLEND_ONE,
|
BlendEnable: true.into(),
|
||||||
DestBlendAlpha: D3D11_BLEND_ONE,
|
SrcBlend: D3D11_BLEND_SRC_ALPHA,
|
||||||
BlendOpAlpha: D3D11_BLEND_OP_ADD,
|
DestBlend: D3D11_BLEND_INV_SRC_ALPHA,
|
||||||
RenderTargetWriteMask: 0x0F,
|
BlendOp: D3D11_BLEND_OP_ADD,
|
||||||
};
|
SrcBlendAlpha: D3D11_BLEND_ONE,
|
||||||
let mut blend = None;
|
DestBlendAlpha: D3D11_BLEND_ONE,
|
||||||
device.CreateBlendState(&bd, Some(&mut blend))?;
|
BlendOpAlpha: D3D11_BLEND_OP_ADD,
|
||||||
let cbd = D3D11_BUFFER_DESC {
|
RenderTargetWriteMask: 0x0F,
|
||||||
ByteWidth: 16, // float to_linear + float3 pad
|
};
|
||||||
Usage: D3D11_USAGE_DYNAMIC,
|
let mut blend = None;
|
||||||
BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32,
|
device.CreateBlendState(&bd, Some(&mut blend))?;
|
||||||
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
|
let cbd = D3D11_BUFFER_DESC {
|
||||||
..Default::default()
|
ByteWidth: 16, // float to_linear + float3 pad
|
||||||
};
|
Usage: D3D11_USAGE_DYNAMIC,
|
||||||
let mut cbuf = None;
|
BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32,
|
||||||
device.CreateBuffer(&cbd, None, Some(&mut cbuf))?;
|
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
|
||||||
Ok(Self {
|
..Default::default()
|
||||||
vs: vs.context("cursor blend vs")?,
|
};
|
||||||
ps: ps.context("cursor blend ps")?,
|
let mut cbuf = None;
|
||||||
sampler: sampler.context("cursor blend sampler")?,
|
device.CreateBuffer(&cbd, None, Some(&mut cbuf))?;
|
||||||
blend: blend.context("cursor blend state")?,
|
Ok(Self {
|
||||||
cbuf: cbuf.context("cursor blend cbuf")?,
|
vs: vs.context("cursor blend vs")?,
|
||||||
cbuf_scale: None,
|
ps: ps.context("cursor blend ps")?,
|
||||||
shape: None,
|
sampler: sampler.context("cursor blend sampler")?,
|
||||||
})
|
blend: blend.context("cursor blend state")?,
|
||||||
|
cbuf: cbuf.context("cursor blend cbuf")?,
|
||||||
|
cbuf_scale: None,
|
||||||
|
shape: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upload `ov`'s bitmap if its serial is new; reuse the cached SRV otherwise.
|
/// Upload `ov`'s bitmap if its serial is new; reuse the cached SRV otherwise.
|
||||||
@@ -116,42 +121,48 @@ impl CursorBlendPass {
|
|||||||
device: &ID3D11Device,
|
device: &ID3D11Device,
|
||||||
ov: &pf_frame::CursorOverlay,
|
ov: &pf_frame::CursorOverlay,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if self.shape.as_ref().is_some_and(|(s, ..)| *s == ov.serial) {
|
// SAFETY: `CreateTexture2D`/`CreateShaderResourceView` are `?`-checked calls on the live
|
||||||
return Ok(());
|
// `device` borrow. `init.pSysMem` points into `ov.rgba`, which the length check above proves
|
||||||
|
// holds at least `ov.w * ov.h * 4` bytes for the declared `SysMemPitch = ov.w * 4`, and which
|
||||||
|
// outlives the synchronous upload.
|
||||||
|
unsafe {
|
||||||
|
if self.shape.as_ref().is_some_and(|(s, ..)| *s == ov.serial) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if ov.rgba.len() < (ov.w as usize) * (ov.h as usize) * 4 || ov.w == 0 || ov.h == 0 {
|
||||||
|
bail!("malformed cursor overlay ({}x{})", ov.w, ov.h);
|
||||||
|
}
|
||||||
|
let desc = D3D11_TEXTURE2D_DESC {
|
||||||
|
Width: ov.w,
|
||||||
|
Height: ov.h,
|
||||||
|
MipLevels: 1,
|
||||||
|
ArraySize: 1,
|
||||||
|
Format: DXGI_FORMAT_R8G8B8A8_UNORM,
|
||||||
|
SampleDesc: DXGI_SAMPLE_DESC {
|
||||||
|
Count: 1,
|
||||||
|
Quality: 0,
|
||||||
|
},
|
||||||
|
Usage: D3D11_USAGE_DEFAULT,
|
||||||
|
BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let init = D3D11_SUBRESOURCE_DATA {
|
||||||
|
pSysMem: ov.rgba.as_ptr().cast(),
|
||||||
|
SysMemPitch: ov.w * 4,
|
||||||
|
SysMemSlicePitch: 0,
|
||||||
|
};
|
||||||
|
let mut tex: Option<ID3D11Texture2D> = None;
|
||||||
|
device
|
||||||
|
.CreateTexture2D(&desc, Some(&init), Some(&mut tex))
|
||||||
|
.context("CreateTexture2D(cursor shape)")?;
|
||||||
|
let tex = tex.context("null cursor shape texture")?;
|
||||||
|
let mut srv: Option<ID3D11ShaderResourceView> = None;
|
||||||
|
device
|
||||||
|
.CreateShaderResourceView(&tex, None, Some(&mut srv))
|
||||||
|
.context("CreateShaderResourceView(cursor shape)")?;
|
||||||
|
self.shape = Some((ov.serial, srv.context("null cursor shape srv")?, ov.w, ov.h));
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
if ov.rgba.len() < (ov.w as usize) * (ov.h as usize) * 4 || ov.w == 0 || ov.h == 0 {
|
|
||||||
bail!("malformed cursor overlay ({}x{})", ov.w, ov.h);
|
|
||||||
}
|
|
||||||
let desc = D3D11_TEXTURE2D_DESC {
|
|
||||||
Width: ov.w,
|
|
||||||
Height: ov.h,
|
|
||||||
MipLevels: 1,
|
|
||||||
ArraySize: 1,
|
|
||||||
Format: DXGI_FORMAT_R8G8B8A8_UNORM,
|
|
||||||
SampleDesc: DXGI_SAMPLE_DESC {
|
|
||||||
Count: 1,
|
|
||||||
Quality: 0,
|
|
||||||
},
|
|
||||||
Usage: D3D11_USAGE_DEFAULT,
|
|
||||||
BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let init = D3D11_SUBRESOURCE_DATA {
|
|
||||||
pSysMem: ov.rgba.as_ptr().cast(),
|
|
||||||
SysMemPitch: ov.w * 4,
|
|
||||||
SysMemSlicePitch: 0,
|
|
||||||
};
|
|
||||||
let mut tex: Option<ID3D11Texture2D> = None;
|
|
||||||
device
|
|
||||||
.CreateTexture2D(&desc, Some(&init), Some(&mut tex))
|
|
||||||
.context("CreateTexture2D(cursor shape)")?;
|
|
||||||
let tex = tex.context("null cursor shape texture")?;
|
|
||||||
let mut srv: Option<ID3D11ShaderResourceView> = None;
|
|
||||||
device
|
|
||||||
.CreateShaderResourceView(&tex, None, Some(&mut srv))
|
|
||||||
.context("CreateShaderResourceView(cursor shape)")?;
|
|
||||||
self.shape = Some((ov.serial, srv.context("null cursor shape srv")?, ov.w, ov.h));
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Alpha-blend `ov` onto `dst` (frame-sized, RENDER_TARGET-capable — the blend scratch).
|
/// Alpha-blend `ov` onto `dst` (frame-sized, RENDER_TARGET-capable — the blend scratch).
|
||||||
@@ -167,50 +178,58 @@ impl CursorBlendPass {
|
|||||||
ov: &pf_frame::CursorOverlay,
|
ov: &pf_frame::CursorOverlay,
|
||||||
linear_scale: f32,
|
linear_scale: f32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.ensure_shape(device, ov)?;
|
// SAFETY: all D3D11 work on the caller's live `device`/`ctx` borrows. The
|
||||||
let (_, srv, w, h) = self.shape.as_ref().expect("shape just ensured");
|
// `copy_nonoverlapping` writes `cb.len()` `f32`s into the pointer the immediately preceding
|
||||||
if self.cbuf_scale != Some(linear_scale) {
|
// `Map` of `self.cbuf` (16 bytes = 4×`f32`, DYNAMIC/WRITE_DISCARD) returned, inside the
|
||||||
let cb: [f32; 4] = [linear_scale, 0.0, 0.0, 0.0];
|
// `is_ok()` arm and before the paired `Unmap`. `ensure_shape` forwards this fn's `device`
|
||||||
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
// borrow, and every `*Set*`/`Draw` takes borrowed slices of live locals or clones of live COM
|
||||||
if ctx
|
// interfaces.
|
||||||
.Map(&self.cbuf, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut mapped))
|
unsafe {
|
||||||
.is_ok()
|
self.ensure_shape(device, ov)?;
|
||||||
{
|
let (_, srv, w, h) = self.shape.as_ref().expect("shape just ensured");
|
||||||
std::ptr::copy_nonoverlapping(cb.as_ptr(), mapped.pData as *mut f32, cb.len());
|
if self.cbuf_scale != Some(linear_scale) {
|
||||||
ctx.Unmap(&self.cbuf, 0);
|
let cb: [f32; 4] = [linear_scale, 0.0, 0.0, 0.0];
|
||||||
|
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
||||||
|
if ctx
|
||||||
|
.Map(&self.cbuf, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut mapped))
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
std::ptr::copy_nonoverlapping(cb.as_ptr(), mapped.pData as *mut f32, cb.len());
|
||||||
|
ctx.Unmap(&self.cbuf, 0);
|
||||||
|
}
|
||||||
|
self.cbuf_scale = Some(linear_scale);
|
||||||
}
|
}
|
||||||
self.cbuf_scale = Some(linear_scale);
|
let mut rtv: Option<ID3D11RenderTargetView> = None;
|
||||||
}
|
device
|
||||||
let mut rtv: Option<ID3D11RenderTargetView> = None;
|
.CreateRenderTargetView(dst, None, Some(&mut rtv))
|
||||||
device
|
.context("CreateRenderTargetView(cursor blend scratch)")?;
|
||||||
.CreateRenderTargetView(dst, None, Some(&mut rtv))
|
let rtv = rtv.context("null cursor blend rtv")?;
|
||||||
.context("CreateRenderTargetView(cursor blend scratch)")?;
|
|
||||||
let rtv = rtv.context("null cursor blend rtv")?;
|
|
||||||
|
|
||||||
ctx.OMSetRenderTargets(Some(&[Some(rtv)]), None);
|
ctx.OMSetRenderTargets(Some(&[Some(rtv)]), None);
|
||||||
ctx.OMSetBlendState(&self.blend, None, 0xffff_ffff);
|
ctx.OMSetBlendState(&self.blend, None, 0xffff_ffff);
|
||||||
ctx.VSSetShader(&self.vs, None);
|
ctx.VSSetShader(&self.vs, None);
|
||||||
ctx.PSSetShader(&self.ps, None);
|
ctx.PSSetShader(&self.ps, None);
|
||||||
ctx.PSSetShaderResources(0, Some(&[Some(srv.clone())]));
|
ctx.PSSetShaderResources(0, Some(&[Some(srv.clone())]));
|
||||||
ctx.PSSetSamplers(0, Some(&[Some(self.sampler.clone())]));
|
ctx.PSSetSamplers(0, Some(&[Some(self.sampler.clone())]));
|
||||||
ctx.PSSetConstantBuffers(0, Some(&[Some(self.cbuf.clone())]));
|
ctx.PSSetConstantBuffers(0, Some(&[Some(self.cbuf.clone())]));
|
||||||
ctx.IASetInputLayout(None);
|
ctx.IASetInputLayout(None);
|
||||||
ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
||||||
// Placement IS the viewport: the VS fills it, the OS clips it to the target.
|
// Placement IS the viewport: the VS fills it, the OS clips it to the target.
|
||||||
let vp = D3D11_VIEWPORT {
|
let vp = D3D11_VIEWPORT {
|
||||||
TopLeftX: ov.x as f32,
|
TopLeftX: ov.x as f32,
|
||||||
TopLeftY: ov.y as f32,
|
TopLeftY: ov.y as f32,
|
||||||
Width: *w as f32,
|
Width: *w as f32,
|
||||||
Height: *h as f32,
|
Height: *h as f32,
|
||||||
MinDepth: 0.0,
|
MinDepth: 0.0,
|
||||||
MaxDepth: 1.0,
|
MaxDepth: 1.0,
|
||||||
};
|
};
|
||||||
ctx.RSSetViewports(Some(&[vp]));
|
ctx.RSSetViewports(Some(&[vp]));
|
||||||
ctx.Draw(3, 0);
|
ctx.Draw(3, 0);
|
||||||
// Unbind so the scratch can be bound as a conversion INPUT without a hazard warning.
|
// Unbind so the scratch can be bound as a conversion INPUT without a hazard warning.
|
||||||
ctx.OMSetRenderTargets(None, None);
|
ctx.OMSetRenderTargets(None, None);
|
||||||
let none_srv: [Option<ID3D11ShaderResourceView>; 1] = [None];
|
let none_srv: [Option<ID3D11ShaderResourceView>; 1] = [None];
|
||||||
ctx.PSSetShaderResources(0, Some(&none_srv));
|
ctx.PSSetShaderResources(0, Some(&none_srv));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,13 +140,17 @@ impl Capturer for SyntheticNv12Capturer {
|
|||||||
/// # Safety
|
/// # Safety
|
||||||
/// Calls DXGI factory/adapter enumeration; returns owned COM objects or an error.
|
/// Calls DXGI factory/adapter enumeration; returns owned COM objects or an error.
|
||||||
unsafe fn resolve_render_adapter() -> Result<IDXGIAdapter1> {
|
unsafe fn resolve_render_adapter() -> Result<IDXGIAdapter1> {
|
||||||
let factory: IDXGIFactory4 = CreateDXGIFactory1().context("CreateDXGIFactory1")?;
|
// SAFETY: three `?`/`Ok`-checked DXGI enumeration calls over owned locals — the factory is
|
||||||
if let Some(luid) = pf_gpu::resolve_render_adapter_luid() {
|
// created here and the adapters it returns own their own COM references. No raw pointers.
|
||||||
if let Ok(a) = factory.EnumAdapterByLuid::<IDXGIAdapter1>(luid) {
|
unsafe {
|
||||||
return Ok(a);
|
let factory: IDXGIFactory4 = CreateDXGIFactory1().context("CreateDXGIFactory1")?;
|
||||||
|
if let Some(luid) = pf_gpu::resolve_render_adapter_luid() {
|
||||||
|
if let Ok(a) = factory.EnumAdapterByLuid::<IDXGIAdapter1>(luid) {
|
||||||
|
return Ok(a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
factory.EnumAdapters1(0).context("EnumAdapters1(0)")
|
||||||
}
|
}
|
||||||
factory.EnumAdapters1(0).context("EnumAdapters1(0)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an NV12 `Texture2D` with the given usage/CPU-access/bind flags.
|
/// Create an NV12 `Texture2D` with the given usage/CPU-access/bind flags.
|
||||||
@@ -177,8 +181,12 @@ unsafe fn create_nv12(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut tex: Option<ID3D11Texture2D> = None;
|
let mut tex: Option<ID3D11Texture2D> = None;
|
||||||
device
|
// SAFETY: one `?`-checked `CreateTexture2D` on the live `device` borrow (per the contract
|
||||||
.CreateTexture2D(&desc, None, Some(&mut tex))
|
// above), with a fully-initialized stack descriptor and a live `Option` out-param.
|
||||||
.context("CreateTexture2D(NV12)")?;
|
unsafe {
|
||||||
|
device
|
||||||
|
.CreateTexture2D(&desc, None, Some(&mut tex))
|
||||||
|
.context("CreateTexture2D(NV12)")?;
|
||||||
|
}
|
||||||
tex.context("CreateTexture2D returned a null NV12 texture")
|
tex.context("CreateTexture2D returned a null NV12 texture")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user