style: rustfmt the recovery + resize changes (Windows CI fmt check)
ci / web (push) Successful in 59s
ci / docs-site (push) Successful in 1m1s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
decky / build-publish (push) Successful in 18s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 59s
apple / swift (push) Successful in 4m26s
ci / bench (push) Successful in 5m53s
ci / rust (push) Failing after 7m35s
windows-host / package (push) Failing after 8m56s
flatpak / build-publish (push) Failing after 8m12s
arch / build-publish (push) Successful in 11m41s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m54s
android / android (push) Successful in 13m28s
deb / build-publish (push) Successful in 13m42s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m16s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m42s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m33s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m0s
docker / deploy-docs (push) Successful in 20s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m48s
release / apple (push) Successful in 25m4s
apple / screenshots (push) Successful in 19m32s

The `cargo fmt --check` step on the x86_64-pc-windows-msvc job was
failing: the mid-stream loss-recovery and resize-overlay commits landed
with unformatted wraps across pf-presenter, pf-client-core, punktfunk-core,
pf-console-ui, and a few host files.

Applied `cargo fmt`, and hand-relocated two trailing comments in
session.rs (a decoded-frame note and the wrap-counter note) to their own
lines so rustfmt no longer column-aligns the following comment block to
a deep indent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 23:25:06 +02:00
parent 89aa6767f9
commit 2271f67202
11 changed files with 87 additions and 38 deletions
+1 -3
View File
@@ -177,9 +177,7 @@ mod session_main {
/// debounced resize→`Reconfigure` machinery on; the callback stores each resize-end's
/// logical window size (load-modify-save, like the console settings screen) so the
/// next launch opens at it.
pub(crate) fn match_window(
settings: &trust::Settings,
) -> Option<Box<dyn FnMut(u32, u32)>> {
pub(crate) fn match_window(settings: &trust::Settings) -> Option<Box<dyn FnMut(u32, u32)>> {
settings.match_window.then(|| {
Box::new(|w: u32, h: u32| {
let mut s = trust::Settings::load();
+14 -6
View File
@@ -466,17 +466,21 @@ fn pump(
.is_none_or(|t| now.duration_since(t) >= Duration::from_millis(100))
{
last_kf_req = Some(now);
let _ = connector
.request_rfi(exp, frame.frame_index.wrapping_sub(1));
let _ =
connector.request_rfi(exp, frame.frame_index.wrapping_sub(1));
}
tracing::trace!(gap, "frame gap — RFI recovery, holding last frame until re-anchor");
tracing::trace!(
gap,
"frame gap — RFI recovery, holding last frame until re-anchor"
);
}
}
None => next_expected_index = Some(frame.frame_index.wrapping_add(1)),
}
match decoder.decode(&frame.data) {
Ok(Some(image)) => {
no_output_streak = 0; // a decoded frame — the anchor holds
// A decoded frame — the anchor holds.
no_output_streak = 0;
// Host-signalled intra-refresh recovery mark: on an IDR-free intra-refresh
// stream this wave-boundary flag is the only clean point the client can honor
// (the decoder never flags the re-anchor — the coded frame stays `P`). A live
@@ -851,7 +855,10 @@ mod tests {
// The first wave boundary after a loss is only half-healed — one mark must hold the freeze.
assert_eq!(REANCHOR_MARKS_TO_LIFT, 2);
assert_eq!(lift_at(&[(false, true)]), None);
assert_eq!(lift_at(&[(false, false), (false, true), (false, false)]), None);
assert_eq!(
lift_at(&[(false, false), (false, true), (false, false)]),
None
);
}
#[test]
@@ -919,7 +926,8 @@ mod tests {
#[test]
fn the_index_counter_wraps_cleanly() {
// last frame = u32::MAX, so the next expected wraps to 0.
assert_eq!(index_gap(0, 0), None); // contiguous across the wrap
// Contiguous across the wrap.
assert_eq!(index_gap(0, 0), None);
// waiting on u32::MAX, frame 0 arrived → MAX was skipped.
assert_eq!(index_gap(u32::MAX, 0), Some(1));
assert_eq!(index_gap(u32::MAX, 2), Some(3));
+4 -1
View File
@@ -420,7 +420,10 @@ mod tests {
assert!(ctx.settings.match_window, "Native → Match window");
assert_eq!((ctx.settings.width, ctx.settings.height), (0, 0));
assert!(adjust(RowId::Resolution, 1, false, &mut ctx));
assert!(!ctx.settings.match_window, "explicit size clears the policy");
assert!(
!ctx.settings.match_window,
"explicit size clears the policy"
);
assert_eq!((ctx.settings.width, ctx.settings.height), (1280, 720));
// Stepping back from an explicit size returns to Match window, then Native.
assert!(adjust(RowId::Resolution, -1, false, &mut ctx));
+6 -1
View File
@@ -472,7 +472,12 @@ impl SkiaOverlay {
self.resizing_since = None;
return None;
}
Some(self.resizing_since.get_or_insert_with(Instant::now).elapsed().as_secs_f64())
Some(
self.resizing_since
.get_or_insert_with(Instant::now)
.elapsed()
.as_secs_f64(),
)
}
/// Make `slots[i]` a render target of exactly `width`×`height` (rebuilt on resize).
+34 -12
View File
@@ -1149,13 +1149,12 @@ fn resize_tick(
persist(lw, lh);
let Some((w, h)) = target else { return };
tracing::info!(w, h, "window resized — requesting mode switch");
if c
.request_mode(Mode {
width: w,
height: h,
refresh_hz: m.refresh_hz,
})
.is_err()
if c.request_mode(Mode {
width: w,
height: h,
refresh_hz: m.refresh_hz,
})
.is_err()
{
tracing::warn!("mode-switch request dropped — control channel closed");
}
@@ -1269,7 +1268,10 @@ impl ResizeIndicator {
/// Timeout safety net: stop showing once [`TIMEOUT`](Self::TIMEOUT) has elapsed with no
/// matching frame (a rejected or host-capped switch never delivers the exact target).
fn tick(&mut self, now: Instant) {
if self.since.is_some_and(|s| now.duration_since(s) >= Self::TIMEOUT) {
if self
.since
.is_some_and(|s| now.duration_since(s) >= Self::TIMEOUT)
{
self.target = None;
self.since = None;
}
@@ -1450,7 +1452,14 @@ mod tests {
// Equal to the streamed mode → settle (persist) but no request.
let mut pending = Some(t0);
assert_eq!(
resize_decision(t0 + ms(400), &mut pending, None, None, (1280, 720), (1280, 720)),
resize_decision(
t0 + ms(400),
&mut pending,
None,
None,
(1280, 720),
(1280, 720)
),
ResizeAction::Settled(None)
);
@@ -1472,7 +1481,14 @@ mod tests {
// Tiny windows clamp to the host's floor.
let mut pending = Some(t0);
assert_eq!(
resize_decision(t0 + ms(400), &mut pending, None, None, (1280, 720), (100, 80)),
resize_decision(
t0 + ms(400),
&mut pending,
None,
None,
(1280, 720),
(100, 80)
),
ResizeAction::Settled(Some((320, 200)))
);
}
@@ -1522,7 +1538,10 @@ mod tests {
let near = t0 + ResizeIndicator::TIMEOUT - ms(1);
ind.steering(1200, 700, near); // new target → timeout re-armed from `near`
ind.tick(t0 + ResizeIndicator::TIMEOUT + ms(1)); // past A's window, within B's
assert!(ind.active(), "retarget re-armed the timeout — no mid-drag flicker");
assert!(
ind.active(),
"retarget re-armed the timeout — no mid-drag flicker"
);
// Re-steering the SAME size does NOT re-arm (so a repeated identical request can't
// hold the scrim open forever).
@@ -1530,7 +1549,10 @@ mod tests {
ind.steering(1000, 600, t0);
ind.steering(1000, 600, t0 + ms(500)); // same target, later — `since` unchanged
ind.tick(t0 + ResizeIndicator::TIMEOUT);
assert!(!ind.active(), "an unchanged target keeps the original timeout");
assert!(
!ind.active(),
"an unchanged target keeps the original timeout"
);
}
fn sample() -> (Stats, PresentedWindow) {
+14 -8
View File
@@ -19,8 +19,8 @@ use crate::packet::FLAG_PROBE;
use crate::quic::{
accept_resync, endpoint, io, wall_clock_ns, window_loss_ppm, BitrateChanged, ClockEcho,
ClockResync, ColorInfo, HdrMeta, Hello, HidOutput, LossReport, ProbeRequest, ProbeResult,
Reconfigure, Reconfigured, RequestKeyframe, RfiRequest, ResyncStep, RichInput, SetBitrate, Start,
Welcome,
Reconfigure, Reconfigured, RequestKeyframe, ResyncStep, RfiRequest, RichInput, SetBitrate,
Start, Welcome,
};
use crate::session::{Frame, Session};
use crate::transport::UdpTransport;
@@ -395,7 +395,9 @@ impl RfiRecovery {
// Forward gap: [exp, frame_index-1] lost. Advance past this frame so the same
// gap isn't re-detected, then fire a throttled RFI for the lost range.
self.next_expected = Some(frame_index.wrapping_add(1));
let send = self.last_req.is_none_or(|t| now.duration_since(t) >= RFI_THROTTLE);
let send = self
.last_req
.is_none_or(|t| now.duration_since(t) >= RFI_THROTTLE);
if send {
self.last_req = Some(now);
}
@@ -968,7 +970,11 @@ impl NativeClient {
/// signal and shares one throttle across RFI + keyframe requests.)
pub fn note_frame_index(&self, frame_index: u32) -> bool {
// Decide (and update state) under the lock; fire the request after releasing it.
let (gap, rfi_range) = self.rfi.lock().unwrap().observe(frame_index, Instant::now());
let (gap, rfi_range) = self
.rfi
.lock()
.unwrap()
.observe(frame_index, Instant::now());
if let Some((first, last)) = rfi_range {
let _ = self.request_rfi(first, last);
}
@@ -2097,7 +2103,7 @@ mod rfi_recovery_tests {
let mut r = RfiRecovery::default();
let t = base();
r.observe(100, t); // expecting 101 next
// 101..=104 were lost; 105 arrived. The RFI must name exactly the missing span.
// 101..=104 were lost; 105 arrived. The RFI must name exactly the missing span.
assert_eq!(r.observe(105, t), (true, Some((101, 104))));
// The expectation advances past the delivered frame so the same gap can't re-fire.
assert_eq!(r.next_expected, Some(106));
@@ -2134,8 +2140,8 @@ mod rfi_recovery_tests {
let t = base();
r.observe(100, t);
r.observe(105, t); // expecting 106 next
// A reordered late arrival (103, well behind 106) is neither a gap nor a request, and it
// must not rewind the expectation — otherwise the next in-order frame would false-gap.
// A reordered late arrival (103, well behind 106) is neither a gap nor a request, and it
// must not rewind the expectation — otherwise the next in-order frame would false-gap.
assert_eq!(r.observe(103, t), (false, None));
assert_eq!(r.next_expected, Some(106));
}
@@ -2156,7 +2162,7 @@ mod rfi_recovery_tests {
let mut r = RfiRecovery::default();
let t = base();
r.observe(u32::MAX - 1, t); // expecting u32::MAX next
// u32::MAX was lost and 1 arrived → the lost span wraps: [u32::MAX, 0].
// u32::MAX was lost and 1 arrived → the lost span wraps: [u32::MAX, 0].
assert_eq!(r.observe(1, t), (true, Some((u32::MAX, 0))));
assert_eq!(r.next_expected, Some(2));
}
+2 -2
View File
@@ -466,8 +466,8 @@ pub mod dxgi;
#[cfg(target_os = "windows")]
#[path = "capture/windows/idd_push.rs"]
pub mod idd_push;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "windows")]
#[path = "capture/windows/synthetic_nv12.rs"]
pub mod synthetic_nv12;
#[cfg(target_os = "linux")]
mod linux;
@@ -50,7 +50,8 @@ impl SyntheticNv12Capturer {
// SAFETY: a self-contained builder owning every handle it creates; each COM call is checked
// and the returned owners drop with their wrappers.
unsafe {
let adapter = resolve_render_adapter().context("resolve render adapter for NV12 source")?;
let adapter =
resolve_render_adapter().context("resolve render adapter for NV12 source")?;
let (device, context) = make_device(&adapter).context("create D3D11 device")?;
let default_tex = create_nv12(
&device,
@@ -2112,7 +2112,9 @@ impl Encoder for AmfEncoder {
}
}
}
inner.pending.push_back((captured.pts_ns, forced, recovery_anchor));
inner
.pending
.push_back((captured.pts_ns, forced, recovery_anchor));
Ok(())
}
+1 -1
View File
@@ -4767,7 +4767,7 @@ mod tests {
assert!(!mark_recovery_boundary(&mut pos, false, period)); // pos 1
assert!(!mark_recovery_boundary(&mut pos, false, period)); // pos 2
assert!(!mark_recovery_boundary(&mut pos, true, period)); // IDR → pos 0, no mark
// Now a fresh full period is needed, not just the 2 remaining frames.
// Now a fresh full period is needed, not just the 2 remaining frames.
assert!(!mark_recovery_boundary(&mut pos, false, period)); // pos 1
assert!(!mark_recovery_boundary(&mut pos, false, period)); // pos 2
assert!(!mark_recovery_boundary(&mut pos, false, period)); // pos 3
+6 -2
View File
@@ -72,14 +72,18 @@ pub fn run(opts: Options) -> Result<()> {
);
Box::new(
capture::synthetic_nv12::SyntheticNv12Capturer::new(
opts.width, opts.height, opts.fps,
opts.width,
opts.height,
opts.fps,
)
.context("open synthetic NV12 capturer")?,
)
}
#[cfg(not(target_os = "windows"))]
{
anyhow::bail!("--source synthetic-nv12 is Windows-only (native AMF / D3D11 encoders)");
anyhow::bail!(
"--source synthetic-nv12 is Windows-only (native AMF / D3D11 encoders)"
);
}
}
Source::Portal => {