atagen

monolith

public
Star0
Watch0

Commit 04f0de436841

tarball

atagen <boss@atagen.co>2026-10-17 04:35 UTC

2 files changed+86-9
render-vk: pool staging buffers instead of allocating per upload

Bare-metal spans showed vkQueueSubmit on the main thread stalling for 1 to 3 ms whenever the import worker staged a large shm copy. Allocating and freeing host-visible memory takes a driver lock on NVIDIA that submission also waits on, and the 14.7 MB staging buffer for a full-output layer came and went every second. Dropped staging buffers now go back to a pool of up to 96 MiB and are reused for uploads of up to twice their size.
Modified file: crates/render-vk/src/device.rs+18 additions
@@ -7,10 +7,27 @@ use std::sync::Mutex;
77 /// Fields drop in declaration order, so the allocator frees its memory before
88 /// the device is destroyed.
99 pub(crate) struct Shared {
10+ /// Staging buffers kept for reuse, see `Staging`.
11+ pub staging_pool: Mutex<Vec<crate::image::Pooled>>,
1012 pub allocator: Mutex<Allocator>,
1113 pub device: Device,
1214 }
1315
16+impl Drop for Shared {
17+ fn drop(&mut self) {
18+ let pool = std::mem::take(
19+ self.staging_pool
20+ .get_mut()
21+ .unwrap_or_else(|e| e.into_inner()),
22+ );
23+ let allocator = self.allocator.get_mut().unwrap_or_else(|e| e.into_inner());
24+ for p in pool {
25+ unsafe { self.device.device.destroy_buffer(p.buffer, None) };
26+ let _ = allocator.free(p.allocation);
27+ }
28+ }
29+}
30+
1431 /// Loaders for the extensions that dmabuf import, export and fence export
1532 /// need. Absent when the device lacks any of them.
1633 pub(crate) struct DmabufExt {
@@ -171,6 +188,7 @@ impl Shared {
171188 spare_fences: Mutex::new(Vec::new()),
172189 };
173190 Ok(Self {
191+ staging_pool: Mutex::new(Vec::new()),
174192 allocator: Mutex::new(allocator?),
175193 device,
176194 })
Modified file: crates/render-vk/src/image.rs+68 additions−9 deletions
@@ -180,13 +180,31 @@ pub(crate) fn color_range() -> vk::ImageSubresourceRange {
180180 .layer_count(1)
181181 }
182182
183-/// A host-visible buffer for uploads and readbacks.
183+/// A host-visible buffer for uploads and readbacks. Dropped ones go back to
184+/// a pool on `Shared` instead of being freed. Allocating and freeing large
185+/// host-visible memory takes a driver lock on NVIDIA that `vkQueueSubmit`
186+/// also waits on, so a staging buffer made per upload on the import worker
187+/// stalled the main thread's submits for 1 to 3 ms.
184188 pub(crate) struct Staging {
185189 pub shared: Arc<Shared>,
186190 pub buffer: vk::Buffer,
187191 pub allocation: Option<Allocation>,
192+ size: usize,
193+ usage: vk::BufferUsageFlags,
194+ location: MemoryLocation,
188195 }
189196
197+pub(crate) struct Pooled {
198+ pub buffer: vk::Buffer,
199+ pub allocation: Allocation,
200+ size: usize,
201+ usage: vk::BufferUsageFlags,
202+ location: MemoryLocation,
203+}
204+
205+/// Pooled staging memory kept at most, past which dropped buffers are freed.
206+const POOL_BYTES: usize = 96 * 1024 * 1024;
207+
190208 impl Staging {
191209 pub fn new(
192210 shared: &Arc<Shared>,
@@ -194,6 +212,26 @@ impl Staging {
194212 usage: vk::BufferUsageFlags,
195213 location: MemoryLocation,
196214 ) -> Result<Self, Error> {
215+ let reused = {
216+ let mut pool = shared
217+ .staging_pool
218+ .lock()
219+ .unwrap_or_else(|e| e.into_inner());
220+ let fits = |p: &Pooled| {
221+ p.usage == usage && p.location == location && p.size >= size && p.size <= size * 2
222+ };
223+ pool.iter().position(fits).map(|i| pool.swap_remove(i))
224+ };
225+ if let Some(p) = reused {
226+ return Ok(Self {
227+ shared: shared.clone(),
228+ buffer: p.buffer,
229+ allocation: Some(p.allocation),
230+ size: p.size,
231+ usage,
232+ location,
233+ });
234+ }
197235 let d = &shared.device.device;
198236 let buffer = unsafe {
199237 d.create_buffer(
@@ -227,6 +265,9 @@ impl Staging {
227265 shared: shared.clone(),
228266 buffer,
229267 allocation: Some(allocation),
268+ size,
269+ usage,
270+ location,
230271 })
231272 }
232273
@@ -247,15 +288,33 @@ impl Staging {
247288
248289 impl Drop for Staging {
249290 fn drop(&mut self) {
250- unsafe { self.shared.device.device.destroy_buffer(self.buffer, None) };
251- if let Some(a) = self.allocation.take() {
252- let _ = self
253- .shared
254- .allocator
255- .lock()
256- .unwrap_or_else(|e| e.into_inner())
257- .free(a);
291+ let Some(allocation) = self.allocation.take() else {
292+ return;
293+ };
294+ let mut pool = self
295+ .shared
296+ .staging_pool
297+ .lock()
298+ .unwrap_or_else(|e| e.into_inner());
299+ let pooled: usize = pool.iter().map(|p| p.size).sum();
300+ if pooled + self.size <= POOL_BYTES {
301+ pool.push(Pooled {
302+ buffer: self.buffer,
303+ allocation,
304+ size: self.size,
305+ usage: self.usage,
306+ location: self.location,
307+ });
308+ return;
258309 }
310+ drop(pool);
311+ unsafe { self.shared.device.device.destroy_buffer(self.buffer, None) };
312+ let _ = self
313+ .shared
314+ .allocator
315+ .lock()
316+ .unwrap_or_else(|e| e.into_inner())
317+ .free(allocation);
259318 }
260319 }