Skip to main content

Module staging

Module staging 

Source
Expand description

StagingRing: a handful of persistently reused, CPU-mapped staging buffers a renderer writes one frame’s uploads into, then copies out of inside the frame’s own command encoder.

§Why a ring, when Queue::write_buffer already stages

wgpu’s Queue::write_buffer/write_texture (and write_buffer_with) are correct by construction — the bytes are copied into queue-owned staging memory at call time and the device-side copy is ordered ahead of the next submission — but on native backends every call creates a new staging buffer: a driver buffer object, a memory sub-allocation and a mapping, all torn down again once the submission retires. A frame issuing half a dozen such writes pays that object churn half a dozen times per frame, every frame, for data whose size barely changes.

The ring keeps the staging memory instead. Each slot is one MAP_WRITE | COPY_SRC buffer; a frame writes every upload into one slot back to back, records copy_buffer_to_buffer/copy_buffer_to_texture from it into the frame’s own encoder, and hands the slot back to the GPU. The steady state creates no buffer at all.

§The fence: re-mapped on submit

A slot is written by the CPU only while it is mapped, and a mapped buffer cannot be used by a submission, so the slot is unmapped when the frame finishes (StagingFrame::finish) and re-mapped once the GPU is done reading it. The re-map is attached to the caller’s encoder with CommandEncoder::map_buffer_on_submit: whoever submits that encoder starts the mapping as part of the submit, and wgpu completes it only after that submission’s work has finished. The completion callback is the slot’s fence — it flips the slot back to SlotState::Ready, and a slot in any other state is never handed out. That keeps the “a renderer never submits the caller’s encoder” contract intact: the ring needs no hand-back from the submit site, because the encoder itself carries the fence there.

The callback runs inside whatever call next processes completed work — Queue::submit does so after every submission, as does Device::poll. StagingRing::begin polls once (without waiting) when every slot is still in flight, so a frame that catches the GPU just finishing does not needlessly give up its slot.

An encoder that is dropped rather than submitted drops the mapping request with it, and the callback with it: the slot is then SlotState::Abandoned — unmapped, read by nothing — and is replaced by a fresh mapped buffer the next time it is picked. A failed mapping (a lost device) is SlotState::Failed and handled the same way.

§Slots and growth

STAGING_SLOTS slots: the swapchain lets two frames be in flight (DESIRED_MAXIMUM_FRAME_LATENCY in crate::surface), and the frame being recorded needs a third. Slots are created on first use, each at the next power of two of what the frame needs (never below MIN_SLOT_BYTES); a frame that outgrows a free slot replaces that slot’s buffer with a larger one. Replacing a slot never invalidates work in flight: only a SlotState::Ready slot is replaced, and wgpu keeps a dropped buffer alive until every submission naming it has retired.

A frame larger than the ring’s per-slot ceiling, or one that finds every slot still in flight, gets no slot (StagingRing::begin answers None) and the caller falls back to queue writes for that frame — the same bytes by the other route, so a refusal costs time, never pixels.

§Host-testable split

The slot policy — which slot a frame takes, when one is created or grown, when one is refused, how offsets are aligned — lives in the buffer-generic StagingRing and SlotLease and is exercised by host tests with a fake StagingAllocator and a fake fence (SlotFence signalled by hand). Only StagingRing::begin, StagingFrame and the wgpu::Device allocator touch a live device.

Structs§

FenceToken
The one-shot signal a slot in flight is waiting for.
SlotFence
One slot’s state, shared between the ring and the completion callback that signals it.
SlotLease
One frame’s hold on one slot: the bump cursor its uploads are placed at.
StagingFrame
One frame’s open slot: a mapped view to write uploads into and the copy commands that move them to their destinations.
StagingRing
The slots, their capacities and fences: one frame’s staging memory at a time, reused once the GPU is done with it.
StagingStats
What a ring has done so far — a diagnostic, never a decision input.

Enums§

SlotState
Where one slot is in its frame cycle.

Constants§

BUFFER_COPY_ALIGNMENT
The alignment a buffer-to-buffer copy’s source offset is placed at.
MAX_SLOT_BYTES
The largest buffer a slot is grown to. A frame needing more than this gets no slot and falls back to queue writes, rather than every slot of the ring being held at a size one outlier frame asked for.
MIN_SLOT_BYTES
The smallest buffer a slot is ever created at, so a ring whose frames grow a little at a time does not reallocate on every one of them.
STAGING_SLOTS
How many slots a ring keeps: the swapchain’s two frames in flight plus the one being recorded.
STAGING_USAGE
The usages every slot buffer is created with: written through a mapping, read by copy commands.
TEXTURE_COPY_ALIGNMENT
The alignment a buffer-to-texture copy’s source offset is placed at.

Traits§

StagingAllocator
Creates slot buffers.

Functions§

align_up
value rounded up to a multiple of alignment (an alignment of zero is treated as one), or None when the rounding overflows.
slot_capacity
The capacity a slot is created or grown at to hold needed bytes: the next power of two, never below MIN_SLOT_BYTES.