Skip to content

Commit

Permalink
[core] Document command encoding and command buffers.
Browse files Browse the repository at this point in the history
Flesh out the documentation for `wgpu_core`'s `CommandBuffer`,
`CommandEncoder`, and associated types.

Allow doc links to private items. `wgpu-core` isn't entirely
user-facing, so it's useful to document internal items.
  • Loading branch information
jimblandy committed Apr 14, 2024
1 parent 5b8be97 commit a3bc86d
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 10 deletions.
160 changes: 159 additions & 1 deletion wgpu-core/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,115 @@ use crate::device::trace::Command as TraceCommand;

const PUSH_CONSTANT_CLEAR_ARRAY: &[u32] = &[0_u32; 64];

/// The current state of a [`CommandBuffer`].
#[derive(Debug)]
pub(crate) enum CommandEncoderStatus {
/// Ready to record commands. An encoder's initial state.
///
/// Command building methods like [`command_encoder_clear_buffer`] and
/// [`command_encoder_run_compute_pass`] require the encoder to be in this
/// state.
///
/// [`command_encoder_clear_buffer`]: Global::command_encoder_clear_buffer
/// [`command_encoder_run_compute_pass`]: Global::command_encoder_run_compute_pass
Recording,

/// Command recording is complete, and the buffer is ready for submission.
///
/// [`Global::command_encoder_finish`] transitions a
/// `CommandBuffer` from the `Recording` state into this state.
///
/// [`Global::queue_submit`] drops command buffers unless they are
/// in this state.
Finished,

/// An error occurred while recording a compute or render pass.
///
/// When a `CommandEncoder` is left in this state, we have also
/// returned an error result from the function that encountered
/// the problem. Future attempts to use the encoder (that is,
/// calls to [`CommandBuffer::get_encoder`]) will also return
/// errors.
///
/// Calling [`Global::command_encoder_finish`] in this state
/// discards the command buffer under construction.
Error,
}

/// A raw [`CommandEncoder`][rce], and the raw [`CommandBuffer`][rcb]s built from it.
///
/// Each wgpu-core [`CommandBuffer`] owns an instance of this type, which is
/// where the commands are actually stored.
///
/// This holds a `Vec` of raw [`CommandBuffer`][rcb]s, not just one. We are not
/// always able to record commands in the order in which they must ultimately be
/// submitted to the queue, but raw command buffers don't permit inserting new
/// commands into the middle of a recorded stream. However, hal queue submission
/// accepts a series of command buffers at once, so we can simply break the
/// stream up into multiple buffers, and then reorder the buffers. See
/// [`CommandEncoder::close_and_swap`] for a specific example of this.
///
/// Note that a [`CommandEncoderId`] actually refers to a [`CommandBuffer`].
/// Methods that take a command encoder id actually look up the command buffer,
/// and then use its encoder.
///
/// [rce]: wgpu_hal::Api::CommandEncoder
/// [rcb]: wgpu_hal::Api::CommandBuffer
pub(crate) struct CommandEncoder<A: HalApi> {
/// The underlying `wgpu_hal` [`CommandEncoder`].
///
/// Successfully executed command buffers' encoders are saved in a
/// [`wgpu_hal::device::CommandAllocator`] for recycling.
///
/// [`CommandEncoder`]: wgpu_hal::Api::CommandEncoder
raw: A::CommandEncoder,

/// All the raw command buffers for our owning [`CommandBuffer`], in
/// submission order.
///
/// These command buffers were all constructed with `raw`. The
/// [`wgpu_hal::CommandEncoder`] trait forbids these from outliving `raw`,
/// and requires that we provide all of these when we call
/// [`raw.reset_all()`][CE::ra], so the encoder and its buffers travel
/// together.
///
/// [CE::ra]: wgpu_hal::CommandEncoder::reset_all
list: Vec<A::CommandBuffer>,

/// True if `raw` is in the "recording" state.
///
/// See the documentation for [`wgpu_hal::CommandEncoder`] for
/// details on the states `raw` can be in.
is_open: bool,

label: Option<String>,
}

//TODO: handle errors better
impl<A: HalApi> CommandEncoder<A> {
/// Closes the live encoder
/// Finish the current command buffer, if any, and place it
/// at the second-to-last position in our list.
///
/// If we have opened this command encoder, finish its current
/// command buffer, and insert it just before the last element in
/// [`self.list`][l]. If this command buffer is closed, do nothing.
///
/// On return, the underlying hal encoder is closed.
///
/// What is this for?
///
/// The `wgpu_hal` contract requires that each render or compute pass's
/// commands be preceded by calls to [`transition_buffers`] and
/// [`transition_textures`], to put the resources the pass operates on in
/// the appropriate state. Unfortunately, we don't know which transitions
/// are needed until we're done recording the pass itself. Rather than
/// iterating over the pass twice, we note the necessary transitions as we
/// record its commands, finish the raw command buffer for the actual pass,
/// record a new raw command buffer for the transitions, and jam that buffer
/// in just before the pass's. This is the function that jams in the
/// transitions' command buffer.
///
/// [l]: CommandEncoder::list
fn close_and_swap(&mut self) -> Result<(), DeviceError> {
if self.is_open {
self.is_open = false;
Expand All @@ -65,6 +157,16 @@ impl<A: HalApi> CommandEncoder<A> {
Ok(())
}

/// Finish the current command buffer, if any, and add it to the
/// end of [`self.list`][l].
///
/// If we have opened this command encoder, finish its current
/// command buffer, and push it onto the end of [`self.list`][l].
/// If this command buffer is closed, do nothing.
///
/// On return, the underlying hal encoder is closed.
///
/// [l]: CommandEncoder::list
fn close(&mut self) -> Result<(), DeviceError> {
if self.is_open {
self.is_open = false;
Expand All @@ -75,13 +177,19 @@ impl<A: HalApi> CommandEncoder<A> {
Ok(())
}

/// Discard the command buffer under construction, if any.
///
/// The underlying hal encoder is closed, if it was recording.
pub(crate) fn discard(&mut self) {
if self.is_open {
self.is_open = false;
unsafe { self.raw.discard_encoding() };
}
}

/// Begin recording a new command buffer, if we haven't already.
///
/// The underlying hal encoder is put in the "recording" state.
pub(crate) fn open(&mut self) -> Result<&mut A::CommandEncoder, DeviceError> {
if !self.is_open {
self.is_open = true;
Expand All @@ -92,6 +200,10 @@ impl<A: HalApi> CommandEncoder<A> {
Ok(&mut self.raw)
}

/// Begin recording a new command buffer for a render pass, with
/// its own label.
///
/// The underlying hal encoder is put in the "recording" state.
fn open_pass(&mut self, label: Option<&str>) -> Result<(), DeviceError> {
self.is_open = true;
unsafe { self.raw.begin_encoding(label)? };
Expand All @@ -111,12 +223,27 @@ pub(crate) struct BakedCommands<A: HalApi> {
pub(crate) struct DestroyedBufferError(pub id::BufferId);
pub(crate) struct DestroyedTextureError(pub id::TextureId);

/// The mutable state of a [`CommandBuffer`].
pub struct CommandBufferMutable<A: HalApi> {
/// The [`wgpu_hal::Api::CommandBuffer`]s we've built so far, and the encoder
/// they belong to.
pub(crate) encoder: CommandEncoder<A>,

/// The current state of this command buffer's encoder.
status: CommandEncoderStatus,

/// All the resources that the commands recorded so far have referred to.
pub(crate) trackers: Tracker<A>,

/// The regions of buffers and textures these commands will read and write.
///
/// This is used to determine which portions of which
/// buffers/textures we actually need to initialize. If we're
/// definitely going to write to something before we read from it,
/// we don't need to clear its contents.
buffer_memory_init_actions: Vec<BufferInitTrackerAction<A>>,
texture_memory_actions: CommandBufferTextureMemoryActions<A>,

pub(crate) pending_query_resets: QueryResetMap<A>,
#[cfg(feature = "trace")]
pub(crate) commands: Option<Vec<TraceCommand>>,
Expand All @@ -133,11 +260,36 @@ impl<A: HalApi> CommandBufferMutable<A> {
}
}

/// A buffer of commands to be submitted to the GPU for execution.
///
/// Whereas the WebGPU API uses two separate types for command buffers and
/// encoders, this type is a fusion of the two:
///
/// - During command recording, this holds a [`CommandEncoder`] accepting this
/// buffer's commands. In this state, the [`CommandBuffer`] type behaves like
/// a WebGPU `GPUCommandEncoder`.
///
/// - Once command recording is finished by calling
/// [`Global::command_encoder_finish`], no further recording is allowed. The
/// internal [`CommandEncoder`] is retained solely as a storage pool for the
/// raw command buffers. In this state, the value behaves like a WebGPU
/// `GPUCommandBuffer`.
///
/// - Once a command buffer is submitted to the queue, it is removed from the id
/// registry, and its contents are taken to construct a [`BakedCommands`],
/// whose contents eventually become the property of the submission queue.
pub struct CommandBuffer<A: HalApi> {
pub(crate) device: Arc<Device<A>>,
limits: wgt::Limits,
support_clear_texture: bool,
pub(crate) info: ResourceInfo<CommandBuffer<A>>,

/// The mutable state of this command buffer.
///
/// This `Option` is populated when the command buffer is first created.
/// When this is submitted, dropped, or destroyed, its contents are
/// extracted into a [`BakedCommands`] by
/// [`CommandBuffer::extract_baked_commands`].
pub(crate) data: Mutex<Option<CommandBufferMutable<A>>>,
}

Expand Down Expand Up @@ -248,6 +400,12 @@ impl<A: HalApi> CommandBuffer<A> {
}

impl<A: HalApi> CommandBuffer<A> {
/// Return the [`CommandBuffer`] for `id`, for recording new commands.
///
/// In `wgpu_core`, the [`CommandBuffer`] type serves both as encoder and
/// buffer, which is why this function takes an [`id::CommandEncoderId`] but
/// returns a [`CommandBuffer`]. The returned command buffer must be in the
/// "recording" state. Otherwise, an error is returned.
fn get_encoder(
hub: &Hub<A>,
id: id::CommandEncoderId,
Expand Down
10 changes: 10 additions & 0 deletions wgpu-core/src/device/life.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,16 @@ struct ActiveSubmission<A: HalApi> {
/// Buffers to be mapped once this submission has completed.
mapped: Vec<Arc<Buffer<A>>>,

/// Command buffers used by this submission, and the encoder that owns them.
///
/// [`wgpu_hal::Queue::submit`] requires the submitted command buffers to
/// remain alive until the submission has completed execution. Command
/// encoders double as allocation pools for command buffers, so holding them
/// here and cleaning them up in [`LifetimeTracker::triage_submissions`]
/// satisfies that requirement.
///
/// Once this submission has completed, the command buffers are reset and
/// the command encoder is recycled.
encoders: Vec<EncoderInFlight<A>>,

/// List of queue "on_submitted_work_done" closures to be called once this
Expand Down
17 changes: 17 additions & 0 deletions wgpu-core/src/device/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,11 +377,24 @@ fn map_buffer<A: HalApi>(
Ok(mapping.ptr)
}

/// A pool of free [`wgpu_hal::CommandEncoder`]s, owned by a `Device`.
///
/// Each encoder in this list is in the "closed" state.
///
/// Since a raw [`CommandEncoder`][ce] is itself a pool for allocating
/// raw [`CommandBuffer`][cb]s, this is a pool of pools.
///
/// [ce]: wgpu_hal::CommandEncoder
/// [cb]: wgpu_hal::Api::CommandBuffer
pub(crate) struct CommandAllocator<A: HalApi> {
free_encoders: Vec<A::CommandEncoder>,
}

impl<A: HalApi> CommandAllocator<A> {
/// Return a fresh [`wgpu_hal::CommandEncoder`] in the "closed" state.
///
/// If we have free encoders in the pool, take one of those. Otherwise,
/// create a new one on `device`.
fn acquire_encoder(
&mut self,
device: &A::Device,
Expand All @@ -396,10 +409,14 @@ impl<A: HalApi> CommandAllocator<A> {
}
}

/// Add `encoder` back to the free pool.
fn release_encoder(&mut self, encoder: A::CommandEncoder) {
self.free_encoders.push(encoder);
}

/// Free the pool of command encoders.
///
/// This is only called when the `Device` is dropped.
fn dispose(self, device: &A::Device) {
resource_log!(
"CommandAllocator::dispose encoders {}",
Expand Down
7 changes: 6 additions & 1 deletion wgpu-core/src/device/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,18 @@ pub enum TempResource<A: HalApi> {
Texture(Arc<Texture<A>>),
}

/// A queue execution for a particular command encoder.
/// A series of [`CommandBuffers`] that have been submitted to a
/// queue, and the [`wgpu_hal::CommandEncoder`] that built them.
pub(crate) struct EncoderInFlight<A: HalApi> {
raw: A::CommandEncoder,
cmd_buffers: Vec<A::CommandBuffer>,
}

impl<A: HalApi> EncoderInFlight<A> {
/// Free all of our command buffers.
///
/// Return the command encoder, fully reset and ready to be
/// reused.
pub(crate) unsafe fn land(mut self) -> A::CommandEncoder {
unsafe { self.raw.reset_all(self.cmd_buffers.into_iter()) };
self.raw
Expand Down
2 changes: 2 additions & 0 deletions wgpu-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
unused_braces,
// It gets in the way a lot and does not prevent bugs in practice.
clippy::pattern_type_mismatch,
// `wgpu-core` isn't entirely user-facing, so it's useful to document internal items.
rustdoc::private_intra_doc_links
)]
#![warn(
trivial_casts,
Expand Down
Loading

0 comments on commit a3bc86d

Please sign in to comment.