From 9fe9c451dcf93e68584f97375942cee7dac30d40 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 18 Jul 2026 19:09:23 +0200 Subject: [PATCH] =?UTF-8?q?perf(pyrowave):=20pool=20encoder=20scratch=20bu?= =?UTF-8?q?ffers=20+=20fix=20client=20parser=20O(n=C2=B2)=20=E2=80=94=20li?= =?UTF-8?q?ft=20the=202.5=20Gbps=20wall?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Video/MetalWaveletDecoder.swift | 19 ++- crates/pf-encode/src/enc/linux/pyrowave.rs | 11 +- crates/pf-encode/src/enc/windows/pyrowave.rs | 18 ++- crates/punktfunk-host/src/native.rs | 70 +++++++++++- .../patches/0004-encoder-buffer-pool.patch | 108 ++++++++++++++++++ .../vendor/pyrowave/PUNKTFUNK-VENDOR.txt | 10 ++ .../vendor/pyrowave/pyrowave_c.cpp | 66 ++++++----- 7 files changed, 264 insertions(+), 38 deletions(-) create mode 100644 crates/pyrowave-sys/patches/0004-encoder-buffer-pool.patch diff --git a/clients/apple/Sources/PunktfunkKit/Video/MetalWaveletDecoder.swift b/clients/apple/Sources/PunktfunkKit/Video/MetalWaveletDecoder.swift index 3765fca6..7a3e3b98 100644 --- a/clients/apple/Sources/PunktfunkKit/Video/MetalWaveletDecoder.swift +++ b/clients/apple/Sources/PunktfunkKit/Video/MetalWaveletDecoder.swift @@ -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.. 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> { diff --git a/crates/pf-encode/src/enc/windows/pyrowave.rs b/crates/pf-encode/src/enc/windows/pyrowave.rs index 47581919..1ed1062e 100644 --- a/crates/pf-encode/src/enc/windows/pyrowave.rs +++ b/crates/pf-encode/src/enc/windows/pyrowave.rs @@ -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> { @@ -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); diff --git a/crates/punktfunk-host/src/native.rs b/crates/punktfunk-host/src/native.rs index 8438e5e6..74db285c 100644 --- a/crates/punktfunk-host/src/native.rs +++ b/crates/punktfunk-host/src/native.rs @@ -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 { + std::env::var("PUNKTFUNK_PYROWAVE_MAX_MBPS") + .ok() + .and_then(|s| s.trim().parse::().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. diff --git a/crates/pyrowave-sys/patches/0004-encoder-buffer-pool.patch b/crates/pyrowave-sys/patches/0004-encoder-buffer-pool.patch new file mode 100644 index 00000000..631e98be --- /dev/null +++ b/crates/pyrowave-sys/patches/0004-encoder-buffer-pool.patch @@ -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 = {}; diff --git a/crates/pyrowave-sys/vendor/pyrowave/PUNKTFUNK-VENDOR.txt b/crates/pyrowave-sys/vendor/pyrowave/PUNKTFUNK-VENDOR.txt index 14987099..1077f8c8 100644 --- a/crates/pyrowave-sys/vendor/pyrowave/PUNKTFUNK-VENDOR.txt +++ b/crates/pyrowave-sys/vendor/pyrowave/PUNKTFUNK-VENDOR.txt @@ -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. 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; - 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 = {};