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 = {};