unlit_wgpu/staging.rs
1//! A reused host-visible staging buffer for per-frame uploads.
2//!
3//! Data that changes every frame cannot be written straight into the buffer
4//! the GPU reads from: a buffer is either mapped by the host or read by a
5//! submission, never both. The usual shortcut, [`wgpu::Queue::write_buffer`],
6//! hides that by allocating a fresh staging buffer per call and freeing it only
7//! after a later submission has finished, so a scene that uploads every frame
8//! allocates every frame.
9//!
10//! [`StagingBuffer`] keeps those staging buffers instead: one per destination
11//! buffer, written through a mapping carried across frames, with the copy
12//! recorded into an encoder the caller already owns. A steady scene then
13//! uploads without allocating.
14//!
15//! Both routes reach the GPU the same way — the copy joins the next
16//! [`wgpu::Queue::submit`] either way — so what a staging buffer buys is the
17//! allocation, which is worth most where the allocation is largest. On WebGL2
18//! it is host memory: buffer mapping is emulated there, so a staging buffer is
19//! a plain byte buffer the copy is uploaded from. See
20//! [the section below](#what-a-staging-buffer-is-on-webgl2).
21//!
22//! ```rust
23//! # let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
24//! # let target = device.create_buffer(&wgpu::BufferDescriptor {
25//! # label: Some("target"),
26//! # size: 64,
27//! # usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
28//! # mapped_at_creation: false,
29//! # });
30//! use unlit_wgpu::staging::StagingBuffer;
31//!
32//! let mut staging = StagingBuffer::new();
33//! let mut encoder = device.create_command_encoder(&Default::default());
34//! staging.write(&device, &mut encoder, &target, 0, &[0u8; 16]);
35//! queue.submit([encoder.finish()]);
36//! ```
37//!
38//! # How a staging buffer is recycled
39//!
40//! A staging buffer is created mapped, so the first frame writes into it
41//! without waiting for anything. To copy out of it, it is unmapped and the
42//! copy is recorded; the mapping is then re-requested with
43//! [`wgpu::CommandEncoder::map_buffer_on_submit`], whose callback runs once
44//! the submission holding the copy has finished and marks the buffer writable
45//! again.
46//!
47//! Nothing polls: [`wgpu::Queue::submit`] maintains the device itself, which
48//! is what fires the callbacks. Until one has fired the buffer is treated as
49//! busy, and a frame that needs a staging buffer while the previous ones are
50//! still in flight — a frame or two ahead of the GPU — gets one of its own.
51//! The pool therefore settles at as many buffers as there are frames in
52//! flight, rather than growing per frame.
53//!
54//! # What a staging buffer is on WebGL2
55//!
56//! WebGL2 has no `glMapBufferRange`, so wgpu emulates mapping in software: a
57//! buffer created with [`wgpu::BufferUsages::MAP_WRITE`] is given no GL buffer
58//! at all and holds its bytes in a host-side `Vec<u8>`, which a copy out of it
59//! uploads with `bufferSubData` when the submission runs.
60//!
61//! A staging buffer is worth even more there than on a native backend, because
62//! the allocation it saves is a host one. [`wgpu::Queue::write_buffer`] on
63//! WebGL2 allocates such a `Vec` per call and keeps it alive until a later
64//! submission has finished with it, so a scene uploading every frame churns
65//! host memory every frame. The pool replaces that with a fixed set of buffers
66//! that the host keeps writing into.
67//!
68//! The recycling scheme is unchanged: the platform supplies
69//! [`wgpu::CommandEncoder::map_buffer_on_submit`] like any other, so nothing
70//! here branches on the backend.
71
72use core::sync::atomic::{AtomicBool, Ordering};
73use std::sync::Arc;
74
75/// One staging buffer and whether the host may write into its mapping.
76struct Slot {
77 /// The staging buffer itself: host-writable and copyable from.
78 buffer: wgpu::Buffer,
79 /// Size in bytes.
80 size: u64,
81 /// Whether the host owns the mapping, and so may write the next upload
82 /// into this buffer. Set when the buffer is created mapped and again by the
83 /// mapping callback; cleared by every write.
84 writable: Arc<AtomicBool>,
85}
86
87impl Slot {
88 /// Allocate a buffer of `size` bytes, mapped and ready to be written.
89 fn new(device: &wgpu::Device, size: u64) -> Self {
90 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
91 label: Some("unlit_wgpu::staging"),
92 size,
93 usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
94 mapped_at_creation: true,
95 });
96 Self {
97 buffer,
98 size,
99 writable: Arc::new(AtomicBool::new(true)),
100 }
101 }
102
103 /// Whether this buffer is writable and holds at least `size` bytes.
104 fn can_reuse(&self, size: u64) -> bool {
105 self.size >= size && self.writable.load(Ordering::Acquire)
106 }
107}
108
109/// A pool of staging buffers for one destination buffer, reused across frames.
110///
111/// One of these belongs to each buffer a caller uploads to every frame; see
112/// the module docs for the recycling scheme.
113#[derive(Default)]
114pub struct StagingBuffer {
115 slots: Vec<Slot>,
116}
117
118impl StagingBuffer {
119 /// A pool holding no staging buffer yet.
120 pub fn new() -> Self {
121 Self::default()
122 }
123
124 /// Upload `data` into `target` at `offset`, recording the copy into
125 /// `encoder`.
126 ///
127 /// The bytes reach a reused staging buffer by host write and the GPU on
128 /// the copy, so `target` needs [`wgpu::BufferUsages::COPY_DST`] and must
129 /// not be mapped. `offset` and the length of `data` must both be multiples
130 /// of [`wgpu::COPY_BUFFER_ALIGNMENT`], as [`wgpu::Queue::write_buffer`]
131 /// requires. An empty `data` records no copy.
132 ///
133 /// # Panics
134 ///
135 /// If `offset` or `data.len()` is not a multiple of
136 /// [`wgpu::COPY_BUFFER_ALIGNMENT`].
137 pub fn write(
138 &mut self,
139 device: &wgpu::Device,
140 encoder: &mut wgpu::CommandEncoder,
141 target: &wgpu::Buffer,
142 offset: u64,
143 data: &[u8],
144 ) {
145 let size = data.len() as u64;
146 if size == 0 {
147 return;
148 }
149 assert!(
150 offset.is_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT),
151 "a staging upload offset of {offset} is not a multiple of `COPY_BUFFER_ALIGNMENT`"
152 );
153 assert!(
154 size.is_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT),
155 "a staging upload of {size} bytes is not a multiple of `COPY_BUFFER_ALIGNMENT`"
156 );
157
158 let slot = self.writable_slot(device, size);
159 slot.buffer
160 .slice(..size)
161 .get_mapped_range_mut()
162 .expect("a staging buffer is mapped while the host owns it")
163 .copy_from_slice(data);
164 // Unmapping ends the host's access, which the copy below needs; the
165 // mapping is asked for again so the buffer outlives the copy.
166 slot.buffer.unmap();
167 encoder.copy_buffer_to_buffer(&slot.buffer, 0, target, offset, size);
168
169 let writable = Arc::clone(&slot.writable);
170 encoder.map_buffer_on_submit(&slot.buffer, wgpu::MapMode::Write, .., move |result| {
171 // A failed mapping leaves the buffer marked unwritable: it is
172 // never written again, which is safer than reusing memory the
173 // device never handed back.
174 if result.is_ok() {
175 writable.store(true, Ordering::Release);
176 }
177 });
178 }
179
180 /// Upload `data` into `texture`, recording the copy into `encoder`.
181 ///
182 /// The image is `layout`-described and spans `size`, exactly as
183 /// [`wgpu::Queue::write_texture`] takes them, but reaches the texture
184 /// through a reused staging buffer rather than a fresh one per call. The
185 /// texture needs [`wgpu::TextureUsages::COPY_DST`].
186 ///
187 /// `layout.bytes_per_row` must be a multiple of
188 /// [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`]: a buffer-to-texture copy
189 /// requires it, unlike [`wgpu::Queue::write_texture`], which re-aligns the
190 /// rows itself. Only `layout.offset` into the staging buffer is always
191 /// zero — the data starts there — so a non-zero `layout.offset` is
192 /// rejected rather than silently reading from the wrong place. An empty
193 /// image records no copy.
194 ///
195 /// # Panics
196 ///
197 /// If `layout.offset` is non-zero, if `layout.bytes_per_row` is set and not
198 /// a multiple of [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`], or if `data` is
199 /// not a multiple of [`wgpu::COPY_BUFFER_ALIGNMENT`] long.
200 pub fn write_texture(
201 &mut self,
202 device: &wgpu::Device,
203 encoder: &mut wgpu::CommandEncoder,
204 texture: &wgpu::Texture,
205 layout: wgpu::TexelCopyBufferLayout,
206 size: wgpu::Extent3d,
207 data: &[u8],
208 ) {
209 if data.is_empty() {
210 return;
211 }
212 assert_eq!(
213 layout.offset, 0,
214 "a staged texture upload starts at the staging buffer's own start"
215 );
216 if let Some(bytes_per_row) = layout.bytes_per_row {
217 assert!(
218 bytes_per_row.is_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT),
219 "a texture upload row of {bytes_per_row} bytes is not a multiple of \
220 `COPY_BYTES_PER_ROW_ALIGNMENT`"
221 );
222 }
223
224 let slot = self.writable_slot(device, data.len() as u64);
225 slot.buffer
226 .slice(..data.len() as u64)
227 .get_mapped_range_mut()
228 .expect("a staging buffer is mapped while the host owns it")
229 .copy_from_slice(data);
230 // Unmapping ends the host's access, which the copy below needs; the
231 // mapping is asked for again so the buffer outlives the copy.
232 slot.buffer.unmap();
233 encoder.copy_buffer_to_texture(
234 wgpu::TexelCopyBufferInfo {
235 buffer: &slot.buffer,
236 layout,
237 },
238 wgpu::TexelCopyTextureInfo {
239 texture,
240 mip_level: 0,
241 origin: wgpu::Origin3d::ZERO,
242 aspect: wgpu::TextureAspect::All,
243 },
244 size,
245 );
246
247 let writable = Arc::clone(&slot.writable);
248 encoder.map_buffer_on_submit(&slot.buffer, wgpu::MapMode::Write, .., move |result| {
249 if result.is_ok() {
250 writable.store(true, Ordering::Release);
251 }
252 });
253 }
254
255 /// A buffer of at least `size` bytes the host may write into.
256 ///
257 /// The smallest writable buffer that fits is preferred, so a buffer grown
258 /// for one heavy frame does not become the only candidate. A writable
259 /// buffer too small for the frame is rebuilt in place: the frame outgrew
260 /// it, and a buffer sized for the largest frame so far is the one later
261 /// frames will reuse — accumulating one buffer per size ever seen is what
262 /// makes reuse grow without bound. When every buffer is in flight another
263 /// is added, so the pool settles at as many buffers as there are frames in
264 /// flight rather than growing per frame.
265 fn writable_slot(&mut self, device: &wgpu::Device, size: u64) -> &mut Slot {
266 let index = match self
267 .slots
268 .iter()
269 .enumerate()
270 .filter(|(_, slot)| slot.can_reuse(size))
271 .min_by_key(|(_, slot)| slot.size)
272 .map(|(index, _)| index)
273 {
274 Some(index) => index,
275 None => {
276 match self
277 .slots
278 .iter()
279 .position(|slot| slot.writable.load(Ordering::Acquire))
280 {
281 Some(index) => {
282 // Writable but too small: the frame has outgrown it.
283 self.slots[index] = Slot::new(device, size);
284 index
285 }
286 None => {
287 self.slots.push(Slot::new(device, size));
288 self.slots.len() - 1
289 }
290 }
291 }
292 };
293 // The caller is about to take the mapping, so the buffer is no longer
294 // writable until the mapping callback hands it back — including a
295 // buffer that was just created mapped.
296 self.slots[index].writable.store(false, Ordering::Release);
297 &mut self.slots[index]
298 }
299
300 /// Release every staging buffer the host is free to write, keeping only
301 /// the ones still in flight.
302 ///
303 /// A buffer is kept automatically because it may be the one a later frame
304 /// reuses, so a pool settles at the frames in flight rather than growing.
305 /// That also means a frame that needed far more data than the ones after
306 /// it leaves its buffer behind. Call this once uploads are known to have
307 /// shrunk — a scene change, a resize — to give the memory back; the pool
308 /// rebuilds from the frames in flight.
309 pub fn release_idle(&mut self) {
310 self.slots
311 .retain(|slot| !slot.writable.load(Ordering::Acquire));
312 }
313
314 /// How many staging buffers the pool holds.
315 ///
316 /// Mostly useful to a caller deciding whether to [release idle
317 /// buffers](Self::release_idle).
318 pub fn len(&self) -> usize {
319 self.slots.len()
320 }
321
322 /// Whether the pool holds no staging buffer at all.
323 pub fn is_empty(&self) -> bool {
324 self.slots.is_empty()
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use crate::util::test::noop_device;
332 use ringbuf::StaticRb;
333 use ringbuf::traits::{Consumer, Producer, Split};
334
335 /// A destination buffer the tests upload into and read back from.
336 fn target(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
337 device.create_buffer(&wgpu::BufferDescriptor {
338 label: Some("test::target"),
339 size,
340 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
341 mapped_at_creation: false,
342 })
343 }
344
345 /// Start an encoder, write `data` into `target` through `staging`, and
346 /// submit it.
347 fn upload(
348 device: &wgpu::Device,
349 queue: &wgpu::Queue,
350 staging: &mut StagingBuffer,
351 target: &wgpu::Buffer,
352 data: &[u8],
353 ) {
354 let mut encoder = device.create_command_encoder(&Default::default());
355 staging.write(device, &mut encoder, target, 0, data);
356 queue.submit([encoder.finish()]);
357 }
358
359 /// Read `target` back and return its first `size` bytes.
360 fn read_back(device: &wgpu::Device, target: &wgpu::Buffer, size: u64) -> Vec<u8> {
361 let slice = target.slice(..size);
362 let (mut tx, mut rx) = StaticRb::<Result<(), wgpu::BufferAsyncError>, 1>::default().split();
363 slice.map_async(wgpu::MapMode::Read, move |result| {
364 tx.try_push(result).expect("the test reads the result");
365 });
366 device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
367 rx.try_pop().expect("the mapping callback runs").unwrap();
368 let bytes = slice
369 .get_mapped_range()
370 .expect("the mapping succeeded")
371 .to_vec();
372 target.unmap();
373 bytes
374 }
375
376 #[test]
377 fn a_write_reaches_the_target_through_the_staging_buffer() {
378 let (device, queue) = noop_device();
379 let target = target(&device, 16);
380 let data: Vec<u8> = (0..16).collect();
381
382 let mut staging = StagingBuffer::new();
383 upload(&device, &queue, &mut staging, &target, &data);
384
385 assert_eq!(read_back(&device, &target, 16), data);
386 }
387
388 #[test]
389 fn the_pool_settles_instead_of_growing_per_frame() {
390 let (device, queue) = noop_device();
391 let target = target(&device, 16);
392
393 // A frame's mapping is handed back once the submission that copied out
394 // of its buffer completes, which a later submit drives. Frame N+1 can
395 // therefore still find frame N's buffer in flight and add another, so
396 // the pool settles at the frames in flight rather than at one buffer.
397 let mut staging = StagingBuffer::new();
398 for _ in 0..16 {
399 upload(&device, &queue, &mut staging, &target, &[7; 16]);
400 }
401
402 assert_eq!(
403 staging.len(),
404 2,
405 "a steady scene stops allocating after the frames in flight"
406 );
407 assert_eq!(read_back(&device, &target, 16), vec![7; 16]);
408 }
409
410 #[test]
411 fn a_frame_arriving_while_the_last_is_in_flight_gets_its_own_buffer() {
412 let (device, _queue) = noop_device();
413 let target = target(&device, 16);
414 let mut staging = StagingBuffer::new();
415
416 // Two frames are recorded without submitting either, so the first
417 // frame's mapping has not been handed back when the second runs.
418 let mut first = device.create_command_encoder(&Default::default());
419 staging.write(&device, &mut first, &target, 0, &[1; 16]);
420 let mut second = device.create_command_encoder(&Default::default());
421 staging.write(&device, &mut second, &target, 0, &[2; 16]);
422
423 assert_eq!(
424 staging.len(),
425 2,
426 "the second frame cannot write the buffer the first still holds"
427 );
428 }
429
430 #[test]
431 fn a_staging_buffer_too_small_for_the_frame_is_replaced_not_kept() {
432 let (device, queue) = noop_device();
433 let target = target(&device, 64);
434 let mut staging = StagingBuffer::new();
435
436 // The first frame sizes the pool at 16 bytes; the frames after it need
437 // 64, so every buffer that was only large enough for the first is
438 // rebuilt rather than kept alongside a larger one.
439 upload(&device, &queue, &mut staging, &target, &[1; 16]);
440 for _ in 0..4 {
441 upload(&device, &queue, &mut staging, &target, &[2; 64]);
442 }
443
444 assert!(
445 staging.slots.iter().all(|slot| slot.size >= 64),
446 "a buffer sized for an earlier, smaller frame is not kept"
447 );
448 assert_eq!(read_back(&device, &target, 64), vec![2; 64]);
449 }
450
451 #[test]
452 fn an_empty_write_records_nothing() {
453 let (device, _queue) = noop_device();
454 let target = target(&device, 16);
455 let mut staging = StagingBuffer::new();
456 let mut encoder = device.create_command_encoder(&Default::default());
457
458 staging.write(&device, &mut encoder, &target, 0, &[]);
459
460 assert!(staging.is_empty(), "nothing was staged");
461 }
462
463 #[test]
464 fn releasing_idle_buffers_gives_back_the_grown_ones() {
465 let (device, queue) = noop_device();
466 let target = target(&device, 64);
467 let mut staging = StagingBuffer::new();
468
469 // A heavy frame grows the pool; the frames after it are small again.
470 upload(&device, &queue, &mut staging, &target, &[1; 64]);
471 upload(&device, &queue, &mut staging, &target, &[2; 16]);
472 assert!(!staging.is_empty());
473
474 staging.release_idle();
475
476 assert!(
477 staging.slots.iter().all(|slot| !slot.can_reuse(64)),
478 "the buffer grown for the heavy frame was released"
479 );
480 }
481
482 #[test]
483 #[should_panic(expected = "COPY_BUFFER_ALIGNMENT")]
484 fn a_write_of_unaligned_size_panics() {
485 let (device, _queue) = noop_device();
486 let target = target(&device, 16);
487 let mut staging = StagingBuffer::new();
488 let mut encoder = device.create_command_encoder(&Default::default());
489
490 staging.write(&device, &mut encoder, &target, 0, &[0; 6]);
491 }
492}