perf(pyrowave): pool encoder scratch buffers + fix client parser O(n²) — lift the 2.5 Gbps wall
ci / web (push) Successful in 47s
ci / docs-site (push) Successful in 1m7s
apple / swift (push) Successful in 1m17s
decky / build-publish (push) Successful in 25s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
ci / bench (push) Successful in 5m51s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 4m51s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
release / apple (push) Successful in 9m11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6m6s
deb / build-publish (push) Successful in 12m55s
android / android (push) Successful in 13m15s
docker / deploy-docs (push) Successful in 25s
deb / build-publish-host (push) Successful in 13m7s
arch / build-publish (push) Successful in 16m4s
windows-host / package (push) Successful in 16m19s
apple / screenshots (push) Successful in 6m34s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m47s
ci / rust (push) Successful in 24m17s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m31s

Real-world PyroWave streaming maxed ~2.5 Gbps with sagging fps while raw
transport does 4.8. Root-caused to serial per-frame paths at BOTH ends
(the transport was never the limit); this fixes the two dominant ones.

Host (vendored shim, patch 0004): pyrowave_encoder_encode_gpu_synchronous
allocated four Vulkan buffers (meta + bitstream, Device + CachedHost) on
EVERY frame. At 240 fps with MB-scale bitstreams that per-frame allocator
churn stalled the encode itself. Pool them on the encoder and reuse across
frames (recreate only on a size grow); the sizes are session-fixed, so it
is pure reuse after frame 1. On an RTX 4090 the 5120x1440 submit+fence-wait
drops ~15 ms -> ~1 ms, i.e. the host serial ceiling goes 64 -> 1025 fps
(444+HDR 44 -> 614). Safe under the synchronous encode model; re-validated
by pyrowave_win_smoke (Windows) and pyrowave_smoke/_444 (Linux). Applies to
both host encoder paths (they share the shim).

Client (Apple Metal decoder): WaveletBitstream.parse reserved the payload
buffer per packet (reserveCapacity(count + words), an exact realloc each of
~3000 packets/frame => O(n²)) and copied word-by-word. Reserve once up
front and memcpy each packet's coefficients in one shot (all Apple
platforms are little-endian, so the wire's LE u32s land verbatim; memcpy is
alignment-free). 5.44 ms -> 0.055 ms per 1.44 MB frame (25x); byte-identical
(parser unit tests + golden-frame PSNR unchanged).

Also:
- native.rs: PUNKTFUNK_PYROWAVE_MAX_MBPS caps PyroWave's open-loop Automatic
  bitrate pin for hosts on a constrained link (unset => no cap; an explicit
  client rate bypasses it). The pin is all-intra + ABR-off, so at a high
  pixel rate it can outrun the fabric (4:4:4+HDR 5120x1440@240 pins ~5.3
  Gbps, over a 5 GbE link) and the overshoot just becomes loss.
- pf-encode caps(): report the real opened chroma instead of a hardcoded
  4:2:0 default, so a genuine 4:4:4 session no longer trips the spurious
  "encoder chroma disagrees with the negotiated Welcome" warn. Also fix a
  latent Windows reset() that rebuilt at 4:2:0 for a 4:4:4 session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-18 19:09:23 +02:00
parent b89dbfa979
commit 9fe9c451dc
7 changed files with 264 additions and 38 deletions
+69 -1
View File
@@ -535,13 +535,41 @@ fn resolve_bitrate_kbps_for(
if bit_depth >= 10 {
bps = bps * 115 / 100;
}
return u32::try_from(bps / 1000)
let pin = u32::try_from(bps / 1000)
.unwrap_or(MAX_BITRATE_KBPS)
.clamp(MIN_BITRATE_KBPS, MAX_BITRATE_KBPS);
// Operator link ceiling. PyroWave's Automatic pin is open-loop (all-intra, so ABR and the
// capacity probe are off) — at a high pixel rate it can outrun the physical link (e.g.
// 4:4:4 + HDR at 5120x1440@240 pins ~5.3 Gbps, over a 5 GbE link), and the overshoot just
// becomes packet loss / partial frames. `PUNKTFUNK_PYROWAVE_MAX_MBPS` lets a host on a
// constrained link cap the pin to what the fabric carries; unset ⇒ no cap (unchanged).
if let Some(ceiling) = pyrowave_auto_pin_ceiling_kbps() {
if pin > ceiling {
tracing::warn!(
pin_kbps = pin,
ceiling_kbps = ceiling,
"PyroWave Automatic bitrate pin exceeds PUNKTFUNK_PYROWAVE_MAX_MBPS — capping \
to the link ceiling (set an explicit client bitrate to choose your own)"
);
return ceiling.max(MIN_BITRATE_KBPS);
}
}
return pin;
}
resolve_bitrate_kbps(requested)
}
/// Operator ceiling for PyroWave's open-loop Automatic bitrate pin: `PUNKTFUNK_PYROWAVE_MAX_MBPS`
/// (megabits/s) → kbps, or `None` when unset/zero/invalid (no cap — the raw bpp pin stands).
/// Only consulted for `requested == 0` PyroWave sessions; an explicit client bitrate bypasses it.
fn pyrowave_auto_pin_ceiling_kbps() -> Option<u32> {
std::env::var("PUNKTFUNK_PYROWAVE_MAX_MBPS")
.ok()
.and_then(|s| s.trim().parse::<u32>().ok())
.filter(|&m| m > 0)
.map(|m| m.saturating_mul(1000))
}
/// Resolve the audio channel count the session will capture + encode from the client's request.
/// Normalizes to one of 2 (stereo) / 6 (5.1) / 8 (7.1); anything else (older client, garbage)
/// becomes stereo. Both backends can produce the requested count (PipeWire pads/upmixes positions,
@@ -1524,6 +1552,46 @@ mod tests {
);
}
#[test]
fn pyrowave_auto_pin_respects_operator_ceiling() {
use crate::encode::{ChromaFormat, Codec};
use punktfunk_core::config::Mode;
// 5120x1440@240 4:4:4 10-bit pins ~5.29 Gbps open-loop — above a 5 GbE link.
let mode = Mode {
width: 5120,
height: 1440,
refresh_hz: 240,
};
let uncapped =
resolve_bitrate_kbps_for(Codec::PyroWave, 0, &mode, ChromaFormat::Yuv444, 10);
assert!(
uncapped > 5_000_000,
"expected the open-loop pin, got {uncapped}"
);
// With the operator ceiling set, the Automatic pin is capped to the link rate...
std::env::set_var("PUNKTFUNK_PYROWAVE_MAX_MBPS", "4500");
assert_eq!(
resolve_bitrate_kbps_for(Codec::PyroWave, 0, &mode, ChromaFormat::Yuv444, 10),
4_500_000
);
// ...but a pin already under the ceiling is untouched (1080p60 4:2:0 ≈ 199 Mbps)...
let small = Mode {
width: 1920,
height: 1080,
refresh_hz: 60,
};
assert_eq!(
resolve_bitrate_kbps_for(Codec::PyroWave, 0, &small, ChromaFormat::Yuv420, 8),
1920 * 1080 * 60 * 16 / 10 / 1000
);
// ...and an explicit client rate bypasses the ceiling entirely.
assert_eq!(
resolve_bitrate_kbps_for(Codec::PyroWave, 6_000_000, &mode, ChromaFormat::Yuv444, 10),
6_000_000
);
std::env::remove_var("PUNKTFUNK_PYROWAVE_MAX_MBPS");
}
#[test]
fn adapt_fec_maps_loss_to_recovery_band() {
// A perfectly clean window (0 loss) lands on the floor.