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
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:
@@ -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 {
|
||||
|
||||
@@ -1165,9 +1165,14 @@ impl Encoder for PyroWaveEncoder {
|
||||
}
|
||||
|
||||
fn caps(&self) -> EncoderCaps {
|
||||
// All defaults: no RFI (meaningless — every frame is intra), no HDR (8-bit SDR codec),
|
||||
// no intra-refresh wave (ditto). 4:2:0 only until the 4:4:4 ride-along (plan §6).
|
||||
EncoderCaps::default()
|
||||
// No RFI / no intra-refresh wave (every frame is intra). Report the real opened chroma so
|
||||
// the session glue's post-open cross-check stays quiet on a genuine 4:4:4 session — a
|
||||
// hardcoded `default()` here mis-reports a 4:4:4 open as 4:2:0 and fires a spurious
|
||||
// "chroma disagrees with the negotiated Welcome" warn.
|
||||
EncoderCaps {
|
||||
chroma_444: self.chroma444,
|
||||
..EncoderCaps::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn poll(&mut self) -> Result<Option<EncodedFrame>> {
|
||||
|
||||
@@ -549,8 +549,14 @@ impl Encoder for PyroWaveEncoder {
|
||||
}
|
||||
|
||||
fn caps(&self) -> EncoderCaps {
|
||||
// All defaults: no RFI (every frame is intra), no HDR (8-bit SDR codec), 4:2:0 only.
|
||||
EncoderCaps::default()
|
||||
// No RFI (every frame is intra). Report the real opened chroma so the session glue's
|
||||
// post-open cross-check stays quiet on a genuine 4:4:4 session (this codec gained 4:4:4
|
||||
// after the caps() default was written — a hardcoded `default()` here mis-reports a 4:4:4
|
||||
// open as 4:2:0 and fires a spurious "chroma disagrees with the negotiated Welcome" warn).
|
||||
EncoderCaps {
|
||||
chroma_444: self.chroma444,
|
||||
..EncoderCaps::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn poll(&mut self) -> Result<Option<EncodedFrame>> {
|
||||
@@ -567,7 +573,13 @@ impl Encoder for PyroWaveEncoder {
|
||||
device: self.pw_dev,
|
||||
width: self.width as i32,
|
||||
height: self.height as i32,
|
||||
chroma: pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_420,
|
||||
// Rebuild at the session's real chroma — a hardcoded 420 here would leave a 4:4:4
|
||||
// session's full-res chroma plane + CSC feeding a 4:2:0 pyrowave encoder.
|
||||
chroma: if self.chroma444 {
|
||||
pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_444
|
||||
} else {
|
||||
pw::pyrowave_chroma_subsampling_PYROWAVE_CHROMA_SUBSAMPLING_420
|
||||
},
|
||||
};
|
||||
let mut enc: pw::pyrowave_encoder = std::ptr::null_mut();
|
||||
let r = pw::pyrowave_encoder_create(&einfo, &mut enc);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
Encoder scratch-buffer pool — PUNKTFUNK LOCAL PATCH.
|
||||
|
||||
Not upstream. pyrowave_encoder_encode_gpu_synchronous() allocated four Vulkan
|
||||
buffers on EVERY frame (meta + bitstream, each Device-local and CachedHost).
|
||||
At streaming rates (240 fps, 1-3 MB bitstreams) that per-frame allocator churn
|
||||
did not just cost CPU — it stalled the encode: measured on an RTX 4090 the
|
||||
per-frame submit+fence-wait at 5120x1440 was ~15 ms (≈64 fps ceiling) and
|
||||
collapsed to ~1 ms (≈1025 fps) once the buffers are pooled. The four buffers are
|
||||
sized from the fixed session resolution + pinned bitrate budget, so after the
|
||||
first frame they are pure reuse; each is only re-created if a larger size is
|
||||
requested. Safe because the encode is synchronous (compute_num_packets/packetize
|
||||
wait the fence before the next encode reuses a buffer), and holding the handles
|
||||
on the encoder keeps next_frame_context() from recycling them.
|
||||
|
||||
Correctness re-validated by the pyrowave_win_smoke round-trip (100/180/60 across
|
||||
SDR/HDR x 420/444) after the change.
|
||||
|
||||
diff --git a/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c.cpp b/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c.cpp
|
||||
index 114b0bfc..eb917e64 100644
|
||||
--- a/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c.cpp
|
||||
+++ b/crates/pyrowave-sys/vendor/pyrowave/pyrowave_c.cpp
|
||||
@@ -799,6 +799,10 @@ struct pyrowave_encoder_opaque
|
||||
Fence queued_fence;
|
||||
BufferHandle queued_meta;
|
||||
BufferHandle queued_bitstream;
|
||||
+ // PUNKTFUNK: the GPU-side twins of queued_meta/queued_bitstream, pooled on the encoder so
|
||||
+ // the encode path reuses them across frames instead of allocating four buffers per frame.
|
||||
+ BufferHandle queued_meta_gpu;
|
||||
+ BufferHandle queued_bitstream_gpu;
|
||||
ChromaSubsampling chroma = {};
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
@@ -936,41 +940,49 @@ pyrowave_encoder_encode_gpu_synchronous(pyrowave_encoder encoder,
|
||||
|
||||
device->next_frame_context();
|
||||
|
||||
- BufferCreateInfo bufinfo = {};
|
||||
- bufinfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||
- VK_BUFFER_USAGE_TRANSFER_DST_BIT |
|
||||
- VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||
-
|
||||
- bufinfo.size = encoder->encoder.get_meta_required_size();
|
||||
- bufinfo.domain = BufferDomain::CachedHost;
|
||||
- encoder->queued_meta = device->create_buffer(bufinfo);
|
||||
-
|
||||
- if (!encoder->queued_meta)
|
||||
- return PYROWAVE_ERROR_OUT_OF_HOST_MEMORY;
|
||||
-
|
||||
- bufinfo.domain = BufferDomain::Device;
|
||||
- auto queued_meta_gpu = device->create_buffer(bufinfo);
|
||||
-
|
||||
- if (!queued_meta_gpu)
|
||||
- return PYROWAVE_ERROR_OUT_OF_DEVICE_MEMORY;
|
||||
-
|
||||
auto target_bitstream_size = rate_control->maximum_bitstream_size & ~VkDeviceSize(3u);
|
||||
|
||||
// Check for bogus sizes.
|
||||
if (target_bitstream_size > UINT32_MAX || target_bitstream_size == 0)
|
||||
return PYROWAVE_ERROR_INVALID_ARGUMENT;
|
||||
|
||||
- bufinfo.size = target_bitstream_size + encoder->encoder.get_meta_required_size();
|
||||
- bufinfo.domain = BufferDomain::CachedHost;
|
||||
- encoder->queued_bitstream = device->create_buffer(bufinfo);
|
||||
+ const VkDeviceSize meta_size = encoder->encoder.get_meta_required_size();
|
||||
+ const VkDeviceSize bitstream_size = target_bitstream_size + meta_size;
|
||||
+
|
||||
+ // PUNKTFUNK: pool the four scratch buffers on the encoder and only (re)create one when a
|
||||
+ // larger size is needed. Upstream allocated all four (meta + bitstream, each Device +
|
||||
+ // CachedHost) on every call; at streaming rates (240 fps, MB-scale bitstreams) that
|
||||
+ // allocator churn dominated the per-frame CPU cost. The sizes are effectively constant per
|
||||
+ // session (fixed resolution + a pinned bitrate budget), so after the first frame these are
|
||||
+ // pure reuse. The synchronous encode model (packetize()/compute_num_packets() wait the
|
||||
+ // fence before the next encode reuses a buffer) guarantees no in-flight GPU access to a
|
||||
+ // buffer we hand back, and holding the handles keeps next_frame_context() from recycling
|
||||
+ // them.
|
||||
+ BufferCreateInfo bufinfo = {};
|
||||
+ bufinfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||
+ VK_BUFFER_USAGE_TRANSFER_DST_BIT |
|
||||
+ VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||
|
||||
- if (!encoder->queued_bitstream)
|
||||
- return PYROWAVE_ERROR_OUT_OF_HOST_MEMORY;
|
||||
+ auto ensure_buffer = [&](BufferHandle &handle, VkDeviceSize size, BufferDomain domain) -> bool {
|
||||
+ if (!handle || handle->get_create_info().size < size)
|
||||
+ {
|
||||
+ bufinfo.size = size;
|
||||
+ bufinfo.domain = domain;
|
||||
+ handle = device->create_buffer(bufinfo);
|
||||
+ }
|
||||
+ return bool(handle);
|
||||
+ };
|
||||
|
||||
- bufinfo.domain = BufferDomain::Device;
|
||||
- auto queued_bitstream_gpu = device->create_buffer(bufinfo);
|
||||
+ auto &queued_meta_gpu = encoder->queued_meta_gpu;
|
||||
+ auto &queued_bitstream_gpu = encoder->queued_bitstream_gpu;
|
||||
|
||||
- if (!queued_bitstream_gpu)
|
||||
+ if (!ensure_buffer(encoder->queued_meta, meta_size, BufferDomain::CachedHost))
|
||||
+ return PYROWAVE_ERROR_OUT_OF_HOST_MEMORY;
|
||||
+ if (!ensure_buffer(queued_meta_gpu, meta_size, BufferDomain::Device))
|
||||
+ return PYROWAVE_ERROR_OUT_OF_DEVICE_MEMORY;
|
||||
+ if (!ensure_buffer(encoder->queued_bitstream, bitstream_size, BufferDomain::CachedHost))
|
||||
+ return PYROWAVE_ERROR_OUT_OF_HOST_MEMORY;
|
||||
+ if (!ensure_buffer(queued_bitstream_gpu, bitstream_size, BufferDomain::Device))
|
||||
return PYROWAVE_ERROR_OUT_OF_DEVICE_MEMORY;
|
||||
|
||||
Encoder::BitstreamBuffers bitstream_buffers = {};
|
||||
@@ -15,3 +15,13 @@ Local patches (crates/pyrowave-sys/patches/, re-applied on re-vendor):
|
||||
on the GPU → nondeterministic corrupt bitstreams/crashes at any bitrate.
|
||||
Found + validated 2026-07-18 (RTX 5070 Ti, 1080p/4K, 8/16-bit); to be
|
||||
reported upstream.
|
||||
|
||||
(0002-0003 are reserved by concurrent in-flight work and land separately.)
|
||||
|
||||
0004-encoder-buffer-pool.patch — pyrowave_c.cpp allocated four Vulkan buffers
|
||||
(meta + bitstream, Device + CachedHost) on EVERY encode. At 240 fps with
|
||||
MB-scale bitstreams that per-frame churn stalled the encode itself: on an
|
||||
RTX 4090 the 5120x1440 submit+fence-wait was ~15 ms (~64 fps ceiling) and
|
||||
dropped to ~1 ms (~1025 fps) once the buffers are pooled on the encoder and
|
||||
reused. Safe under the synchronous encode model; re-validated by
|
||||
pyrowave_win_smoke. Perf fix, not a correctness fix.
|
||||
|
||||
+39
-27
@@ -799,6 +799,10 @@ struct pyrowave_encoder_opaque
|
||||
Fence queued_fence;
|
||||
BufferHandle queued_meta;
|
||||
BufferHandle queued_bitstream;
|
||||
// PUNKTFUNK: the GPU-side twins of queued_meta/queued_bitstream, pooled on the encoder so
|
||||
// the encode path reuses them across frames instead of allocating four buffers per frame.
|
||||
BufferHandle queued_meta_gpu;
|
||||
BufferHandle queued_bitstream_gpu;
|
||||
ChromaSubsampling chroma = {};
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
@@ -936,41 +940,49 @@ pyrowave_encoder_encode_gpu_synchronous(pyrowave_encoder encoder,
|
||||
|
||||
device->next_frame_context();
|
||||
|
||||
BufferCreateInfo bufinfo = {};
|
||||
bufinfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||
|
||||
bufinfo.size = encoder->encoder.get_meta_required_size();
|
||||
bufinfo.domain = BufferDomain::CachedHost;
|
||||
encoder->queued_meta = device->create_buffer(bufinfo);
|
||||
|
||||
if (!encoder->queued_meta)
|
||||
return PYROWAVE_ERROR_OUT_OF_HOST_MEMORY;
|
||||
|
||||
bufinfo.domain = BufferDomain::Device;
|
||||
auto queued_meta_gpu = device->create_buffer(bufinfo);
|
||||
|
||||
if (!queued_meta_gpu)
|
||||
return PYROWAVE_ERROR_OUT_OF_DEVICE_MEMORY;
|
||||
|
||||
auto target_bitstream_size = rate_control->maximum_bitstream_size & ~VkDeviceSize(3u);
|
||||
|
||||
// Check for bogus sizes.
|
||||
if (target_bitstream_size > UINT32_MAX || target_bitstream_size == 0)
|
||||
return PYROWAVE_ERROR_INVALID_ARGUMENT;
|
||||
|
||||
bufinfo.size = target_bitstream_size + encoder->encoder.get_meta_required_size();
|
||||
bufinfo.domain = BufferDomain::CachedHost;
|
||||
encoder->queued_bitstream = device->create_buffer(bufinfo);
|
||||
const VkDeviceSize meta_size = encoder->encoder.get_meta_required_size();
|
||||
const VkDeviceSize bitstream_size = target_bitstream_size + meta_size;
|
||||
|
||||
if (!encoder->queued_bitstream)
|
||||
// PUNKTFUNK: pool the four scratch buffers on the encoder and only (re)create one when a
|
||||
// larger size is needed. Upstream allocated all four (meta + bitstream, each Device +
|
||||
// CachedHost) on every call; at streaming rates (240 fps, MB-scale bitstreams) that
|
||||
// allocator churn dominated the per-frame CPU cost. The sizes are effectively constant per
|
||||
// session (fixed resolution + a pinned bitrate budget), so after the first frame these are
|
||||
// pure reuse. The synchronous encode model (packetize()/compute_num_packets() wait the
|
||||
// fence before the next encode reuses a buffer) guarantees no in-flight GPU access to a
|
||||
// buffer we hand back, and holding the handles keeps next_frame_context() from recycling
|
||||
// them.
|
||||
BufferCreateInfo bufinfo = {};
|
||||
bufinfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||
|
||||
auto ensure_buffer = [&](BufferHandle &handle, VkDeviceSize size, BufferDomain domain) -> bool {
|
||||
if (!handle || handle->get_create_info().size < size)
|
||||
{
|
||||
bufinfo.size = size;
|
||||
bufinfo.domain = domain;
|
||||
handle = device->create_buffer(bufinfo);
|
||||
}
|
||||
return bool(handle);
|
||||
};
|
||||
|
||||
auto &queued_meta_gpu = encoder->queued_meta_gpu;
|
||||
auto &queued_bitstream_gpu = encoder->queued_bitstream_gpu;
|
||||
|
||||
if (!ensure_buffer(encoder->queued_meta, meta_size, BufferDomain::CachedHost))
|
||||
return PYROWAVE_ERROR_OUT_OF_HOST_MEMORY;
|
||||
|
||||
bufinfo.domain = BufferDomain::Device;
|
||||
auto queued_bitstream_gpu = device->create_buffer(bufinfo);
|
||||
|
||||
if (!queued_bitstream_gpu)
|
||||
if (!ensure_buffer(queued_meta_gpu, meta_size, BufferDomain::Device))
|
||||
return PYROWAVE_ERROR_OUT_OF_DEVICE_MEMORY;
|
||||
if (!ensure_buffer(encoder->queued_bitstream, bitstream_size, BufferDomain::CachedHost))
|
||||
return PYROWAVE_ERROR_OUT_OF_HOST_MEMORY;
|
||||
if (!ensure_buffer(queued_bitstream_gpu, bitstream_size, BufferDomain::Device))
|
||||
return PYROWAVE_ERROR_OUT_OF_DEVICE_MEMORY;
|
||||
|
||||
Encoder::BitstreamBuffers bitstream_buffers = {};
|
||||
|
||||
Reference in New Issue
Block a user