Skip to main content

frust_gpu/
staging.rs

1//! [`StagingRing`]: a handful of persistently reused, CPU-mapped staging
2//! buffers a renderer writes one frame's uploads into, then copies out of
3//! inside the frame's own command encoder.
4//!
5//! # Why a ring, when `Queue::write_buffer` already stages
6//!
7//! `wgpu`'s `Queue::write_buffer`/`write_texture` (and `write_buffer_with`)
8//! are correct by construction — the bytes are copied into queue-owned staging
9//! memory at call time and the device-side copy is ordered ahead of the next
10//! submission — but on native backends every call creates a *new* staging
11//! buffer: a driver buffer object, a memory sub-allocation and a mapping, all
12//! torn down again once the submission retires. A frame issuing half a dozen
13//! such writes pays that object churn half a dozen times per frame, every
14//! frame, for data whose size barely changes.
15//!
16//! The ring keeps the staging memory instead. Each slot is one
17//! `MAP_WRITE | COPY_SRC` buffer; a frame writes every upload into one slot
18//! back to back, records `copy_buffer_to_buffer`/`copy_buffer_to_texture` from
19//! it into the frame's own encoder, and hands the slot back to the GPU. The
20//! steady state creates no buffer at all.
21//!
22//! # The fence: re-mapped on submit
23//!
24//! A slot is written by the CPU only while it is mapped, and a mapped buffer
25//! cannot be used by a submission, so the slot is unmapped when the frame
26//! finishes ([`StagingFrame::finish`]) and re-mapped once the GPU is done
27//! reading it. The re-map is attached to the *caller's* encoder with
28//! `CommandEncoder::map_buffer_on_submit`: whoever submits that encoder
29//! starts the mapping as part of the submit, and `wgpu` completes it only after
30//! that submission's work has finished. The completion callback is the slot's
31//! fence — it flips the slot back to [`SlotState::Ready`], and a slot in any
32//! other state is never handed out. That keeps the "a renderer never submits
33//! the caller's encoder" contract intact: the ring needs no hand-back from the
34//! submit site, because the encoder itself carries the fence there.
35//!
36//! The callback runs inside whatever call next processes completed work —
37//! `Queue::submit` does so after every submission, as does `Device::poll`.
38//! [`StagingRing::begin`] polls once (without waiting) when every slot is
39//! still in flight, so a frame that catches the GPU just finishing does not
40//! needlessly give up its slot.
41//!
42//! An encoder that is dropped rather than submitted drops the mapping request
43//! with it, and the callback with it: the slot is then [`SlotState::Abandoned`]
44//! — unmapped, read by nothing — and is replaced by a fresh mapped buffer the
45//! next time it is picked. A failed mapping (a lost device) is
46//! [`SlotState::Failed`] and handled the same way.
47//!
48//! # Slots and growth
49//!
50//! [`STAGING_SLOTS`] slots: the swapchain lets two frames be in flight
51//! (`DESIRED_MAXIMUM_FRAME_LATENCY` in [`crate::surface`]), and the frame being
52//! recorded needs a third. Slots are created on first use, each at the next
53//! power of two of what the frame needs (never below [`MIN_SLOT_BYTES`]); a
54//! frame that outgrows a free slot replaces that slot's buffer with a larger
55//! one. Replacing a slot never invalidates work in flight: only a
56//! [`SlotState::Ready`] slot is replaced, and `wgpu` keeps a dropped buffer
57//! alive until every submission naming it has retired.
58//!
59//! A frame larger than the ring's per-slot ceiling, or one that finds every
60//! slot still in flight, gets no slot ([`StagingRing::begin`] answers `None`)
61//! and the caller falls back to queue writes for that frame — the same bytes
62//! by the other route, so a refusal costs time, never pixels.
63//!
64//! # Host-testable split
65//!
66//! The slot policy — which slot a frame takes, when one is created or grown,
67//! when one is refused, how offsets are aligned — lives in the buffer-generic
68//! [`StagingRing`] and [`SlotLease`] and is exercised by host tests with a
69//! fake [`StagingAllocator`] and a fake fence ([`SlotFence`] signalled by
70//! hand). Only [`StagingRing::begin`], [`StagingFrame`] and the `wgpu::Device`
71//! allocator touch a live device.
72
73use std::sync::Arc;
74use std::sync::atomic::{AtomicU8, Ordering};
75
76/// How many slots a ring keeps: the swapchain's two frames in flight plus the
77/// one being recorded.
78pub const STAGING_SLOTS: usize = 3;
79
80/// The smallest buffer a slot is ever created at, so a ring whose frames grow
81/// a little at a time does not reallocate on every one of them.
82pub const MIN_SLOT_BYTES: u64 = 64 * 1024;
83
84/// The largest buffer a slot is grown to. A frame needing more than this gets
85/// no slot and falls back to queue writes, rather than every slot of the ring
86/// being held at a size one outlier frame asked for.
87pub const MAX_SLOT_BYTES: u64 = 32 * 1024 * 1024;
88
89/// The usages every slot buffer is created with: written through a mapping,
90/// read by copy commands.
91pub const STAGING_USAGE: wgpu::BufferUsages =
92    wgpu::BufferUsages::MAP_WRITE.union(wgpu::BufferUsages::COPY_SRC);
93
94/// The alignment a buffer-to-buffer copy's source offset is placed at.
95///
96/// `wgpu` needs [`wgpu::COPY_BUFFER_ALIGNMENT`] (4); sixteen keeps every
97/// record a uniform or vertex stream holds on its natural boundary as well.
98pub const BUFFER_COPY_ALIGNMENT: u64 = 16;
99
100/// The alignment a buffer-to-texture copy's source offset is placed at.
101///
102/// `wgpu` needs a multiple of the texel block size; 512 also satisfies the
103/// strictest native placement rule (D3D12's texture data placement
104/// alignment), so no backend has to split the copy.
105pub const TEXTURE_COPY_ALIGNMENT: u64 = 512;
106
107/// `value` rounded up to a multiple of `alignment` (an `alignment` of zero is
108/// treated as one), or `None` when the rounding overflows.
109#[must_use]
110pub fn align_up(value: u64, alignment: u64) -> Option<u64> {
111    value.checked_next_multiple_of(alignment.max(1))
112}
113
114/// The capacity a slot is created or grown at to hold `needed` bytes: the
115/// next power of two, never below [`MIN_SLOT_BYTES`].
116#[must_use]
117pub fn slot_capacity(needed: u64) -> u64 {
118    needed
119        .checked_next_power_of_two()
120        .unwrap_or(needed)
121        .max(MIN_SLOT_BYTES)
122}
123
124/// Where one slot is in its frame cycle.
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum SlotState {
127    /// Mapped and read by no submission: the only state a slot is handed out
128    /// in.
129    Ready,
130    /// Handed to a frame that is still writing into it.
131    Recording,
132    /// Unmapped and carried by an encoder: waiting for that encoder's
133    /// submission to finish and the re-map to complete.
134    InFlight,
135    /// Its encoder was dropped without being submitted, so no re-map is
136    /// coming: the slot is unmapped and idle, and is replaced when next
137    /// picked.
138    Abandoned,
139    /// Its re-map failed (a lost device): replaced when next picked.
140    Failed,
141}
142
143impl SlotState {
144    const fn to_u8(self) -> u8 {
145        match self {
146            Self::Ready => 0,
147            Self::Recording => 1,
148            Self::InFlight => 2,
149            Self::Abandoned => 3,
150            Self::Failed => 4,
151        }
152    }
153
154    const fn from_u8(raw: u8) -> Self {
155        match raw {
156            0 => Self::Ready,
157            1 => Self::Recording,
158            2 => Self::InFlight,
159            3 => Self::Abandoned,
160            _ => Self::Failed,
161        }
162    }
163}
164
165/// One slot's state, shared between the ring and the completion callback that
166/// signals it.
167///
168/// Atomic because that callback runs on whichever thread next processes
169/// completed GPU work, which need not be the thread recording frames.
170#[derive(Debug, Clone)]
171pub struct SlotFence(Arc<AtomicU8>);
172
173impl SlotFence {
174    /// A fence for a freshly created, mapped slot.
175    #[must_use]
176    pub fn ready() -> Self {
177        Self(Arc::new(AtomicU8::new(SlotState::Ready.to_u8())))
178    }
179
180    /// The slot's current state.
181    #[must_use]
182    pub fn state(&self) -> SlotState {
183        SlotState::from_u8(self.0.load(Ordering::Acquire))
184    }
185
186    fn set(&self, state: SlotState) {
187        self.0.store(state.to_u8(), Ordering::Release);
188    }
189
190    /// Moves a recording slot in flight and returns the token its completion
191    /// callback carries.
192    #[must_use]
193    pub fn arm(&self) -> FenceToken {
194        self.set(SlotState::InFlight);
195        FenceToken {
196            fence: Some(Arc::clone(&self.0)),
197        }
198    }
199}
200
201/// The one-shot signal a slot in flight is waiting for.
202///
203/// [`Self::signal`] is called by the re-map's completion callback. A token
204/// dropped without being signalled — the encoder carrying it was dropped
205/// unsubmitted, so its callback was dropped too — marks the slot
206/// [`SlotState::Abandoned`] instead of leaving it in flight forever.
207#[derive(Debug)]
208pub struct FenceToken {
209    fence: Option<Arc<AtomicU8>>,
210}
211
212impl FenceToken {
213    /// The re-map completed (`mapped`) or failed: the slot is
214    /// [`SlotState::Ready`] or [`SlotState::Failed`].
215    pub fn signal(mut self, mapped: bool) {
216        if let Some(fence) = self.fence.take() {
217            let state = if mapped {
218                SlotState::Ready
219            } else {
220                SlotState::Failed
221            };
222            fence.store(state.to_u8(), Ordering::Release);
223        }
224    }
225}
226
227impl Drop for FenceToken {
228    fn drop(&mut self) {
229        if let Some(fence) = self.fence.take() {
230            // Only a slot still in flight is abandoned: one already signalled
231            // took the `signal` path above and never reaches here.
232            let _ = fence.compare_exchange(
233                SlotState::InFlight.to_u8(),
234                SlotState::Abandoned.to_u8(),
235                Ordering::AcqRel,
236                Ordering::Acquire,
237            );
238        }
239    }
240}
241
242/// Creates slot buffers.
243///
244/// Exists so the ring's policy takes a creation *capability* rather than a
245/// `wgpu::Device`: a host test implements it with a counter and plain ids.
246/// `wgpu::Device` is the only production implementation.
247pub trait StagingAllocator {
248    /// The created buffer — `wgpu::Buffer` in production.
249    type Buffer: Clone;
250
251    /// A [`STAGING_USAGE`] buffer of `size` bytes, already mapped for writing.
252    fn create_mapped(&self, size: u64) -> Self::Buffer;
253}
254
255impl StagingAllocator for wgpu::Device {
256    type Buffer = wgpu::Buffer;
257
258    fn create_mapped(&self, size: u64) -> wgpu::Buffer {
259        self.create_buffer(&wgpu::BufferDescriptor {
260            label: Some("frust-gpu staging ring slot"),
261            size,
262            usage: STAGING_USAGE,
263            mapped_at_creation: true,
264        })
265    }
266}
267
268/// What a ring has done so far — a diagnostic, never a decision input.
269#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
270pub struct StagingStats {
271    /// Frames handed a slot.
272    pub frames: u64,
273    /// Frames refused because every slot was still in flight.
274    pub busy: u64,
275    /// Frames refused because they needed more than a slot may hold.
276    pub too_large: u64,
277    /// Slot buffers created, first creations and replacements both.
278    pub buffers_created: u64,
279}
280
281#[derive(Debug)]
282struct Slot<B> {
283    buffer: B,
284    capacity: u64,
285    fence: SlotFence,
286}
287
288/// The slots, their capacities and fences: one frame's staging memory at a
289/// time, reused once the GPU is done with it.
290///
291/// Generic over the buffer type for the reason the module header gives; the
292/// engine uses the default `StagingRing<wgpu::Buffer>`.
293#[derive(Debug)]
294pub struct StagingRing<B = wgpu::Buffer> {
295    slots: Vec<Slot<B>>,
296    max_slots: usize,
297    max_slot_bytes: u64,
298    stats: StagingStats,
299}
300
301impl<B> Default for StagingRing<B> {
302    fn default() -> Self {
303        Self::new()
304    }
305}
306
307impl<B> StagingRing<B> {
308    /// An empty ring of [`STAGING_SLOTS`] slots of at most
309    /// [`MAX_SLOT_BYTES`] each. No buffer exists until the first frame.
310    #[must_use]
311    pub fn new() -> Self {
312        Self::with_limits(STAGING_SLOTS, MAX_SLOT_BYTES)
313    }
314
315    /// An empty ring with its slot count and per-slot ceiling stated.
316    #[must_use]
317    pub fn with_limits(max_slots: usize, max_slot_bytes: u64) -> Self {
318        Self {
319            slots: Vec::with_capacity(max_slots),
320            max_slots: max_slots.max(1),
321            max_slot_bytes,
322            stats: StagingStats::default(),
323        }
324    }
325
326    /// What this ring has done so far.
327    #[must_use]
328    pub fn stats(&self) -> StagingStats {
329        self.stats
330    }
331
332    /// How many slots exist.
333    #[must_use]
334    pub fn slot_count(&self) -> usize {
335        self.slots.len()
336    }
337
338    /// Slot `index`'s capacity and state, if it exists.
339    #[must_use]
340    pub fn slot(&self, index: usize) -> Option<(u64, SlotState)> {
341        self.slots
342            .get(index)
343            .map(|slot| (slot.capacity, slot.fence.state()))
344    }
345
346    /// Whether some slot could be handed out right now without waiting on the
347    /// GPU: a ready slot, a replaceable one, or room for a new one.
348    fn has_free_slot(&self) -> bool {
349        self.slots.len() < self.max_slots
350            || self.slots.iter().any(|slot| {
351                matches!(
352                    slot.fence.state(),
353                    SlotState::Ready | SlotState::Abandoned | SlotState::Failed
354                )
355            })
356    }
357}
358
359impl<B: Clone> StagingRing<B> {
360    /// Hands out a slot holding at least `bytes`, or `None` when the frame
361    /// needs more than a slot may hold or every slot is still in flight.
362    ///
363    /// In order of preference: a ready slot already large enough (no
364    /// allocation), a ready slot too small or an abandoned/failed one
365    /// replaced at [`slot_capacity`]`(bytes)`, a new slot while the ring has
366    /// fewer than its limit. A slot in flight is never handed out.
367    pub fn acquire<A>(&mut self, allocator: &A, bytes: u64) -> Option<SlotLease<B>>
368    where
369        A: StagingAllocator<Buffer = B>,
370    {
371        let bytes = bytes.max(wgpu::COPY_BUFFER_ALIGNMENT);
372        if bytes > self.max_slot_bytes {
373            self.stats.too_large = self.stats.too_large.saturating_add(1);
374            return None;
375        }
376        let capacity = slot_capacity(bytes).min(self.max_slot_bytes.max(bytes));
377
378        let fitting = self
379            .slots
380            .iter()
381            .position(|slot| slot.fence.state() == SlotState::Ready && slot.capacity >= bytes);
382        let index = if let Some(index) = fitting {
383            index
384        } else if let Some(index) = self.slots.iter().position(|slot| {
385            matches!(
386                slot.fence.state(),
387                SlotState::Ready | SlotState::Abandoned | SlotState::Failed
388            )
389        }) {
390            let slot = self.slots.get_mut(index)?;
391            slot.buffer = allocator.create_mapped(capacity);
392            slot.capacity = capacity;
393            slot.fence = SlotFence::ready();
394            self.stats.buffers_created = self.stats.buffers_created.saturating_add(1);
395            index
396        } else if self.slots.len() < self.max_slots {
397            self.slots.push(Slot {
398                buffer: allocator.create_mapped(capacity),
399                capacity,
400                fence: SlotFence::ready(),
401            });
402            self.stats.buffers_created = self.stats.buffers_created.saturating_add(1);
403            self.slots.len() - 1
404        } else {
405            self.stats.busy = self.stats.busy.saturating_add(1);
406            return None;
407        };
408
409        let slot = self.slots.get(index)?;
410        slot.fence.set(SlotState::Recording);
411        self.stats.frames = self.stats.frames.saturating_add(1);
412        Some(SlotLease {
413            index,
414            buffer: slot.buffer.clone(),
415            capacity: slot.capacity,
416            cursor: 0,
417            fence: slot.fence.clone(),
418        })
419    }
420}
421
422/// One frame's hold on one slot: the bump cursor its uploads are placed at.
423///
424/// Released by [`Self::finish`], which moves the slot in flight and returns
425/// the token its fence waits on. A lease dropped without finishing leaves the
426/// slot [`SlotState::Abandoned`] — whatever it recorded is still correct to
427/// submit, but no re-map was asked for, so the slot is replaced rather than
428/// reused.
429#[derive(Debug)]
430pub struct SlotLease<B> {
431    index: usize,
432    buffer: B,
433    capacity: u64,
434    cursor: u64,
435    fence: SlotFence,
436}
437
438impl<B> SlotLease<B> {
439    /// Which slot of the ring this is.
440    #[must_use]
441    pub fn index(&self) -> usize {
442        self.index
443    }
444
445    /// The slot's buffer, which every copy this frame records reads from.
446    #[must_use]
447    pub fn buffer(&self) -> &B {
448        &self.buffer
449    }
450
451    /// The slot's capacity in bytes.
452    #[must_use]
453    pub fn capacity(&self) -> u64 {
454        self.capacity
455    }
456
457    /// Bytes placed so far, alignment padding included.
458    #[must_use]
459    pub fn used(&self) -> u64 {
460        self.cursor
461    }
462
463    /// Places `len` bytes at the next multiple of `alignment`, returning
464    /// their offset, or `None` (placing nothing) when they do not fit.
465    pub fn reserve(&mut self, len: u64, alignment: u64) -> Option<u64> {
466        let offset = align_up(self.cursor, alignment)?;
467        let end = offset.checked_add(len)?;
468        if end > self.capacity {
469            return None;
470        }
471        self.cursor = end;
472        Some(offset)
473    }
474
475    /// Ends the frame's hold: the slot goes in flight, waiting on the token.
476    #[must_use]
477    pub fn finish(mut self) -> FenceToken {
478        let token = self.fence.arm();
479        // Disarm the drop below: the slot is in flight, not abandoned.
480        self.cursor = u64::MAX;
481        token
482    }
483}
484
485impl<B> Drop for SlotLease<B> {
486    fn drop(&mut self) {
487        if self.cursor != u64::MAX && self.fence.state() == SlotState::Recording {
488            self.fence.set(SlotState::Abandoned);
489        }
490    }
491}
492
493impl StagingRing<wgpu::Buffer> {
494    /// Opens one frame's staging: a mapped slot of at least `bytes`, or `None`
495    /// when the caller should fall back to queue writes this frame (see
496    /// [`Self::acquire`]).
497    ///
498    /// When every slot is still in flight the device is polled once, without
499    /// waiting, so a slot whose submission has already finished is re-mapped
500    /// rather than skipped.
501    pub fn begin(&mut self, device: &wgpu::Device, bytes: u64) -> Option<StagingFrame> {
502        if !self.has_free_slot() {
503            let _ = device.poll(wgpu::PollType::Poll);
504        }
505        let lease = self.acquire(device, bytes)?;
506        let view = lease
507            .buffer()
508            .slice(..lease.capacity())
509            .get_mapped_range_mut();
510        match view {
511            Ok(view) => Some(StagingFrame {
512                lease: Some(lease),
513                view: Some(view),
514            }),
515            // Unreachable while the fence and the mapping agree; a slot that
516            // reads ready but cannot be viewed is dropped as abandoned (the
517            // lease's own drop) and the frame takes the queue-write route.
518            Err(_) => None,
519        }
520    }
521}
522
523/// One frame's open slot: a mapped view to write uploads into and the copy
524/// commands that move them to their destinations.
525///
526/// Every copy is recorded into the encoder the caller passes, at the point
527/// the caller calls — so a renderer records its copies ahead of the passes
528/// that read their destinations. [`Self::finish`] must run before that encoder
529/// is submitted: it unmaps the slot (a mapped buffer cannot be submitted) and
530/// attaches the slot's fence to the encoder. A frame dropped without it
531/// unmaps the slot and leaves it abandoned, which is still safe to submit.
532#[derive(Debug)]
533pub struct StagingFrame {
534    lease: Option<SlotLease<wgpu::Buffer>>,
535    view: Option<wgpu::BufferViewMut>,
536}
537
538impl StagingFrame {
539    /// Bytes placed so far, alignment padding included.
540    #[must_use]
541    pub fn used(&self) -> u64 {
542        self.lease.as_ref().map_or(0, SlotLease::used)
543    }
544
545    /// The slot this frame writes into.
546    #[must_use]
547    pub fn slot(&self) -> Option<usize> {
548        self.lease.as_ref().map(SlotLease::index)
549    }
550
551    /// Writes `bytes` into the slot and records their copy to `dst` at
552    /// `dst_offset`, answering whether it did; `false` (nothing written,
553    /// nothing recorded) means the caller writes them by queue instead.
554    ///
555    /// Refused when they do not fit the slot or are not a whole number of
556    /// [`wgpu::COPY_BUFFER_ALIGNMENT`] units, which a copy command cannot
557    /// move.
558    pub fn copy_to_buffer(
559        &mut self,
560        encoder: &mut wgpu::CommandEncoder,
561        bytes: &[u8],
562        dst: &wgpu::Buffer,
563        dst_offset: u64,
564    ) -> bool {
565        self.copy_parts_to_buffer(encoder, &[bytes], dst, dst_offset)
566    }
567
568    /// [`Self::copy_to_buffer`] over `parts` laid back to back: one copy
569    /// command for data the caller holds in pieces, landing in `dst` exactly
570    /// as the pieces' concatenation. Only the whole is held to the copy
571    /// alignment, not each piece.
572    pub fn copy_parts_to_buffer(
573        &mut self,
574        encoder: &mut wgpu::CommandEncoder,
575        parts: &[&[u8]],
576        dst: &wgpu::Buffer,
577        dst_offset: u64,
578    ) -> bool {
579        let len: u64 = parts.iter().map(|part| part.len() as u64).sum();
580        if len == 0 {
581            return true;
582        }
583        if !len.is_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT)
584            || !dst_offset.is_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT)
585        {
586            return false;
587        }
588        let (Some(lease), Some(view)) = (self.lease.as_mut(), self.view.as_mut()) else {
589            return false;
590        };
591        let Some(offset) = lease.reserve(len, BUFFER_COPY_ALIGNMENT) else {
592            return false;
593        };
594        let Ok(mut start) = usize::try_from(offset) else {
595            return false;
596        };
597        for part in parts {
598            let end = start + part.len();
599            view.slice(start..end).copy_from_slice(part);
600            start = end;
601        }
602        encoder.copy_buffer_to_buffer(lease.buffer(), offset, dst, dst_offset, len);
603        true
604    }
605
606    /// Writes one texture upload of `rows` rows of `bytes_per_row` bytes into
607    /// the slot — `fill` is handed exactly that many bytes of the mapping and
608    /// must write every one of them — and records its copy to `dst` over
609    /// `extent`, answering whether it did. `false` (nothing written, nothing
610    /// recorded) means the caller writes the texture by queue instead.
611    ///
612    /// Refused when the upload does not fit the slot, or when `bytes_per_row`
613    /// is not a multiple of [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`], which a
614    /// buffer-to-texture copy requires (a queue write does not).
615    pub fn copy_to_texture(
616        &mut self,
617        encoder: &mut wgpu::CommandEncoder,
618        dst: wgpu::TexelCopyTextureInfo<'_>,
619        bytes_per_row: u32,
620        rows: u32,
621        extent: wgpu::Extent3d,
622        fill: impl FnOnce(wgpu::WriteOnly<'_, [u8]>),
623    ) -> bool {
624        if !bytes_per_row.is_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) {
625            return false;
626        }
627        let len = u64::from(bytes_per_row) * u64::from(rows);
628        if len == 0 {
629            return false;
630        }
631        let (Some(lease), Some(view)) = (self.lease.as_mut(), self.view.as_mut()) else {
632            return false;
633        };
634        let Some(offset) = lease.reserve(len, TEXTURE_COPY_ALIGNMENT) else {
635            return false;
636        };
637        let (Ok(start), Ok(end)) = (usize::try_from(offset), usize::try_from(offset + len)) else {
638            return false;
639        };
640        fill(view.slice(start..end));
641        encoder.copy_buffer_to_texture(
642            wgpu::TexelCopyBufferInfo {
643                buffer: lease.buffer(),
644                layout: wgpu::TexelCopyBufferLayout {
645                    offset,
646                    bytes_per_row: Some(bytes_per_row),
647                    rows_per_image: Some(rows),
648                },
649            },
650            dst,
651            extent,
652        );
653        true
654    }
655
656    /// Ends the frame: unmaps the slot and attaches its fence to `encoder`, so
657    /// the slot is re-mapped — and handed out again — only once the
658    /// submission carrying `encoder` has finished on the GPU.
659    pub fn finish(mut self, encoder: &wgpu::CommandEncoder) {
660        drop(self.view.take());
661        if let Some(lease) = self.lease.take() {
662            lease.buffer().unmap();
663            let buffer = lease.buffer().clone();
664            let token = lease.finish();
665            encoder.map_buffer_on_submit(&buffer, wgpu::MapMode::Write, .., move |result| {
666                token.signal(result.is_ok());
667            });
668        }
669    }
670}
671
672impl Drop for StagingFrame {
673    fn drop(&mut self) {
674        drop(self.view.take());
675        if let Some(lease) = self.lease.take() {
676            // Unmapped so an encoder that did record this frame's copies can
677            // still be submitted; the lease's own drop marks the slot
678            // abandoned.
679            lease.buffer().unmap();
680        }
681    }
682}