94ca4041ca
capture/linux (PipeWire portal) + capture/windows (IDD direct-push: dxgi mechanics, idd_push + submodules, synthetic_nv12) + pwinit move into crates/pf-capture behind the Capturer trait + synthetic sources (plan §W6). The crate speaks pf-frame (CapturedFrame/PixelFormat + the DXGI identity), pf-zerocopy (CUDA import), and the pf-win-display leaves, and NEVER pf-encode — the capture->encode edge is one-way. This completes the deliberate capture/encode crate split (the invasive path the plan had merged into one pf-media): capture and encode are now separate subsystem crates sharing only pf-frame. Four seams keep the capturer off the orchestrator: - VirtualOutput is EXPLODED into primitives (remote_fd/node_id/preferred_mode/ keepalive) by the host facade, so pf-capture never depends on the vdisplay type; - FrameChannelSender: the sealed-channel delivery is a Send+Sync closure the host facade builds from the pf-vdisplay control device + send_frame_channel IOCTL and hands in; ChannelBroker holds the closure instead of the control HANDLE (the whole-desktop handle-duplication security boundary is byte-for-byte unchanged); - console_session_mismatch + desktop_bounds live in pf-win-display (leaf peers); - pwinit moves here (audio caller -> pf_capture::pwinit). The host keeps capture.rs as a thin BRIDGE: it re-exports the vocabulary + capturer types (every crate::capture::* path is unchanged) and keeps open_portal_monitor / capture_virtual_output, which resolve the ZeroCopyPolicy + FrameChannelSender and call into pf-capture. verify_is_wudfhost + install_gpu_pref_hook are re-exported (the gamepad-channel bootstrap + the main.rs subcommand consume them). Co-developed: the resident-HID-mouse compose-kick hook (HID_COMPOSE_KICK + the HID-first cursor kick + _display_wake) rides this commit into pf-capture; the host mouse_windows registration side lands separately on top. Verified: Linux clippy -D warnings (pf-capture + host nvenc,vulkan-encode,pyrowave --all-targets) + host tests 299/299; Windows clippy -D warnings (pf-capture --all-targets + host nvenc,amf-qsv --all-targets) Finished exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
83 lines
3.9 KiB
Rust
83 lines
3.9 KiB
Rust
//! Capture-stall detection (plan §W4, carved out of the IDD-push capturer): flags multi-hundred-ms
|
||
//! holes in DWM frame delivery that open while the desktop was actively composing.
|
||
|
||
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||
|
||
use super::*;
|
||
|
||
/// A detected capture stall: a multi-hundred-ms hole in DWM's frame delivery that opened while the
|
||
/// desktop was actively composing right beforehand (see [`StallWatch`]).
|
||
pub(super) struct Stall {
|
||
/// How long the hole lasted (last fresh frame → the frame that ended it).
|
||
pub(super) gap: Duration,
|
||
/// `Some(mean period)` when this stall completes a metronomic cycle (see
|
||
/// [`pf_frame::metronome::Metronome`]).
|
||
pub(super) metronomic: Option<Duration>,
|
||
}
|
||
|
||
/// Capture-stall watch — the "sole virtual display" stutter diagnostic (field reports: Exclusive
|
||
/// topology = periodic double-jolt, Extend = smooth, i.e. the disturbance lives in the display/present
|
||
/// path BELOW capture and only while no physical output is active).
|
||
///
|
||
/// On a damage-driven capture an idle desktop legitimately goes quiet (no damage → no frames), so a
|
||
/// gap only counts as a stall when the [`Self::RECENT`] frames before it all arrived within
|
||
/// [`Self::ACTIVE_SPAN`] — sustained ≥ ~20 fps flow (a game or video), not a blinking caret or a
|
||
/// mouse twitch. Each stall feeds a [`pf_frame::metronome::Metronome`], so periodic stalls self-diagnose
|
||
/// in the log WITHOUT needing any client keyframe request — discriminating "DWM stopped composing"
|
||
/// from encode/network causes that the recovery-cadence detector covers. Pure logic — unit-tested
|
||
/// below; the caller does the logging.
|
||
pub(super) struct StallWatch {
|
||
/// The last [`Self::RECENT`] fresh-frame instants (pre-gap history for the activity gate).
|
||
recent: std::collections::VecDeque<Instant>,
|
||
cadence: pf_frame::metronome::Metronome,
|
||
}
|
||
|
||
impl StallWatch {
|
||
/// Frames of pre-gap history that must be tight for flow to count as active. Stalls are thus
|
||
/// naturally spaced ≥ RECENT frame times apart — no extra log rate limit needed.
|
||
const RECENT: usize = 8;
|
||
/// The RECENT pre-gap frames must all fit in this span (8 frames in 400 ms ≈ ≥ 20 fps flow —
|
||
/// loose enough for a 30 fps-capped game, tight enough to reject idle-desktop damage).
|
||
const ACTIVE_SPAN: Duration = Duration::from_millis(400);
|
||
/// The smallest hole that counts as a stall (~9 missed frames at 60 Hz) — well below the
|
||
/// reported 300–700 ms freezes, above encode/present jitter.
|
||
const STALL_MIN: Duration = Duration::from_millis(150);
|
||
|
||
pub(super) fn new() -> Self {
|
||
Self {
|
||
recent: std::collections::VecDeque::with_capacity(Self::RECENT + 1),
|
||
cadence: pf_frame::metronome::Metronome::new(),
|
||
}
|
||
}
|
||
|
||
/// Forget the flow history (a ring recreate's gap is self-inflicted, not a DWM stall — without
|
||
/// the reset the first post-recreate frame would read as one).
|
||
pub(super) fn reset(&mut self) {
|
||
self.recent.clear();
|
||
}
|
||
|
||
/// Record a fresh driver frame at `now`; `Some` exactly when it ended a stall.
|
||
pub(super) fn note_fresh(&mut self, now: Instant) -> Option<Stall> {
|
||
let was_active = self.recent.len() == Self::RECENT
|
||
&& self
|
||
.recent
|
||
.back()
|
||
.zip(self.recent.front())
|
||
.is_some_and(|(b, f)| b.duration_since(*f) <= Self::ACTIVE_SPAN);
|
||
let gap = self.recent.back().map(|last| now.duration_since(*last));
|
||
self.recent.push_back(now);
|
||
if self.recent.len() > Self::RECENT {
|
||
self.recent.pop_front();
|
||
}
|
||
let gap = gap?;
|
||
if !was_active || gap < Self::STALL_MIN {
|
||
return None;
|
||
}
|
||
Some(Stall {
|
||
gap,
|
||
metronomic: self.cadence.note(now),
|
||
})
|
||
}
|
||
}
|