feat(windows/capture): host-side cursor compositing for the capture model — the deterministic fix
Root cause, proven on-glass across five driver builds: a declared IddCx hardware cursor is IRREVOCABLE. There is no un-declare DDI, the empty-caps re-setup is rejected INVALID_PARAMETER, and after a successful same-mode re-commit with the driver's re-declare provably suppressed (sticky per-target flag, zero re-setups logged) DWM still never composites the software cursor back into the monitor's frames. Every DWM-based composite flip is therefore unfixable. The composite (capture) model is now implemented in OUR pipeline: - the driver keeps its hardware cursor declared for the session's whole life (the state that works) — frames stay pointer-free always; - in composite mode try_consume routes the conversion through a blend scratch: slot copy + one alpha-blended quad of the GDI poller's shape at its polled position (CursorBlendPass — fullscreen-triangle VS with viewport placement, straight-alpha PS, sRGB→linear for FP16/HDR rings), covering all four convert paths (NV12, 4:4:4 copy, P010, PyroWave) GPU-side under the slot's keyed mutex; - pointer-only motion produces no driver publish (declared hw cursor), so try_consume REGENERATES from the last slot whenever the polled cursor state changes — the cursor moves on a static desktop at tick rate, without faking freshness for the stall/death watchdogs; - set_cursor_forward becomes purely host-internal state; the SET_CURSOR_FORWARD IOCTL machinery is no longer used by the host (driver keeps it, dormant) and its sender plumbing is removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -79,12 +79,13 @@ pub trait Capturer: Send {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// LIVE cursor-render flip for a cursor-forward session (design/remote-desktop-sweep.md §8):
|
/// LIVE cursor-render flip for a cursor-forward session (design/remote-desktop-sweep.md §8):
|
||||||
/// `on = true` — the client draws the pointer, keep it OUT of the video (the Windows IDD
|
/// `on = true` — the client draws the pointer, keep it OUT of the video; `on = false` —
|
||||||
/// capturer (re)declares the driver's hardware cursor so DWM excludes it); `on = false` —
|
/// the capture mouse model, the pointer must be IN the video again. The Windows IDD
|
||||||
/// the client flipped to the capture mouse model, the pointer must be IN the video again
|
/// capturer implements the composite side ITSELF (slot-copy + alpha-blended quad from the
|
||||||
/// (the IDD capturer un-declares, DWM composites — the pre-channel path). Default no-op:
|
/// GDI poller) — a declared IddCx hardware cursor is irrevocable, so DWM can never be
|
||||||
/// the Linux portal never bakes the pointer into frames — the encode loop blends its
|
/// handed the job back. Called every encode tick (implementations cache; steady state is
|
||||||
/// overlay instead, so there is nothing to switch at the capture layer.
|
/// one compare). Default no-op: the Linux portal never bakes the pointer into frames —
|
||||||
|
/// the encode loop blends its overlay instead.
|
||||||
fn set_cursor_forward(&mut self, _on: bool) {}
|
fn set_cursor_forward(&mut self, _on: bool) {}
|
||||||
|
|
||||||
fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> {
|
fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> {
|
||||||
@@ -395,14 +396,6 @@ pub type CursorChannelSender = std::sync::Arc<
|
|||||||
dyn Fn(&pf_driver_proto::control::SetCursorChannelRequest) -> Result<()> + Send + Sync,
|
dyn Fn(&pf_driver_proto::control::SetCursorChannelRequest) -> Result<()> + Send + Sync,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/// Mid-session cursor-render flip (`IOCTL_SET_CURSOR_FORWARD`, proto v6) — the live sibling of
|
|
||||||
/// [`CursorChannelSender`], retained by the capturer for the session's lifetime so the client's
|
|
||||||
/// mouse-model flips can (un)declare the driver's hardware cursor at any time.
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub type CursorForwardSender = std::sync::Arc<
|
|
||||||
dyn Fn(&pf_driver_proto::control::SetCursorForwardRequest) -> Result<()> + Send + Sync,
|
|
||||||
>;
|
|
||||||
|
|
||||||
// One-time PipeWire library init, shared by the video (portal) and audio capture threads.
|
// One-time PipeWire library init, shared by the video (portal) and audio capture threads.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub mod pwinit;
|
pub mod pwinit;
|
||||||
@@ -487,7 +480,6 @@ pub fn open_idd_push(
|
|||||||
keepalive: Box<dyn Send>,
|
keepalive: Box<dyn Send>,
|
||||||
sender: FrameChannelSender,
|
sender: FrameChannelSender,
|
||||||
cursor_sender: Option<CursorChannelSender>,
|
cursor_sender: Option<CursorChannelSender>,
|
||||||
cursor_forward_sender: Option<CursorForwardSender>,
|
|
||||||
) -> std::result::Result<Box<dyn Capturer>, (anyhow::Error, Box<dyn Send>)> {
|
) -> std::result::Result<Box<dyn Capturer>, (anyhow::Error, Box<dyn Send>)> {
|
||||||
idd_push::IddPushCapturer::open(
|
idd_push::IddPushCapturer::open(
|
||||||
target,
|
target,
|
||||||
@@ -498,7 +490,6 @@ pub fn open_idd_push(
|
|||||||
keepalive,
|
keepalive,
|
||||||
sender,
|
sender,
|
||||||
cursor_sender,
|
cursor_sender,
|
||||||
cursor_forward_sender,
|
|
||||||
)
|
)
|
||||||
.map(|c| Box::new(c) as Box<dyn Capturer>)
|
.map(|c| Box::new(c) as Box<dyn Capturer>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ pub fn install_gpu_pref_hook() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
let mut blob: Option<ID3DBlob> = None;
|
||||||
let mut errs: Option<ID3DBlob> = None;
|
let mut errs: Option<ID3DBlob> = None;
|
||||||
let r = D3DCompile(
|
let r = D3DCompile(
|
||||||
@@ -194,7 +194,7 @@ unsafe fn compile_shader(src: &str, entry: PCSTR, target: PCSTR) -> Result<Vec<u
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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).
|
||||||
const HDR_VS: &str = r"
|
pub(crate) const HDR_VS: &str = r"
|
||||||
struct VOut { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
|
struct VOut { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
|
||||||
VOut main(uint vid : SV_VertexID) {
|
VOut main(uint vid : SV_VertexID) {
|
||||||
float2 uv = float2((vid << 1) & 2, vid & 2);
|
float2 uv = float2((vid << 1) & 2, vid & 2);
|
||||||
|
|||||||
@@ -367,6 +367,8 @@ pub unsafe fn verify_is_wudfhost(process: HANDLE, wudf_pid: u32, what: &str) ->
|
|||||||
mod channel;
|
mod channel;
|
||||||
#[path = "idd_push/cursor.rs"]
|
#[path = "idd_push/cursor.rs"]
|
||||||
mod cursor;
|
mod cursor;
|
||||||
|
#[path = "idd_push/cursor_blend.rs"]
|
||||||
|
mod cursor_blend;
|
||||||
#[path = "idd_push/cursor_poll.rs"]
|
#[path = "idd_push/cursor_poll.rs"]
|
||||||
mod cursor_poll;
|
mod cursor_poll;
|
||||||
#[path = "idd_push/descriptor.rs"]
|
#[path = "idd_push/descriptor.rs"]
|
||||||
@@ -400,22 +402,33 @@ pub struct IddPushCapturer {
|
|||||||
/// The GDI cursor-shape poller (design §8): the overlay source while alive. `Some` exactly
|
/// The GDI cursor-shape poller (design §8): the overlay source while alive. `Some` exactly
|
||||||
/// when `cursor_shared` is (both ride the negotiated cursor channel + successful delivery).
|
/// when `cursor_shared` is (both ride the negotiated cursor channel + successful delivery).
|
||||||
cursor_poll: Option<cursor_poll::CursorPoller>,
|
cursor_poll: Option<cursor_poll::CursorPoller>,
|
||||||
/// Retained live-flip sender (`IOCTL_SET_CURSOR_FORWARD`): the client's mouse-model flips
|
|
||||||
/// (un)declare the driver's hardware cursor mid-session ([`Capturer::set_cursor_forward`]).
|
|
||||||
/// `None` = no cursor channel this session, or an older driver — the flip degrades to a
|
|
||||||
/// logged no-op (exclusion stays as-is).
|
|
||||||
cursor_forward_sender: Option<crate::CursorForwardSender>,
|
|
||||||
/// Retained delivery sender (`IOCTL_SET_CURSOR_CHANNEL`) for RE-delivery: a driver-side
|
/// Retained delivery sender (`IOCTL_SET_CURSOR_CHANNEL`) for RE-delivery: a driver-side
|
||||||
/// monitor re-arrival (match-window re-arrival resize, a sibling session recreating the
|
/// monitor re-arrival (match-window re-arrival resize, a sibling session recreating the
|
||||||
/// shared slot) destroys the driver's cursor worker — the section here survives, so the
|
/// shared slot) destroys the driver's cursor worker — the section here survives, so the
|
||||||
/// channel is re-delivered on ring recreates and on a flip that reports NOT_FOUND.
|
/// channel is re-delivered on ring recreates.
|
||||||
cursor_sender: Option<crate::CursorChannelSender>,
|
cursor_sender: Option<crate::CursorChannelSender>,
|
||||||
/// The last cursor-render state APPLIED to the driver (`None` = never / must re-apply).
|
/// The CAPTURE mouse model is active — the HOST composites the pointer into the frame
|
||||||
/// The stream loop calls [`Capturer::set_cursor_forward`] every tick; this cache makes
|
/// (see cursor_blend.rs for why DWM cannot: a declared IddCx hardware cursor is forever).
|
||||||
/// that an Option compare in steady state, and resetting it to `None` after any channel
|
composite_cursor: bool,
|
||||||
/// (re)delivery makes the state survive driver-side monitor re-arrivals (whose fresh
|
/// The cursor-quad blend pass (lazy; per capture device). `None` after a build failure —
|
||||||
/// entries default to declared-at-delivery).
|
/// composite mode then degrades to pointer-less frames (warned once).
|
||||||
cursor_forward_applied: Option<bool>,
|
cursor_blend: Option<cursor_blend::CursorBlendPass>,
|
||||||
|
cursor_blend_failed: bool,
|
||||||
|
/// The frame-sized blend scratch (slot copy + cursor quad): texture + SRV + (w, h, fmt)
|
||||||
|
/// it was built for — rebuilt when the ring geometry changes.
|
||||||
|
blend_scratch: Option<(
|
||||||
|
ID3D11Texture2D,
|
||||||
|
ID3D11ShaderResourceView,
|
||||||
|
u32,
|
||||||
|
u32,
|
||||||
|
DXGI_FORMAT,
|
||||||
|
)>,
|
||||||
|
/// The (serial, x, y, visible) of the LAST blended pointer — the composite-regen change
|
||||||
|
/// key: pointer-only motion produces no driver publish (the declared hardware cursor
|
||||||
|
/// doesn't dirty frames), so `try_consume` regenerates from the last slot when this moves.
|
||||||
|
last_blend_key: Option<(u64, i32, i32, bool)>,
|
||||||
|
/// The ring slot of the last FRESH publish — the regen source.
|
||||||
|
last_slot: Option<usize>,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
slots: Vec<HostSlot>,
|
slots: Vec<HostSlot>,
|
||||||
@@ -643,7 +656,6 @@ impl IddPushCapturer {
|
|||||||
keepalive: Box<dyn Send>,
|
keepalive: Box<dyn Send>,
|
||||||
sender: crate::FrameChannelSender,
|
sender: crate::FrameChannelSender,
|
||||||
cursor_sender: Option<crate::CursorChannelSender>,
|
cursor_sender: Option<crate::CursorChannelSender>,
|
||||||
cursor_forward_sender: Option<crate::CursorForwardSender>,
|
|
||||||
) -> std::result::Result<Self, (anyhow::Error, Box<dyn Send>)> {
|
) -> std::result::Result<Self, (anyhow::Error, Box<dyn Send>)> {
|
||||||
// The stall-attribution listener (idempotent): started with the first IDD-push capturer so
|
// The stall-attribution listener (idempotent): started with the first IDD-push capturer so
|
||||||
// the stall log can correlate DWM holes with OS display events for the session's lifetime.
|
// the stall log can correlate DWM holes with OS display events for the session's lifetime.
|
||||||
@@ -656,7 +668,6 @@ impl IddPushCapturer {
|
|||||||
pyrowave,
|
pyrowave,
|
||||||
sender,
|
sender,
|
||||||
cursor_sender,
|
cursor_sender,
|
||||||
cursor_forward_sender,
|
|
||||||
) {
|
) {
|
||||||
Ok(mut me) => {
|
Ok(mut me) => {
|
||||||
me._keepalive = keepalive;
|
me._keepalive = keepalive;
|
||||||
@@ -675,7 +686,6 @@ impl IddPushCapturer {
|
|||||||
pyrowave: bool,
|
pyrowave: bool,
|
||||||
sender: crate::FrameChannelSender,
|
sender: crate::FrameChannelSender,
|
||||||
cursor_sender: Option<crate::CursorChannelSender>,
|
cursor_sender: Option<crate::CursorChannelSender>,
|
||||||
cursor_forward_sender: Option<crate::CursorForwardSender>,
|
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
// The ring MUST live on the adapter the driver's swap-chain renders on. Primary: the
|
// The ring MUST live on the adapter the driver's swap-chain renders on. Primary: the
|
||||||
// selected render GPU — the same pick SET_RENDER_ADAPTER pinned the driver to at monitor
|
// selected render GPU — the same pick SET_RENDER_ADAPTER pinned the driver to at monitor
|
||||||
@@ -699,7 +709,6 @@ impl IddPushCapturer {
|
|||||||
luid,
|
luid,
|
||||||
sender.clone(),
|
sender.clone(),
|
||||||
cursor_sender.clone(),
|
cursor_sender.clone(),
|
||||||
cursor_forward_sender.clone(),
|
|
||||||
) {
|
) {
|
||||||
Ok(me) => Ok(me),
|
Ok(me) => Ok(me),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -734,7 +743,6 @@ impl IddPushCapturer {
|
|||||||
drv,
|
drv,
|
||||||
sender,
|
sender,
|
||||||
cursor_sender,
|
cursor_sender,
|
||||||
cursor_forward_sender,
|
|
||||||
)
|
)
|
||||||
.context("IDD-push rebind to the driver's reported render adapter")
|
.context("IDD-push rebind to the driver's reported render adapter")
|
||||||
}
|
}
|
||||||
@@ -751,7 +759,6 @@ impl IddPushCapturer {
|
|||||||
luid: LUID,
|
luid: LUID,
|
||||||
sender: crate::FrameChannelSender,
|
sender: crate::FrameChannelSender,
|
||||||
cursor_sender: Option<crate::CursorChannelSender>,
|
cursor_sender: Option<crate::CursorChannelSender>,
|
||||||
cursor_forward_sender: Option<crate::CursorForwardSender>,
|
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let (pw, ph, _hz) = preferred
|
let (pw, ph, _hz) = preferred
|
||||||
.context("IDD push needs the negotiated mode (WxH) to size the shared ring")?;
|
.context("IDD push needs the negotiated mode (WxH) to size the shared ring")?;
|
||||||
@@ -1076,11 +1083,13 @@ impl IddPushCapturer {
|
|||||||
status_logged: false,
|
status_logged: false,
|
||||||
cursor_shared,
|
cursor_shared,
|
||||||
cursor_poll,
|
cursor_poll,
|
||||||
cursor_forward_sender,
|
|
||||||
cursor_sender,
|
cursor_sender,
|
||||||
// Open-time deliveries declare (the driver entry's flag starts true); the first
|
composite_cursor: false,
|
||||||
// set_cursor_forward tick reconciles to the session's actual state.
|
cursor_blend: None,
|
||||||
cursor_forward_applied: None,
|
cursor_blend_failed: false,
|
||||||
|
blend_scratch: None,
|
||||||
|
last_blend_key: None,
|
||||||
|
last_slot: None,
|
||||||
// Held from BEFORE the first-frame gate (the display must not idle off while we
|
// Held from BEFORE the first-frame gate (the display must not idle off while we
|
||||||
// wait for the first compose) until the capturer drops with the session.
|
// wait for the first compose) until the capturer drops with the session.
|
||||||
_display_wake: pf_frame::session_tuning::DisplayWakeRequest::new(),
|
_display_wake: pf_frame::session_tuning::DisplayWakeRequest::new(),
|
||||||
@@ -1430,10 +1439,9 @@ impl IddPushCapturer {
|
|||||||
// hardware-cursor declaration follows the CURRENT monitor generation.
|
// hardware-cursor declaration follows the CURRENT monitor generation.
|
||||||
if let (Some(cs), Some(send)) = (self.cursor_shared.as_ref(), self.cursor_sender.as_ref()) {
|
if let (Some(cs), Some(send)) = (self.cursor_shared.as_ref(), self.cursor_sender.as_ref()) {
|
||||||
let _ = deliver_cursor_channel(&self.broker, self.target_id, cs, send);
|
let _ = deliver_cursor_channel(&self.broker, self.target_id, cs, send);
|
||||||
// The fresh driver worker declared per the (possibly re-created, default-true)
|
|
||||||
// entry flag — force the next tick to re-apply the session's actual render state.
|
|
||||||
self.cursor_forward_applied = None;
|
|
||||||
}
|
}
|
||||||
|
self.blend_scratch = None; // ring geometry/format changed — rebuild at next blend
|
||||||
|
self.last_slot = None; // old-ring slot indices are meaningless now
|
||||||
self.last_seq = 0;
|
self.last_seq = 0;
|
||||||
self.out_ring.clear(); // the output format changed → rebuild lazily at the new format
|
self.out_ring.clear(); // the output format changed → rebuild lazily at the new format
|
||||||
self.video_conv = None; // converters are sized + HDR-specific → rebuild at the new mode
|
self.video_conv = None; // converters are sized + HDR-specific → rebuild at the new mode
|
||||||
@@ -1719,6 +1727,111 @@ impl IddPushCapturer {
|
|||||||
Ok(Some((self.pyro_fence_handle, value)))
|
Ok(Some((self.pyro_fence_handle, value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The (serial, x, y, visible) of the CURRENT polled cursor — the composite-regen change
|
||||||
|
/// key. `None` while the poller has no shape yet (or isn't running).
|
||||||
|
fn cursor_blend_key(&self) -> Option<(u64, i32, i32, bool)> {
|
||||||
|
self.cursor_poll
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.read())
|
||||||
|
.map(|o| (o.serial, o.x, o.y, o.visible))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Composite the pointer for this convert: ensure the frame-sized blend scratch, copy the
|
||||||
|
/// slot into it, and alpha-blend the GDI poller's shape at its polled position. Returns the
|
||||||
|
/// scratch (texture + SRV) the conversion should read INSTEAD of the slot; `None` degrades
|
||||||
|
/// to the pointer-less slot (scratch/pass creation failed — warned once). A hidden pointer
|
||||||
|
/// blends nothing (the plain copy is the correct frame).
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// D3D11 calls on the owning capture/encode thread's device + immediate context, called
|
||||||
|
/// while holding the slot's keyed mutex (the copy reads the slot).
|
||||||
|
unsafe fn prepare_blend_scratch(
|
||||||
|
&mut self,
|
||||||
|
slot_tex: &ID3D11Texture2D,
|
||||||
|
) -> Option<(ID3D11Texture2D, ID3D11ShaderResourceView)> {
|
||||||
|
let fmt = self.ring_format();
|
||||||
|
// (Re)build the scratch at the current ring geometry.
|
||||||
|
let stale = self
|
||||||
|
.blend_scratch
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|(_, _, w, h, f)| (*w, *h, *f) != (self.width, self.height, fmt));
|
||||||
|
if stale {
|
||||||
|
self.blend_scratch = None;
|
||||||
|
let desc = D3D11_TEXTURE2D_DESC {
|
||||||
|
Width: self.width,
|
||||||
|
Height: self.height,
|
||||||
|
MipLevels: 1,
|
||||||
|
ArraySize: 1,
|
||||||
|
Format: fmt,
|
||||||
|
SampleDesc: DXGI_SAMPLE_DESC {
|
||||||
|
Count: 1,
|
||||||
|
Quality: 0,
|
||||||
|
},
|
||||||
|
Usage: D3D11_USAGE_DEFAULT,
|
||||||
|
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut tex: Option<ID3D11Texture2D> = None;
|
||||||
|
let built = self
|
||||||
|
.device
|
||||||
|
.CreateTexture2D(&desc, None, Some(&mut tex))
|
||||||
|
.ok()
|
||||||
|
.and_then(|_| tex)
|
||||||
|
.and_then(|t| {
|
||||||
|
let mut srv: Option<ID3D11ShaderResourceView> = None;
|
||||||
|
self.device
|
||||||
|
.CreateShaderResourceView(&t, None, Some(&mut srv))
|
||||||
|
.ok()
|
||||||
|
.and_then(|_| srv)
|
||||||
|
.map(|v| (t, v))
|
||||||
|
});
|
||||||
|
match built {
|
||||||
|
Some((t, v)) => self.blend_scratch = Some((t, v, self.width, self.height, fmt)),
|
||||||
|
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.
|
||||||
|
let is_linear = self.display_hdr;
|
||||||
|
if let Err(e) = pass.blend(&self.device, &self.context, &tex, &ov, is_linear) {
|
||||||
|
if !self.cursor_blend_failed {
|
||||||
|
self.cursor_blend_failed = true;
|
||||||
|
tracing::warn!("cursor blend draw failed — pointer-less frames: {e:#}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((tex, srv))
|
||||||
|
}
|
||||||
|
|
||||||
fn try_consume(&mut self) -> Result<Option<CapturedFrame>> {
|
fn try_consume(&mut self) -> Result<Option<CapturedFrame>> {
|
||||||
self.log_driver_status_once();
|
self.log_driver_status_once();
|
||||||
// Follow the display: a "Use HDR" flip recreates the ring at the matching format.
|
// Follow the display: a "Use HDR" flip recreates the ring at the matching format.
|
||||||
@@ -1779,9 +1892,25 @@ impl IddPushCapturer {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let seq = u64::from(tok.seq);
|
let seq = u64::from(tok.seq);
|
||||||
let slot = tok.slot as usize;
|
let mut slot = tok.slot as usize;
|
||||||
if seq == self.last_seq || slot >= self.slots.len() {
|
let fresh = seq != self.last_seq && slot < self.slots.len();
|
||||||
return Ok(None);
|
let mut regen = false;
|
||||||
|
if !fresh {
|
||||||
|
// Composite cursor model: pointer-only motion produces NO new publish (the declared
|
||||||
|
// hardware cursor never dirties the frame), so a static desktop would freeze the
|
||||||
|
// blended pointer. Regenerate from the LAST slot whenever the polled cursor state
|
||||||
|
// changed — the re-converted out-ring frame carries the pointer's new position.
|
||||||
|
let moved = self.composite_cursor
|
||||||
|
&& self.last_slot.is_some()
|
||||||
|
&& self.cursor_blend_key() != self.last_blend_key;
|
||||||
|
if !moved {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
slot = self.last_slot.expect("checked above");
|
||||||
|
if slot >= self.slots.len() {
|
||||||
|
return Ok(None); // ring shrank across a recreate — wait for a fresh publish
|
||||||
|
}
|
||||||
|
regen = true;
|
||||||
}
|
}
|
||||||
// Build the ring + converter BEFORE acquiring the slot so nothing between Acquire and Release
|
// Build the ring + converter BEFORE acquiring the slot so nothing between Acquire and Release
|
||||||
// can `?`-return and leak the keyed-mutex lock (which would stall the driver on that slot).
|
// can `?`-return and leak the keyed-mutex lock (which would stall the driver on that slot).
|
||||||
@@ -1815,18 +1944,31 @@ impl IddPushCapturer {
|
|||||||
// Hold the slot's keyed mutex only across the convert/copy into the host out-ring (NOT across the
|
// Hold the slot's keyed mutex only across the convert/copy into the host out-ring (NOT across the
|
||||||
// ~3 ms encode — NVENC reads the host out-ring slot, not the keyed-mutex slot), so the driver gets
|
// ~3 ms encode — NVENC reads the host out-ring slot, not the keyed-mutex slot), so the driver gets
|
||||||
// the slot back immediately and the encode of the PREVIOUS frame overlaps this convert.
|
// the slot back immediately and the encode of the PREVIOUS frame overlaps this convert.
|
||||||
let s = &self.slots[slot];
|
// Clone the slot's COM interfaces (an AddRef each) so the guard borrows LOCALS, leaving
|
||||||
|
// `self` free for the composite blend prep inside the lock.
|
||||||
|
let (slot_tex, slot_srv, slot_mutex) = {
|
||||||
|
let s = &self.slots[slot];
|
||||||
|
(s.tex.clone(), s.srv.clone(), s.mutex.clone())
|
||||||
|
};
|
||||||
// Acquire the slot's keyed mutex via a RAII guard, scoped to JUST the convert/copy below so it
|
// Acquire the slot's keyed mutex via a RAII guard, scoped to JUST the convert/copy below so it
|
||||||
// releases at the same point as the old hand-written `ReleaseSync` (the driver gets the slot back
|
// releases at the same point as the old hand-written `ReleaseSync` (the driver gets the slot back
|
||||||
// immediately, NOT held across the rest of `try_consume`) — but now leak-proof on any early return.
|
// immediately, NOT held across the rest of `try_consume`) — but now leak-proof on any early return.
|
||||||
{
|
{
|
||||||
let Some(_lock) = KeyedMutexGuard::acquire(&s.mutex, 0, 8) else {
|
let Some(_lock) = KeyedMutexGuard::acquire(&slot_mutex, 0, 8) else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
// SAFETY: convert on the owning (encode) thread's immediate context, holding the slot lock.
|
// SAFETY: convert on the owning (encode) thread's immediate context, holding the slot lock.
|
||||||
// A `?` here is leak-safe: `_lock` (the KeyedMutexGuard) drops on the early return, releasing
|
// A `?` here is leak-safe: `_lock` (the KeyedMutexGuard) drops on the early return, releasing
|
||||||
// the slot back to the driver.
|
// the slot back to the driver.
|
||||||
unsafe {
|
unsafe {
|
||||||
|
// Composite cursor model: divert the convert input through the blend scratch —
|
||||||
|
// a slot copy with the pointer quad alpha-blended on top. `None` = compositing
|
||||||
|
// off or degraded (the conversion then reads the slot as always).
|
||||||
|
let blended = if self.composite_cursor {
|
||||||
|
self.prepare_blend_scratch(&slot_tex)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
if self.pyrowave {
|
if self.pyrowave {
|
||||||
// PyroWave: ring slot SRV (BGRA for SDR, scRGB FP16 for HDR) → the two separate
|
// PyroWave: ring slot SRV (BGRA for SDR, scRGB FP16 for HDR) → the two separate
|
||||||
// plane textures via the mode-aware CSC; the shared fence signalled just after
|
// plane textures via the mode-aware CSC; the shared fence signalled just after
|
||||||
@@ -1834,22 +1976,17 @@ impl IddPushCapturer {
|
|||||||
// convert. The composition format is pinned to the negotiated depth.
|
// convert. The composition format is pinned to the negotiated depth.
|
||||||
let (_, y_rtv, _, cbcr_rtv) = pyro_slot.as_ref().expect("pyro slot");
|
let (_, y_rtv, _, cbcr_rtv) = pyro_slot.as_ref().expect("pyro slot");
|
||||||
if let Some(conv) = self.pyro_conv.as_ref() {
|
if let Some(conv) = self.pyro_conv.as_ref() {
|
||||||
conv.convert(
|
let src = blended.as_ref().map(|(_, srv)| srv).unwrap_or(&slot_srv);
|
||||||
&self.context,
|
conv.convert(&self.context, src, y_rtv, cbcr_rtv, self.width, self.height)?;
|
||||||
&s.srv,
|
|
||||||
y_rtv,
|
|
||||||
cbcr_rtv,
|
|
||||||
self.width,
|
|
||||||
self.height,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
} else if self.display_hdr {
|
} else if self.display_hdr {
|
||||||
// HDR: FP16 slot SRV → P010 (BT.2020 PQ) via the shader; NVENC takes native P010.
|
// HDR: FP16 slot SRV → P010 (BT.2020 PQ) via the shader; NVENC takes native P010.
|
||||||
if let Some(conv) = self.hdr_p010_conv.as_ref() {
|
if let Some(conv) = self.hdr_p010_conv.as_ref() {
|
||||||
|
let src = blended.as_ref().map(|(_, srv)| srv).unwrap_or(&slot_srv);
|
||||||
conv.convert(
|
conv.convert(
|
||||||
&self.device,
|
&self.device,
|
||||||
&self.context,
|
&self.context,
|
||||||
&s.srv,
|
src,
|
||||||
out.as_ref().expect("out ring"),
|
out.as_ref().expect("out ring"),
|
||||||
self.width,
|
self.width,
|
||||||
self.height,
|
self.height,
|
||||||
@@ -1859,12 +1996,14 @@ impl IddPushCapturer {
|
|||||||
// SDR 4:4:4: pass the BGRA slot through untouched — NVENC ingests full-chroma
|
// SDR 4:4:4: pass the BGRA slot through untouched — NVENC ingests full-chroma
|
||||||
// RGB and CSCs to YUV 4:4:4 itself (per the always-written BT.709 VUI). Plain
|
// RGB and CSCs to YUV 4:4:4 itself (per the always-written BT.709 VUI). Plain
|
||||||
// copy-engine move; the slot releases back to the driver immediately.
|
// copy-engine move; the slot releases back to the driver immediately.
|
||||||
|
let src = blended.as_ref().map(|(t, _)| t).unwrap_or(&slot_tex);
|
||||||
self.context
|
self.context
|
||||||
.CopyResource(out.as_ref().expect("out ring"), &s.tex);
|
.CopyResource(out.as_ref().expect("out ring"), src);
|
||||||
} else {
|
} else {
|
||||||
// SDR: BGRA slot → NV12 on the VIDEO engine; NVENC takes native NV12, no SM-side CSC.
|
// SDR: BGRA slot → NV12 on the VIDEO engine; NVENC takes native NV12, no SM-side CSC.
|
||||||
if let Some(conv) = self.video_conv.as_ref() {
|
if let Some(conv) = self.video_conv.as_ref() {
|
||||||
conv.convert(&s.tex, out.as_ref().expect("out ring"))?;
|
let src = blended.as_ref().map(|(t, _)| t).unwrap_or(&slot_tex);
|
||||||
|
conv.convert(src, out.as_ref().expect("out ring"))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1872,13 +2011,20 @@ impl IddPushCapturer {
|
|||||||
}
|
}
|
||||||
self.out_idx = (i + 1) % ring_len;
|
self.out_idx = (i + 1) % ring_len;
|
||||||
self.last_seq = seq;
|
self.last_seq = seq;
|
||||||
|
if fresh {
|
||||||
|
self.last_slot = Some(slot);
|
||||||
|
}
|
||||||
if let Some((y, _, cbcr, _)) = pyro_slot.as_ref() {
|
if let Some((y, _, cbcr, _)) = pyro_slot.as_ref() {
|
||||||
self.pyro_last = Some((y.clone(), cbcr.clone()));
|
self.pyro_last = Some((y.clone(), cbcr.clone()));
|
||||||
} else {
|
} else {
|
||||||
self.last_present = Some((out.as_ref().expect("out ring").clone(), pf));
|
self.last_present = Some((out.as_ref().expect("out ring").clone(), pf));
|
||||||
}
|
}
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if self.recovering_since.take().is_some() {
|
if regen {
|
||||||
|
// A regen re-encodes OLD desktop content at a new pointer position — it is not a
|
||||||
|
// fresh driver frame; feeding the freshness/stall bookkeeping would mask a dead
|
||||||
|
// driver and pollute stall attribution.
|
||||||
|
} else if self.recovering_since.take().is_some() {
|
||||||
// A fresh frame resumed → recovered. The recovery gap is self-inflicted (ring
|
// A fresh frame resumed → recovered. The recovery gap is self-inflicted (ring
|
||||||
// recreate, already logged by the recreate path) — reset the stall watch so it
|
// recreate, already logged by the recreate path) — reset the stall watch so it
|
||||||
// doesn't read as a DWM stall.
|
// doesn't read as a DWM stall.
|
||||||
@@ -1950,10 +2096,12 @@ impl IddPushCapturer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.last_fresh = now; // feeds the driver-death watch
|
if !regen {
|
||||||
// Build the frame. For PyroWave the encode input is the Y plane
|
self.last_fresh = now; // feeds the driver-death watch
|
||||||
// (`texture`) + the CbCr plane & fence in `pyro`; signal the shared fence
|
}
|
||||||
// after the convert above. SAFETY: on the owning capture/encode thread.
|
// Build the frame. For PyroWave the encode input is the Y plane
|
||||||
|
// (`texture`) + the CbCr plane & fence in `pyro`; signal the shared fence
|
||||||
|
// after the convert above. SAFETY: on the owning capture/encode thread.
|
||||||
let (texture, pyro) = if let Some((y, _, cbcr, _)) = pyro_slot {
|
let (texture, pyro) = if let Some((y, _, cbcr, _)) = pyro_slot {
|
||||||
// SAFETY: on the owning capture/encode thread holding the immediate context.
|
// SAFETY: on the owning capture/encode thread holding the immediate context.
|
||||||
let (fence_handle, fence_value) =
|
let (fence_handle, fence_value) =
|
||||||
@@ -2132,73 +2280,23 @@ impl Capturer for IddPushCapturer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_cursor_forward(&mut self, on: bool) {
|
fn set_cursor_forward(&mut self, on: bool) {
|
||||||
// No cursor channel this session (or delivery failed): the driver never declared the
|
// The composite (capture) model is implemented HOST-side: the driver's hardware cursor
|
||||||
// hardware cursor, DWM composites already — both flip directions are no-ops. Called
|
// stays declared for the session's whole life — the only dependable state (there is NO
|
||||||
// EVERY encode tick; the `cursor_forward_applied` cache makes the steady state one
|
// working un-declare; see cursor_blend.rs) — keeping every frame pointer-free, and the
|
||||||
// Option compare, and a channel (re)delivery clears it so re-created driver entries
|
// capturer blends the GDI poller's shape into the frame itself. No driver round-trip.
|
||||||
// get the state re-applied.
|
let composite = !on && self.cursor_shared.is_some();
|
||||||
if self.cursor_shared.is_none() || self.cursor_forward_applied == Some(on) {
|
if self.composite_cursor != composite {
|
||||||
return;
|
self.composite_cursor = composite;
|
||||||
}
|
self.last_blend_key = None; // regenerate immediately at the current pointer state
|
||||||
// Every exit below counts as applied: failures give up until the next edge/redelivery
|
tracing::info!(
|
||||||
// instead of retrying (and re-warning) sixty times a second.
|
composite,
|
||||||
self.cursor_forward_applied = Some(on);
|
"cursor render model: host compositing {}",
|
||||||
let Some(send) = self.cursor_forward_sender.as_ref() else {
|
if composite {
|
||||||
tracing::warn!(
|
"ON (capture model — blending the pointer into frames)"
|
||||||
on,
|
} else {
|
||||||
"cursor render flip requested but no forward sender — exclusion stays as-is \
|
"OFF (client draws locally)"
|
||||||
(older driver / degraded session)"
|
}
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
};
|
|
||||||
let req = pf_driver_proto::control::SetCursorForwardRequest {
|
|
||||||
target_id: self.target_id,
|
|
||||||
enable: on as u32,
|
|
||||||
};
|
|
||||||
let mut result = send(&req);
|
|
||||||
if result.is_err() {
|
|
||||||
// NOT_FOUND usually means the driver-side monitor RE-ARRIVED since delivery
|
|
||||||
// (match-window re-arrival resize / a sibling session recreating the shared slot):
|
|
||||||
// the fresh entry has no cursor worker. Re-deliver the surviving section — the
|
|
||||||
// driver spawns a new worker (declared per its flag) — then retry the flip once.
|
|
||||||
if let (Some(cs), Some(sc)) = (self.cursor_shared.as_ref(), self.cursor_sender.as_ref())
|
|
||||||
{
|
|
||||||
if deliver_cursor_channel(&self.broker, self.target_id, cs, sc) {
|
|
||||||
result = send(&req);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match result {
|
|
||||||
Ok(()) => {
|
|
||||||
tracing::info!(
|
|
||||||
target_id = self.target_id,
|
|
||||||
enable = on,
|
|
||||||
"IDD push(host): cursor forward flip delivered to the driver"
|
|
||||||
);
|
|
||||||
if !on {
|
|
||||||
// There is no IddCx un-declare DDI (empty-caps re-setup is rejected
|
|
||||||
// INVALID_PARAMETER — on-glass). The OS reverts to the SOFTWARE cursor on
|
|
||||||
// every mode commit, and the driver now skips its per-commit re-declare —
|
|
||||||
// so force a same-mode re-commit to make DWM composite the pointer NOW.
|
|
||||||
// The swap-chain flap this causes is the class the driver's preserved-
|
|
||||||
// publisher machinery already rides out.
|
|
||||||
// SAFETY: read-only CCD query + a same-config SetDisplayConfig apply over
|
|
||||||
// owned locals; mid-session there is no concurrent topology mutator (the
|
|
||||||
// manager's isolate/layout writes are session-setup-scoped).
|
|
||||||
let committed =
|
|
||||||
unsafe { pf_win_display::win_display::force_mode_reenumeration() };
|
|
||||||
tracing::info!(
|
|
||||||
committed,
|
|
||||||
"cursor composite flip: forced same-mode re-commit (software cursor \
|
|
||||||
from here — DWM composites the pointer)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => tracing::warn!(
|
|
||||||
target_id = self.target_id,
|
|
||||||
enable = on,
|
|
||||||
"cursor forward flip failed (older driver? exclusion stays as-is): {e:#}"
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
//! Host-side cursor compositing for the CAPTURE mouse model (design/remote-desktop-sweep.md §8).
|
||||||
|
//!
|
||||||
|
//! Why the host draws it: once a monitor has ever declared an IddCx hardware cursor, DWM will
|
||||||
|
//! not composite the software cursor back into its frames — there is no un-declare DDI (the
|
||||||
|
//! empty-caps re-setup is rejected `STATUS_INVALID_PARAMETER`), and a successful same-mode
|
||||||
|
//! re-commit with the driver's re-declare provably suppressed still leaves the pointer excluded
|
||||||
|
//! (all observed on-glass, 26100). So the driver keeps its hardware cursor declared for the
|
||||||
|
//! session's whole life — the state that works — and when the client flips to the capture model
|
||||||
|
//! the HOST composites the pointer into the frame itself: a slot→scratch copy plus one
|
||||||
|
//! alpha-blended quad (the GDI poller's full-fidelity shape at its polled position), entirely
|
||||||
|
//! GPU-side on the capture device, before the normal conversion runs from the scratch.
|
||||||
|
|
||||||
|
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||||
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use windows::core::s;
|
||||||
|
use windows::Win32::Graphics::Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
|
||||||
|
use windows::Win32::Graphics::Direct3D11::{
|
||||||
|
ID3D11BlendState, ID3D11Buffer, ID3D11PixelShader, ID3D11SamplerState, ID3D11VertexShader,
|
||||||
|
D3D11_BIND_CONSTANT_BUFFER, D3D11_BLEND_DESC, D3D11_BLEND_INV_SRC_ALPHA, D3D11_BLEND_ONE,
|
||||||
|
D3D11_BLEND_OP_ADD, D3D11_BLEND_SRC_ALPHA, D3D11_BUFFER_DESC, D3D11_COMPARISON_NEVER,
|
||||||
|
D3D11_CPU_ACCESS_WRITE, D3D11_FILTER_MIN_MAG_MIP_LINEAR, D3D11_MAPPED_SUBRESOURCE,
|
||||||
|
D3D11_MAP_WRITE_DISCARD, D3D11_RENDER_TARGET_BLEND_DESC, D3D11_SAMPLER_DESC,
|
||||||
|
D3D11_SUBRESOURCE_DATA, D3D11_TEXTURE_ADDRESS_CLAMP, D3D11_USAGE_DYNAMIC, D3D11_VIEWPORT,
|
||||||
|
};
|
||||||
|
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||||
|
|
||||||
|
/// Straight-alpha sample of the cursor bitmap; `to_linear` ≠ 0 linearizes sRGB→scRGB for an
|
||||||
|
/// FP16 (HDR-composed) frame so the cursor doesn't glow at 8× SDR white.
|
||||||
|
const CURSOR_PS: &str = r"
|
||||||
|
Texture2D<float4> tx : register(t0);
|
||||||
|
SamplerState sm : register(s0);
|
||||||
|
cbuffer C : register(b0) { float to_linear; float3 pad; };
|
||||||
|
float4 main(float4 pos : SV_POSITION, float2 uv : TEXCOORD0) : SV_Target {
|
||||||
|
float4 c = tx.Sample(sm, uv);
|
||||||
|
if (to_linear != 0.0) {
|
||||||
|
c.rgb = pow(abs(c.rgb), 2.2);
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
";
|
||||||
|
|
||||||
|
/// The cursor-quad blend pass + its shape-texture cache. One per capturer (device-scoped).
|
||||||
|
pub(super) struct CursorBlendPass {
|
||||||
|
vs: ID3D11VertexShader,
|
||||||
|
ps: ID3D11PixelShader,
|
||||||
|
sampler: ID3D11SamplerState,
|
||||||
|
blend: ID3D11BlendState,
|
||||||
|
cbuf: ID3D11Buffer,
|
||||||
|
cbuf_is_linear: Option<bool>,
|
||||||
|
/// The uploaded shape (serial-keyed): SRV + dims in host pixels.
|
||||||
|
shape: Option<(u64, ID3D11ShaderResourceView, u32, u32)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorBlendPass {
|
||||||
|
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"))?;
|
||||||
|
let psb = crate::dxgi::compile_shader(CURSOR_PS, s!("main"), s!("ps_5_0"))?;
|
||||||
|
let mut vs = None;
|
||||||
|
device.CreateVertexShader(&vsb, None, Some(&mut vs))?;
|
||||||
|
let mut ps = None;
|
||||||
|
device.CreatePixelShader(&psb, None, Some(&mut ps))?;
|
||||||
|
let sd = D3D11_SAMPLER_DESC {
|
||||||
|
// LINEAR: the quad is drawn 1:1 in frame pixels, so this only matters at the
|
||||||
|
// half-texel edges; linear keeps them soft instead of ringing.
|
||||||
|
Filter: D3D11_FILTER_MIN_MAG_MIP_LINEAR,
|
||||||
|
AddressU: D3D11_TEXTURE_ADDRESS_CLAMP,
|
||||||
|
AddressV: D3D11_TEXTURE_ADDRESS_CLAMP,
|
||||||
|
AddressW: D3D11_TEXTURE_ADDRESS_CLAMP,
|
||||||
|
ComparisonFunc: D3D11_COMPARISON_NEVER,
|
||||||
|
MaxLOD: f32::MAX,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut sampler = None;
|
||||||
|
device.CreateSamplerState(&sd, Some(&mut sampler))?;
|
||||||
|
// Straight-alpha over: dst.rgb = src.rgb*a + dst.rgb*(1-a); keep dst alpha.
|
||||||
|
let mut bd = D3D11_BLEND_DESC::default();
|
||||||
|
bd.RenderTarget[0] = D3D11_RENDER_TARGET_BLEND_DESC {
|
||||||
|
BlendEnable: true.into(),
|
||||||
|
SrcBlend: D3D11_BLEND_SRC_ALPHA,
|
||||||
|
DestBlend: D3D11_BLEND_INV_SRC_ALPHA,
|
||||||
|
BlendOp: D3D11_BLEND_OP_ADD,
|
||||||
|
SrcBlendAlpha: D3D11_BLEND_ONE,
|
||||||
|
DestBlendAlpha: D3D11_BLEND_ONE,
|
||||||
|
BlendOpAlpha: D3D11_BLEND_OP_ADD,
|
||||||
|
RenderTargetWriteMask: 0x0F,
|
||||||
|
};
|
||||||
|
let mut blend = None;
|
||||||
|
device.CreateBlendState(&bd, Some(&mut blend))?;
|
||||||
|
let cbd = D3D11_BUFFER_DESC {
|
||||||
|
ByteWidth: 16, // float to_linear + float3 pad
|
||||||
|
Usage: D3D11_USAGE_DYNAMIC,
|
||||||
|
BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32,
|
||||||
|
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut cbuf = None;
|
||||||
|
device.CreateBuffer(&cbd, None, Some(&mut cbuf))?;
|
||||||
|
Ok(Self {
|
||||||
|
vs: vs.context("cursor blend vs")?,
|
||||||
|
ps: ps.context("cursor blend ps")?,
|
||||||
|
sampler: sampler.context("cursor blend sampler")?,
|
||||||
|
blend: blend.context("cursor blend state")?,
|
||||||
|
cbuf: cbuf.context("cursor blend cbuf")?,
|
||||||
|
cbuf_is_linear: None,
|
||||||
|
shape: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload `ov`'s bitmap if its serial is new; reuse the cached SRV otherwise.
|
||||||
|
unsafe fn ensure_shape(
|
||||||
|
&mut self,
|
||||||
|
device: &ID3D11Device,
|
||||||
|
ov: &pf_frame::CursorOverlay,
|
||||||
|
) -> Result<()> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alpha-blend `ov` onto `dst` (frame-sized, RENDER_TARGET-capable — the blend scratch).
|
||||||
|
/// `is_linear` = the frame is FP16 scRGB (HDR composition). The quad is placed purely via
|
||||||
|
/// the viewport (the fullscreen-triangle VS fills whatever viewport is set), clipped by the
|
||||||
|
/// target automatically.
|
||||||
|
pub(super) unsafe fn blend(
|
||||||
|
&mut self,
|
||||||
|
device: &ID3D11Device,
|
||||||
|
ctx: &ID3D11DeviceContext,
|
||||||
|
dst: &ID3D11Texture2D,
|
||||||
|
ov: &pf_frame::CursorOverlay,
|
||||||
|
is_linear: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.ensure_shape(device, ov)?;
|
||||||
|
let (_, srv, w, h) = self.shape.as_ref().expect("shape just ensured");
|
||||||
|
if self.cbuf_is_linear != Some(is_linear) {
|
||||||
|
let cb: [f32; 4] = [if is_linear { 1.0 } else { 0.0 }, 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_is_linear = Some(is_linear);
|
||||||
|
}
|
||||||
|
let mut rtv: Option<ID3D11RenderTargetView> = None;
|
||||||
|
device
|
||||||
|
.CreateRenderTargetView(dst, None, Some(&mut rtv))
|
||||||
|
.context("CreateRenderTargetView(cursor blend scratch)")?;
|
||||||
|
let rtv = rtv.context("null cursor blend rtv")?;
|
||||||
|
|
||||||
|
ctx.OMSetRenderTargets(Some(&[Some(rtv)]), None);
|
||||||
|
ctx.OMSetBlendState(&self.blend, None, 0xffff_ffff);
|
||||||
|
ctx.VSSetShader(&self.vs, None);
|
||||||
|
ctx.PSSetShader(&self.ps, None);
|
||||||
|
ctx.PSSetShaderResources(0, Some(&[Some(srv.clone())]));
|
||||||
|
ctx.PSSetSamplers(0, Some(&[Some(self.sampler.clone())]));
|
||||||
|
ctx.PSSetConstantBuffers(0, Some(&[Some(self.cbuf.clone())]));
|
||||||
|
ctx.IASetInputLayout(None);
|
||||||
|
ctx.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
||||||
|
// Placement IS the viewport: the VS fills it, the OS clips it to the target.
|
||||||
|
let vp = D3D11_VIEWPORT {
|
||||||
|
TopLeftX: ov.x as f32,
|
||||||
|
TopLeftY: ov.y as f32,
|
||||||
|
Width: *w as f32,
|
||||||
|
Height: *h as f32,
|
||||||
|
MinDepth: 0.0,
|
||||||
|
MaxDepth: 1.0,
|
||||||
|
};
|
||||||
|
ctx.RSSetViewports(Some(&[vp]));
|
||||||
|
ctx.Draw(3, 0);
|
||||||
|
// Unbind so the scratch can be bound as a conversion INPUT without a hazard warning.
|
||||||
|
ctx.OMSetRenderTargets(None, None);
|
||||||
|
let none_srv: [Option<ID3D11ShaderResourceView>; 1] = [None];
|
||||||
|
ctx.PSSetShaderResources(0, Some(&none_srv));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -174,26 +174,6 @@ pub fn capture_virtual_output(
|
|||||||
},
|
},
|
||||||
) as pf_capture::CursorChannelSender
|
) as pf_capture::CursorChannelSender
|
||||||
});
|
});
|
||||||
// The mid-stream cursor-render flip (proto v6) — retained by the capturer so the client's
|
|
||||||
// mouse-model chords can (un)declare the driver's hardware cursor live. Same facade contract
|
|
||||||
// as `cursor_sender`.
|
|
||||||
let cursor_forward_sender: Option<pf_capture::CursorForwardSender> =
|
|
||||||
want.hw_cursor.then(|| {
|
|
||||||
std::sync::Arc::new(
|
|
||||||
move |req: &pf_driver_proto::control::SetCursorForwardRequest| {
|
|
||||||
// SAFETY: `control_raw` is the pf-vdisplay control handle resolved above; it is
|
|
||||||
// never closed for the process lifetime (`send_cursor_forward`'s precondition).
|
|
||||||
unsafe {
|
|
||||||
crate::vdisplay::driver::send_cursor_forward(
|
|
||||||
windows::Win32::Foundation::HANDLE(
|
|
||||||
control_raw as *mut core::ffi::c_void,
|
|
||||||
),
|
|
||||||
req,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) as pf_capture::CursorForwardSender
|
|
||||||
});
|
|
||||||
pf_capture::open_idd_push(
|
pf_capture::open_idd_push(
|
||||||
target,
|
target,
|
||||||
pref,
|
pref,
|
||||||
@@ -203,7 +183,6 @@ pub fn capture_virtual_output(
|
|||||||
keep,
|
keep,
|
||||||
sender,
|
sender,
|
||||||
cursor_sender,
|
cursor_sender,
|
||||||
cursor_forward_sender,
|
|
||||||
)
|
)
|
||||||
.map_err(|(e, _keep)| e.context("IDD-push capture open (no fallback)"))
|
.map_err(|(e, _keep)| e.context("IDD-push capture open (no fallback)"))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user