Skip to main content

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}