atagen

monolith

public
Star0
Watch0

Commit 61ebcd0a3619

tarball

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

3 files changed+147-42
render-vk: batch shm uploads into the next submit

copy_staged ended in its own queue submit, so a frame uploading more than eight surfaces could step past the ABSORB_AHEAD window and take the NVIDIA 1024th-submit stall mid-frame. Uploads now queue as Upload entries and are recorded at the head of the frame's command buffer, or of the read_back command buffer, so a frame is one submit however many surfaces changed.
Modified file: crates/render-vk/src/renderer/mod.rs+109 additions−41 deletions
@@ -39,11 +39,24 @@ struct Pending {
3939 fence: Arc<FenceInner>,
4040 cmd: vk::CommandBuffer,
4141 _keep: Vec<Arc<ImageInner>>,
42- _staging: Option<Staging>,
42+ _staging: Vec<Staging>,
4343 }
4444
45+/// A queued copy of staged pixels into `image`, recorded by the next submit.
46+struct Upload {
47+ image: Arc<ImageInner>,
48+ staging: Staging,
49+ copy: vk::BufferImageCopy,
50+}
51+
52+/// Queued staging memory past which uploads are submitted without waiting
53+/// for a frame.
54+const UPLOAD_BATCH_BYTES: usize = 64 * 1024 * 1024;
55+
4556 pub struct VulkanRenderer {
4657 pending: Vec<Pending>,
58+ uploads: Vec<Upload>,
59+ upload_bytes: usize,
4760 dmabuf_textures: HashMap<WeakDmabuf, VulkanTexture>,
4861 dmabuf_targets: HashMap<WeakDmabuf, Arc<ImageInner>>,
4962 pipelines: Pipelines,
@@ -118,6 +131,8 @@ impl VulkanRenderer {
118131 }
119132 Ok(Self {
120133 pending: Vec::new(),
134+ uploads: Vec::new(),
135+ upload_bytes: 0,
121136 dmabuf_textures: HashMap::new(),
122137 dmabuf_targets: HashMap::new(),
123138 pipelines: Pipelines::new(&shared)?,
@@ -251,21 +266,97 @@ impl VulkanRenderer {
251266 Ok(fence)
252267 }
253268
254- /// Records, submits and waits for a short command buffer.
269+ /// Records, submits and waits for a short command buffer. Queued uploads
270+ /// go first, so `record` sees their pixels.
255271 fn one_shot(
256272 &mut self,
257273 record: impl FnOnce(&ash::Device, vk::CommandBuffer, u32),
258274 ) -> Result<(), Error> {
259275 let cmd = self.begin()?;
276+ let uploads = self.record_uploads(cmd);
260277 record(
261278 &self.shared.device.device,
262279 cmd,
263280 self.shared.device.queue_family,
264281 );
282+ let fence = self.finish(cmd, uploads, Vec::new())?;
283+ Ok(fence.wait()?)
284+ }
285+
286+ /// Submits `cmd`, which holds `uploads` and works on `keep`, and tracks
287+ /// it until its fence signals.
288+ fn finish(
289+ &mut self,
290+ cmd: vk::CommandBuffer,
291+ uploads: Vec<Upload>,
292+ mut keep: Vec<Arc<ImageInner>>,
293+ ) -> Result<Arc<FenceInner>, Error> {
265294 let fence = self.end_and_submit(cmd)?;
266- let res = fence.wait();
267- self.spare_cmds.push(cmd);
268- Ok(res?)
295+ keep.extend(uploads.iter().map(|u| u.image.clone()));
296+ self.pending.push(Pending {
297+ fence: fence.clone(),
298+ cmd,
299+ _keep: keep,
300+ _staging: uploads.into_iter().map(|u| u.staging).collect(),
301+ });
302+ Ok(fence)
303+ }
304+
305+ /// Records every queued upload into `cmd` and hands them back to keep
306+ /// alive until it finishes.
307+ fn record_uploads(&mut self, cmd: vk::CommandBuffer) -> Vec<Upload> {
308+ let uploads = std::mem::take(&mut self.uploads);
309+ self.upload_bytes = 0;
310+ if uploads.is_empty() {
311+ return uploads;
312+ }
313+ let d = &self.shared.device.device;
314+ let family = self.shared.device.queue_family;
315+ unsafe {
316+ for u in &uploads {
317+ acquire(d, cmd, family, &u.image, vk::ImageLayout::TRANSFER_DST_OPTIMAL);
318+ }
319+ for (i, u) in uploads.iter().enumerate() {
320+ if uploads[..i].iter().any(|e| Arc::ptr_eq(&e.image, &u.image)) {
321+ let order = [vk::MemoryBarrier2::default()
322+ .src_stage_mask(vk::PipelineStageFlags2::TRANSFER)
323+ .src_access_mask(vk::AccessFlags2::TRANSFER_WRITE)
324+ .dst_stage_mask(vk::PipelineStageFlags2::TRANSFER)
325+ .dst_access_mask(vk::AccessFlags2::TRANSFER_WRITE)];
326+ d.cmd_pipeline_barrier2(
327+ cmd,
328+ &vk::DependencyInfo::default().memory_barriers(&order),
329+ );
330+ }
331+ d.cmd_copy_buffer_to_image(
332+ cmd,
333+ u.staging.buffer,
334+ u.image.image,
335+ vk::ImageLayout::TRANSFER_DST_OPTIMAL,
336+ &[u.copy],
337+ );
338+ }
339+ for u in &uploads {
340+ acquire(
341+ d,
342+ cmd,
343+ family,
344+ &u.image,
345+ vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
346+ );
347+ }
348+ }
349+ uploads
350+ }
351+
352+ /// Submits the queued uploads without waiting for a frame to carry them.
353+ fn flush_uploads(&mut self) -> Result<(), Error> {
354+ if self.uploads.is_empty() {
355+ return Ok(());
356+ }
357+ let cmd = self.begin()?;
358+ let uploads = self.record_uploads(cmd);
359+ self.finish(cmd, uploads, Vec::new()).map(drop)
269360 }
270361
271362 #[profiling::function]
@@ -294,6 +385,7 @@ impl VulkanRenderer {
294385 });
295386 }
296387 let cmd = self.begin()?;
388+ let uploads = self.record_uploads(cmd);
297389 let d = &self.shared.device.device;
298390 let family = self.shared.device.queue_family;
299391 let push = &self.shared.device.push_descriptor;
@@ -416,14 +508,8 @@ impl VulkanRenderer {
416508 release(d, cmd, family, t, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
417509 }
418510 }
419- let fence = self.end_and_submit(cmd)?;
420511 sampled.push(image);
421- self.pending.push(Pending {
422- fence: fence.clone(),
423- cmd,
424- _keep: sampled,
425- _staging: None,
426- });
512+ let fence = self.finish(cmd, uploads, sampled)?;
427513 Ok(SyncPoint::from(VkFence(fence)))
428514 }
429515
@@ -454,8 +540,8 @@ impl VulkanRenderer {
454540 }
455541
456542 /// Uploads `region` of `image`, taking each image row `y` in the region
457- /// from `row(y)`. The CPU does not wait: the copy is ordered before later
458- /// frames by its barrier, and its staging memory lives until its fence.
543+ /// from `row(y)`. The pixels are staged now and copied by the next submit,
544+ /// which the CPU does not wait for.
459545 #[profiling::function]
460546 fn upload_with<'a>(
461547 &mut self,
@@ -474,7 +560,7 @@ impl VulkanRenderer {
474560 self.copy_staged(image, region, staging)
475561 }
476562
477- /// Copies staged pixels into `region` of `image` on the GPU.
563+ /// Queues staged pixels to be copied into `region` of `image`.
478564 fn copy_staged(
479565 &mut self,
480566 image: &Arc<ImageInner>,
@@ -499,33 +585,15 @@ impl VulkanRenderer {
499585 height: h as u32,
500586 depth: 1,
501587 });
502- let cmd = self.begin()?;
503- let d = &self.shared.device.device;
504- let family = self.shared.device.queue_family;
505- unsafe {
506- acquire(d, cmd, family, image, vk::ImageLayout::TRANSFER_DST_OPTIMAL);
507- d.cmd_copy_buffer_to_image(
508- cmd,
509- staging.buffer,
510- image.image,
511- vk::ImageLayout::TRANSFER_DST_OPTIMAL,
512- &[copy],
513- );
514- acquire(
515- d,
516- cmd,
517- family,
518- image,
519- vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
520- );
521- }
522- let fence = self.end_and_submit(cmd)?;
523- self.pending.push(Pending {
524- fence,
525- cmd,
526- _keep: vec![image.clone()],
527- _staging: Some(staging),
588+ self.uploads.push(Upload {
589+ image: image.clone(),
590+ staging,
591+ copy,
528592 });
593+ self.upload_bytes += w * h * BYTES_PER_PIXEL;
594+ if self.upload_bytes > UPLOAD_BATCH_BYTES {
595+ self.flush_uploads()?;
596+ }
529597 Ok(())
530598 }
531599
Modified file: crates/render-vk/src/renderer/stall.rs+1 additions−1 deletions
@@ -64,7 +64,7 @@ impl VulkanRenderer {
6464 fence,
6565 cmd,
6666 _keep: Vec::new(),
67- _staging: None,
67+ _staging: Vec::new(),
6868 });
6969 }
7070 Ok(())
Modified file: crates/render-vk/tests/golden.rs+37 additions
@@ -238,3 +238,40 @@ fn quarter_turn_on_a_wide_output_fills_it() {
238238 assert!(g.0 != r.0 && g.1 != r.1, "{transform:?} not diagonal");
239239 }
240240 }
241+
242+#[test]
243+fn queued_uploads_to_one_texture_land_in_order() {
244+ let mut s = Scene::new();
245+ let size = (2, 2);
246+ let bytes = |p: Pixel| -> Vec<u8> { [p; 4].iter().flatten().copied().collect() };
247+ let tex = s.texture(size, &[RED; 4]);
248+ for colour in [WHITE, GREEN] {
249+ s.renderer
250+ .update_memory(&tex, &bytes(colour), Rectangle::from_size(size.into()))
251+ .unwrap();
252+ }
253+ let read = s
254+ .renderer
255+ .copy_texture(&tex, Rectangle::from_size(size.into()), Fourcc::Abgr8888)
256+ .unwrap();
257+ assert_eq!(s.renderer.map_texture(&read).unwrap()[..4], GREEN);
258+ s.renderer
259+ .update_memory(&tex, &bytes(WHITE), Rectangle::from_size(size.into()))
260+ .unwrap();
261+ s.renderer
262+ .update_memory(&tex, &bytes(RED), Rectangle::from_size(size.into()))
263+ .unwrap();
264+ s.draw(Transform::Normal, |f| {
265+ f.render_texture_from_to(
266+ &tex,
267+ Rectangle::from_size((2.0, 2.0).into()),
268+ full(),
269+ &[full()],
270+ &[],
271+ Transform::Normal,
272+ 1.0,
273+ )
274+ .unwrap();
275+ });
276+ assert_eq!(at(&s.pixels(), 32, 32), RED);
277+}