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}