From 789dafc95f2b500976bf56d73f52957673a7f53a Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 30 Jul 2026 22:21:50 +0200 Subject: [PATCH] feat(capture/windows): the stall micro-probes get an off switch, the depth pin a backoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two standing costs from the interval-stutter investigation, consolidated: - PUNKTFUNK_STALL_PROBES=0 now opts a box out of the micro-probe engine (per-GPU 10 Hz fence copies, a parked blocking-DwmFlush waiter, the 5 ms-cadence CPU sentinel). Default stays ON while the field program runs; off, stall lines keep the driver telemetry and the cheap ETW present/queue discriminator and only lose the corroborating probe legs — the verdict matrix already treats an absent probe window honestly. - The negotiated-depth pin-back stops hammering a display that refuses the flip: it was 4 CCD writes + 8 display-config queries per second, forever, all on the session-global display-config lock. After ~2 s of eager retries it re-attempts every ~4 s — still self-healing, without the drumbeat. Co-Authored-By: Claude Fable 5 --- crates/pf-capture/src/windows/idd_push.rs | 111 ++++++++++++------ .../pf-capture/src/windows/idd_push/open.rs | 5 +- crates/pf-host-config/src/lib.rs | 10 ++ 3 files changed, 89 insertions(+), 37 deletions(-) diff --git a/crates/pf-capture/src/windows/idd_push.rs b/crates/pf-capture/src/windows/idd_push.rs index 999f5884..3f60bf32 100644 --- a/crates/pf-capture/src/windows/idd_push.rs +++ b/crates/pf-capture/src/windows/idd_push.rs @@ -429,6 +429,11 @@ pub struct IddPushCapturer { /// One-shot latch for [`Self::poll_display_hdr`]'s "could not pin the negotiated depth" error — /// the poller samples every ~250 ms, and a display that refuses the flip refuses it every time. hdr_pin_warned: bool, + /// Consecutive failed depth-pin attempts ([`Self::poll_display_hdr`]) — past + /// [`Self::HDR_PIN_EAGER`] the re-pin backs off to every [`Self::HDR_PIN_RETRY_EVERY`]th + /// descriptor sample: a display that refuses the flip was costing 4 CCD writes + 8 display- + /// config queries per second, forever, all on the session-global display-config lock. + hdr_pin_failures: u32, /// The session negotiated full-chroma 4:4:4: while the display is SDR the BGRA slot passes /// THROUGH (a plain copy into the out ring, no NV12 VideoConverter) so NVENC gets full-chroma /// RGB and CSCs to 4:4:4 itself — measured on-glass: `chromaFormatIDC=3` + ARGB input yields @@ -496,8 +501,11 @@ pub struct IddPushCapturer { offered_at_fresh: u64, max_hb_age_us: u64, /// The Phase A.2 micro-probe engine (refcounted process singleton) — its window read rides - /// every stall report so the verdict matrix can name the disturbance class. - probes: Arc, + /// every stall report so the verdict matrix can name the disturbance class. `None` when + /// `PUNKTFUNK_STALL_PROBES=0` opted the box out (the engine costs standing threads); the + /// verdict matrix already treats an absent probe window as never-stalled, so reports just + /// lose the corroborating legs, never invent them. + probes: Option>, /// The Phase A.3 DxgKrnl ETW watch; `None` when the session can't start (non-admin dev run) /// — reports then say `etw=unavailable`. etw: Option>, @@ -539,6 +547,13 @@ pub struct IddPushCapturer { unsafe impl Send for IddPushCapturer {} impl IddPushCapturer { + /// Failed depth-pin attempts before [`Self::poll_display_hdr`] backs its re-pin off + /// (≈2 s of eager retries at the descriptor poller's 4 Hz). + const HDR_PIN_EAGER: u32 = 8; + /// While backed off, re-attempt the depth pin on every this-many-th descriptor sample + /// (~4 s at 4 Hz) — self-healing without the 4 Hz CCD-write drumbeat. + const HDR_PIN_RETRY_EVERY: u64 = 16; + #[inline] fn latest(&self) -> u64 { // SAFETY: `self.header` is the live, owned shared-header mapping (page-aligned, sized for a @@ -832,40 +847,63 @@ impl IddPushCapturer { // either direction (its encoder re-inits on the depth change). if (self.pyrowave || !self.client_10bit) && now.hdr != self.client_10bit { let want = self.client_10bit; - // OBSERVE the flip; never assert it. This used to discard `set_advanced_color`'s `bool` - // and then write `now.hdr = self.client_10bit` — substituting the DESIRED state for the - // observed one, which broke in both directions on a display that cannot be flipped - // (the state this file already logs as "Downgrade point D" at open): - // - want HDR, display stays SDR: the fabricated `true` differed from `current`, so two - // samples drove `recreate_ring(true, …)` and rebuilt the ring FP16 while the driver - // composed 8-bit BGRA. Every publish was then dropped by the driver's format guard, - // `recovering_since` expired, and `try_consume` bailed — a permanent 3-second - // reconnect loop. - // - want SDR, display stays HDR: the fabricated `false` MATCHED `current`, so no - // recreate ever fired and the ring stayed BGRA against an FP16 composition. Same - // dropped-publish outcome, silently. - // Reading back immediately can catch a flip that has not settled yet; that costs one - // debounce cycle (the poller re-samples in ~250 ms) and never a wrong ring, which is why - // this does not block the frame path on a settle poll the way `open_on` does. - let requested = pf_win_display::win_display::set_advanced_color(self.target_id, want); - let observed = pf_win_display::win_display::advanced_color_enabled(self.target_id); - // A failed READ is not evidence of a failed flip — keep the poller's sample then. - now.hdr = observed.unwrap_or(now.hdr); - if now.hdr != want && !self.hdr_pin_warned { - self.hdr_pin_warned = true; - tracing::error!( - target_id = self.target_id, - want_hdr = want, - observed_hdr = ?observed, - set_advanced_color_returned = requested, - pyrowave = self.pyrowave, - client_10bit = self.client_10bit, - "IDD push: could not pin the display to the NEGOTIATED depth — following what \ - it actually composes instead (a physical display forcing HDR, or a driver that \ - refuses the flip). The stream's depth will not match the negotiation; the \ - encoder's caps cross-check reports the truth to the client" - ); + // A display that refuses the pin refuses it on every 250 ms sample — past + // [`Self::HDR_PIN_EAGER`] consecutive failures the write+read-back re-fires only on + // every [`Self::HDR_PIN_RETRY_EVERY`]th sample (~4 s), instead of 4 CCD writes + + // 8 display-config queries per second forever, all on the session-global + // display-config lock. Skipped samples keep the poller's own advanced-color read, + // so nothing is fabricated and a display that becomes flippable is re-pinned on + // the next retry sample. + if self.hdr_pin_failures < Self::HDR_PIN_EAGER + || self.desc_seq % Self::HDR_PIN_RETRY_EVERY == 0 + { + // OBSERVE the flip; never assert it. This used to discard `set_advanced_color`'s + // `bool` and then write `now.hdr = self.client_10bit` — substituting the DESIRED + // state for the observed one, which broke in both directions on a display that + // cannot be flipped (the state this file already logs as "Downgrade point D" at + // open): + // - want HDR, display stays SDR: the fabricated `true` differed from `current`, + // so two samples drove `recreate_ring(true, …)` and rebuilt the ring FP16 + // while the driver composed 8-bit BGRA. Every publish was then dropped by the + // driver's format guard, `recovering_since` expired, and `try_consume` + // bailed — a permanent 3-second reconnect loop. + // - want SDR, display stays HDR: the fabricated `false` MATCHED `current`, so no + // recreate ever fired and the ring stayed BGRA against an FP16 composition. + // Same dropped-publish outcome, silently. + // Reading back immediately can catch a flip that has not settled yet; that costs + // one debounce cycle (the poller re-samples in ~250 ms) and never a wrong ring, + // which is why this does not block the frame path on a settle poll the way + // `open_on` does. + let requested = + pf_win_display::win_display::set_advanced_color(self.target_id, want); + let observed = pf_win_display::win_display::advanced_color_enabled(self.target_id); + // A failed READ is not evidence of a failed flip — keep the poller's sample then. + now.hdr = observed.unwrap_or(now.hdr); + if now.hdr != want { + self.hdr_pin_failures = self.hdr_pin_failures.saturating_add(1); + if !self.hdr_pin_warned { + self.hdr_pin_warned = true; + tracing::error!( + target_id = self.target_id, + want_hdr = want, + observed_hdr = ?observed, + set_advanced_color_returned = requested, + pyrowave = self.pyrowave, + client_10bit = self.client_10bit, + "IDD push: could not pin the display to the NEGOTIATED depth — following what \ + it actually composes instead (a physical display forcing HDR, or a driver that \ + refuses the flip). The stream's depth will not match the negotiation; the \ + encoder's caps cross-check reports the truth to the client" + ); + } + } else { + self.hdr_pin_failures = 0; + } } + } else { + // No mismatch (or the session follows flips): the pin is healthy — a later refusal + // starts its eager attempts fresh. + self.hdr_pin_failures = 0; } let current = DisplayDescriptor { hdr: self.display_hdr, @@ -1613,7 +1651,8 @@ impl IddPushCapturer { // uses (the gap plus a lead-in for the disturbance that CAUSED it). probes: now .checked_sub(stall.gap + Duration::from_millis(300)) - .map(|from| self.probes.window(from, now)), + .zip(self.probes.as_deref()) + .map(|(from, p)| p.window(from, now)), etw: self.etw.as_ref().and_then(|w| { now.checked_sub(stall.gap + Duration::from_millis(300)) .map(|from| w.summary(from, now)) diff --git a/crates/pf-capture/src/windows/idd_push/open.rs b/crates/pf-capture/src/windows/idd_push/open.rs index 7cdda75d..28df97fe 100644 --- a/crates/pf-capture/src/windows/idd_push/open.rs +++ b/crates/pf-capture/src/windows/idd_push/open.rs @@ -610,6 +610,7 @@ impl IddPushCapturer { client_10bit, display_hdr, hdr_pin_warned: false, + hdr_pin_failures: 0, want_444, pyrowave, pyro_fence: None, @@ -635,7 +636,9 @@ impl IddPushCapturer { stall_watch: StallWatch::new(), offered_at_fresh: 0, max_hb_age_us: 0, - probes: super::probes::acquire(), + probes: pf_host_config::config() + .stall_probes + .then(super::probes::acquire), etw: super::dxgkrnl_etw::acquire(), out_ring: Vec::new(), out_idx: 0, diff --git a/crates/pf-host-config/src/lib.rs b/crates/pf-host-config/src/lib.rs index 471d66e9..11690892 100644 --- a/crates/pf-host-config/src/lib.rs +++ b/crates/pf-host-config/src/lib.rs @@ -120,6 +120,14 @@ pub struct HostConfig { /// backend (the legacy SudoVDA backend was removed), so this is currently informational — kept for the /// shipped `host.env` and as a forward seam if a second backend is ever added. pub vdisplay: Option, + /// `PUNKTFUNK_STALL_PROBES` — run the Windows IDD-push capture's micro-probe engine (per-GPU + /// fence probes, DWM tick/flush watchdogs, scanline + CPU sentinels — `idd_push/probes.rs`), + /// the corroborating evidence legs on every stall report. Default ON while the + /// interval-stutter field program runs; explicit-off grammar for perf-sensitive boxes — the + /// engine costs standing threads (a blocking `DwmFlush` waiter, ~10 Hz fence copies per GPU, + /// a 5 ms-cadence CPU sentinel). Off, stall lines still carry the driver telemetry + the ETW + /// present/queue discriminator (cheap, session-filtered); only the probe legs read absent. + pub stall_probes: bool, /// `PUNKTFUNK_GAMESCOPE_STEAM` — force the bare headless gamescope spawn into its Steam /// integration mode (`--steam`) for EVERY launch. A Steam title auto-enables `--steam` on its /// own regardless of this knob; it exists to force it on for non-Steam launches too. Managed @@ -235,6 +243,8 @@ impl HostConfig { // per-session switch; see the field doc). chacha20: env_on("PUNKTFUNK_CHACHA20").unwrap_or(true), perf: flag("PUNKTFUNK_PERF"), + // Default ON while the interval-stutter field program runs (see the field doc). + stall_probes: env_on("PUNKTFUNK_STALL_PROBES").unwrap_or(true), video_source: val("PUNKTFUNK_VIDEO_SOURCE"), // Trimmed + emptied-to-None: `PUNKTFUNK_CAPTURE_MONITOR=` in a host.env means "not // set", not "match the monitor named empty string".