feat(presenter,docs): PyroWave mid-stream resize — HUD follows any mode switch; docs
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 54s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 17s
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 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 44s
ci / bench (push) Successful in 5m47s
flatpak / build-publish (push) Failing after 8m11s
docker / deploy-docs (push) Successful in 27s
arch / build-publish (push) Successful in 10m58s
deb / build-publish (push) Successful in 12m26s
android / android (push) Successful in 16m59s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m16s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m59s
ci / rust (push) Successful in 24m26s
windows-host / package (push) Failing after 12m3s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m50s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m16s
apple / swift (push) Successful in 4m57s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m14s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m3s
release / apple (push) Successful in 25m6s
apple / screenshots (push) Has been cancelled

- pf-presenter: the HUD/title mode line lived inside the match-window (D2)
  gate, so an accepted switch from any OTHER trigger (the
  PUNKTFUNK_DEBUG_RECONFIGURE lever, a host-side corrective rollback) left the
  label stale. Hoisted into its own per-iteration tick that runs whenever a
  stream is up.
- docs: pyrowave.md — the Automatic bitrate pin now follows a mid-stream
  resize; drop the "resolution changes rebuild the stream" limitation.

Completes the resize-rebuild work whose core landed in 9127c346
(video_pyrowave.rs sequence-header dims sniff + in-place decoder/plane-ring
rebuild with retired-ring lifetime handling, host per-mode ~1.6 bpp re-pin,
128px floor, debug reconfigure lever). On-glass validated on .21
(RTX 5070 Ti, Mutter virtual display): 1080p->720p and 1080p->1440p
mid-stream switches, lossless AND under 2% netem loss — decoder rebuilt in
place, 60 fps sustained (partials during loss), pinned rate re-resolved
199065->88473 / ->353894 kbps, HUD flips to the new mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 12:00:41 +02:00
parent 9127c3465f
commit a70811043e
2 changed files with 27 additions and 16 deletions
+25 -15
View File
@@ -880,10 +880,16 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
} }
} }
// --- Match-window (D2): debounced mode-follow + HUD/title refresh on a switch ---- // HUD/title follow the live mode slot on ANY accepted switch — also when the
// match-window follower is off (a switch can come from elsewhere, e.g. the
// PUNKTFUNK_DEBUG_RECONFIGURE lever, or a host-side corrective rollback).
if let Some(st) = stream.as_mut() {
hud_mode_tick(st, &mut window, &opts.window_title);
}
// --- Match-window (D2): debounced mode-follow ----
if let Some(persist) = opts.match_window.as_mut() { if let Some(persist) = opts.match_window.as_mut() {
if let Some(st) = stream.as_mut() { if let Some(st) = stream.as_mut() {
resize_tick(st, &mut window, &opts.window_title, persist.as_mut()); resize_tick(st, &mut window, persist.as_mut());
} }
} }
// Resize overlay timeout: a switch the host rejected/capped never delivers the exact // Resize overlay timeout: a switch the host rejected/capped never delivers the exact
@@ -1201,10 +1207,23 @@ fn apply_match_window(params: &mut SessionParams, window: &sdl3::video::Window)
); );
} }
/// Match-window (D2) per-iteration tick: refresh the HUD line + window title when the /// Per-iteration HUD/title refresh: follow the live mode slot (updated by any accepted
/// live mode moves (an accepted switch's ack, or a corrective rollback), then fire the /// ack — a follower request, another trigger, or a host-side corrective rollback).
/// debounced `Reconfigure` once ~400 ms pass with no further resize events. The shared fn hud_mode_tick(st: &mut StreamState, window: &mut sdl3::video::Window, title_base: &str) {
/// trigger discipline: let Some(c) = &st.connector else {
return;
};
let m = c.mode();
if st.shown_mode.is_some_and(|prev| prev != m) {
st.mode_line = format!("{}×{}@{}", m.width, m.height, m.refresh_hz);
tracing::info!(mode = %st.mode_line, "stream mode switched");
let _ = window.set_title(&format!("{title_base} · {}", st.mode_line));
}
st.shown_mode = Some(m);
}
/// Match-window (D2) per-iteration tick: fire the debounced `Reconfigure` once ~400 ms
/// pass with no further resize events. The shared trigger discipline:
/// * physical pixels, even-floored, clamped ≥ 320×200; the current refresh is kept; /// * physical pixels, even-floored, clamped ≥ 320×200; the current refresh is kept;
/// * ≥ 1 s between requests (the accept ack round-trips in milliseconds — it precedes /// * ≥ 1 s between requests (the accept ack round-trips in milliseconds — it precedes
/// the host's rebuild — so the spacing also keeps at most ~one request outstanding); /// the host's rebuild — so the spacing also keeps at most ~one request outstanding);
@@ -1214,21 +1233,12 @@ fn apply_match_window(params: &mut SessionParams, window: &sdl3::video::Window)
fn resize_tick( fn resize_tick(
st: &mut StreamState, st: &mut StreamState,
window: &mut sdl3::video::Window, window: &mut sdl3::video::Window,
title_base: &str,
persist: &mut dyn FnMut(u32, u32), persist: &mut dyn FnMut(u32, u32),
) { ) {
let Some(c) = &st.connector else { let Some(c) = &st.connector else {
return; // not connected yet — the pending stamp survives until we are return; // not connected yet — the pending stamp survives until we are
}; };
// HUD/title follow the live mode slot (updated by any accepted ack).
let m = c.mode(); let m = c.mode();
if st.shown_mode.is_some_and(|prev| prev != m) {
st.mode_line = format!("{}×{}@{}", m.width, m.height, m.refresh_hz);
tracing::info!(mode = %st.mode_line, "stream mode switched");
let _ = window.set_title(&format!("{title_base} · {}", st.mode_line));
}
st.shown_mode = Some(m);
match resize_decision( match resize_decision(
Instant::now(), Instant::now(),
&mut st.resize_pending, &mut st.resize_pending,
+2 -1
View File
@@ -54,6 +54,8 @@ setting.
your mode (≈200 Mbps at 1080p60). An explicit bitrate is honored if you set one, but the your mode (≈200 Mbps at 1080p60). An explicit bitrate is honored if you set one, but the
adaptive-bitrate controller stays off either way — this codec has no useful low-rate adaptive-bitrate controller stays off either way — this codec has no useful low-rate
regime, so under sustained loss the right move is switching back to HEVC, not degrading. regime, so under sustained loss the right move is switching back to HEVC, not degrading.
The pin follows the resolution: a mid-stream resize (e.g. Match window) re-pins the rate
for the new mode, so resizing a window down also cuts the bandwidth.
The stats overlay shows `pyrowave` as the decode path when the mode is active. The stats overlay shows `pyrowave` as the decode path when the mode is active.
@@ -62,4 +64,3 @@ The stats overlay shows `pyrowave` as the decode path when the mode is active.
- Linux host + Linux client (including docked Deck) today; the Windows host and the Apple - Linux host + Linux client (including docked Deck) today; the Windows host and the Apple
native port are tracked on the [roadmap](/docs/roadmap). native port are tracked on the [roadmap](/docs/roadmap).
- 8-bit SDR, 4:2:0 only. - 8-bit SDR, 4:2:0 only.
- Mid-stream resolution changes rebuild the stream rather than switching seamlessly.