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
@@ -138,6 +138,12 @@ enum WaveletBitstream {
/// decoding upstream's `decoded_blocks > total/2` partial rule).
static func parse(au: Data, chunkAligned: Bool, windowSize: Int) -> ParsedWaveletFrame? {
var state = ParseState()
// Reserve the coefficient buffer ONCE, up front. Every packet's payload is a slice of the
// AU, so `au.count / 4` words is a tight upper bound reserving it here lets the per-packet
// appends stay amortized O(1). (Reserving per packet forces Swift to allocate the exact new
// size each time, turning the walk O(n²) invisible on the tiny golden fixtures, but ~5 ms
// per 1.4 MB frame on a real 5120x1440 stream.)
state.payload.reserveCapacity(au.count / 4)
let ok = au.withUnsafeBytes { (raw: UnsafeRawBufferPointer) -> Bool in
guard let base = raw.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return false
@@ -244,7 +250,6 @@ enum WaveletBitstream {
let l = WaveletLayout(width: w, height: h, chroma444: chroma444)
layout = l
offsets = [UInt32](repeating: .max, count: l.blockCount32)
payload.reserveCapacity(64 * 1024 / 4)
totalBlocks = Int(word1 & 0xff_ffff)
bt2020 = (word1 >> 29) & 1 != 0
// transfer_function bit: PQ an HDR session (16-bit studio-code
@@ -266,9 +271,15 @@ enum WaveletBitstream {
if offsets[blockIndex] == .max {
offsets[blockIndex] = UInt32(payload.count)
decodedBlocks += 1
payload.reserveCapacity(payload.count + payloadWords)
for w in 0..<payloadWords {
payload.append(loadWord(base, pos + w * 4))
// Bulk-copy the packet's coefficient words in one memcpy rather than
// word-by-word. All Apple platforms are little-endian, so the wire's LE
// u32s land in the [UInt32] buffer verbatim; memcpy has no alignment
// requirement, so a non-word-aligned `base + pos` is fine. `reserveCapacity`
// up in `parse` keeps the grow amortized O(1).
let dstWord = payload.count
payload.append(contentsOf: repeatElement(0, count: payloadWords))
payload.withUnsafeMutableBytes { dst in
_ = memcpy(dst.baseAddress! + dstWord * 4, base + pos, payloadWords * 4)
}
}
} else if layout != nil {