atagen

monolith

public
Star0
Watch0

Commit 7af1a807d966

tarball

atagen <boss@atagen.co>2026-10-19 10:36 UTC

1 file changed+36-18
render-vk: flush queued uploads at the pool size and keep readback staging pinned

Uploads were batched up to 64 MiB, twice what StagingPool keeps, so each flush freed half of it and brought back the NVIDIA host-allocation stall the pool exists to avoid. read_back dropped its staging into the pool after a fence timeout while a hung GPU might still write it, so it now moves into the Pending entry. Shm commits also called the full reap, which walks four maps and locks the pool, where freeing finished fences is enough.
Modified file: crates/render-vk/src/renderer/mod.rs+36 additions−18 deletions
@@ -12,7 +12,7 @@ use crate::format::{BYTES_PER_PIXEL, SUPPORTED};
1212 use crate::frame::{Op, VulkanFrame};
1313 use crate::image::{ImageInner, VulkanImage, VulkanMapping, VulkanTarget, VulkanTexture};
1414 use crate::pipeline::Pipelines;
15-use crate::staging::{Staging, StagingPool};
15+use crate::staging::{POOL_BYTES, Staging, StagingPool};
1616 use crate::sync::{FenceInner, VkFence};
1717 use ash::vk;
1818 use gpu_allocator::MemoryLocation;
@@ -63,10 +63,6 @@ struct Recorded {
6363 layouts: LayoutLog,
6464 }
6565
66-/// Queued staging memory past which uploads are submitted without waiting
67-/// for a frame.
68-const UPLOAD_BATCH_BYTES: usize = 64 * 1024 * 1024;
69-
7066 pub struct VulkanRenderer {
7167 pending: Vec<Pending>,
7268 uploads: Vec<Upload>,
@@ -212,6 +208,17 @@ impl VulkanRenderer {
212208 /// Recycles finished work. A fence that cannot be queried stays
213209 /// pending and fails the call, since its work may never have finished.
214210 fn reap(&mut self) -> Result<(), Error> {
211+ let reaped = self.reap_fences();
212+ self.dmabuf_textures.retain(|k, _| !k.is_gone());
213+ self.dmabuf_targets.retain(|k, _| !k.is_gone());
214+ self.staged.retain(|_, (b, _)| b.is_alive());
215+ self.staging_pool.trim();
216+ reaped
217+ }
218+
219+ /// The part of `reap` that frees command buffers and staging, cheap enough
220+ /// to run for every shm commit.
221+ fn reap_fences(&mut self) -> Result<(), Error> {
215222 let spare = &mut self.spare_cmds;
216223 let mut failed = None;
217224 self.pending.retain(|p| match p.fence.status() {
@@ -225,10 +232,6 @@ impl VulkanRenderer {
225232 true
226233 }
227234 });
228- self.dmabuf_textures.retain(|k, _| !k.is_gone());
229- self.dmabuf_targets.retain(|k, _| !k.is_gone());
230- self.staged.retain(|_, (b, _)| b.is_alive());
231- self.staging_pool.trim();
232235 failed.map_or(Ok(()), |e| Err(e.into()))
233236 }
234237
@@ -303,12 +306,12 @@ impl VulkanRenderer {
303306 Ok(fence)
304307 }
305308
306- /// Records, submits and waits for a short command buffer. Queued uploads
307- /// go first, so `record` sees their pixels.
309+ /// Records and submits a short command buffer. Queued uploads go first, so
310+ /// `record` sees their pixels.
308311 fn one_shot(
309312 &mut self,
310313 record: impl FnOnce(&ash::Device, vk::CommandBuffer, u32, &mut LayoutLog),
311- ) -> Result<(), Error> {
314+ ) -> Result<Arc<FenceInner>, Error> {
312315 let cmd = self.begin()?;
313316 let mut recorded = self.record_uploads(cmd);
314317 record(
@@ -317,8 +320,7 @@ impl VulkanRenderer {
317320 self.shared.device.queue_family,
318321 &mut recorded.layouts,
319322 );
320- let fence = self.finish(cmd, recorded, Vec::new())?;
321- Ok(fence.wait()?)
323+ self.finish(cmd, recorded, Vec::new())
322324 }
323325
324326 /// Submits `cmd`, which holds `recorded` and works on `keep`, and tracks
@@ -406,8 +408,13 @@ impl VulkanRenderer {
406408 Recorded { uploads, layouts }
407409 }
408410
411+ /// Whether pixels are staged for a frame to carry to the GPU.
412+ pub fn has_queued_uploads(&self) -> bool {
413+ !self.uploads.is_empty()
414+ }
415+
409416 /// Submits the queued uploads without waiting for a frame to carry them.
410- fn flush_uploads(&mut self) -> Result<(), Error> {
417+ pub fn flush_uploads(&mut self) -> Result<(), Error> {
411418 if self.uploads.is_empty() {
412419 return Ok(());
413420 }
@@ -613,7 +620,7 @@ impl VulkanRenderer {
613620 if image.external {
614621 return Err(Error::ForeignTexture);
615622 }
616- self.reap()?;
623+ self.reap_fences()?;
617624 if region.size.w <= 0 || region.size.h <= 0 {
618625 return Ok(());
619626 }
@@ -653,7 +660,7 @@ impl VulkanRenderer {
653660 };
654661 self.upload_bytes += upload.bytes();
655662 self.uploads.push(upload);
656- if self.upload_bytes > UPLOAD_BATCH_BYTES {
663+ if self.upload_bytes >= POOL_BYTES {
657664 self.flush_uploads()?;
658665 }
659666 Ok(())
@@ -698,7 +705,7 @@ impl VulkanRenderer {
698705 });
699706 let buffer = staging.buffer;
700707 let source = image.clone();
701- self.one_shot(|d, cmd, family, layouts| unsafe {
708+ let fence = self.one_shot(|d, cmd, family, layouts| unsafe {
702709 let mut barriers = Barriers::default();
703710 barriers.acquire(
704711 family,
@@ -729,6 +736,17 @@ impl VulkanRenderer {
729736 &vk::DependencyInfo::default().buffer_memory_barriers(&to_host),
730737 );
731738 })?;
739+ if let Err(e) = fence.wait() {
740+ // A timed out GPU may still write the buffer, so it stays out of the pool.
741+ if let Some(p) = self
742+ .pending
743+ .iter_mut()
744+ .find(|p| Arc::ptr_eq(&p.fence, &fence))
745+ {
746+ p._staging.push(staging);
747+ }
748+ return Err(e.into());
749+ }
732750 }
733751 let mut data = staging.bytes()?[..w as usize * h as usize * BYTES_PER_PIXEL].to_vec();
734752 if swap {