Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Perform "valid to use with" checks when recording render bundles. #2690

Merged
merged 2 commits into from
May 28, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 87 additions & 25 deletions wgpu-core/src/command/bundle.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,72 @@
/*! Render Bundles

## Software implementation
A render bundle is a prerecorded sequence of commands that can be replayed on a
command encoder with a single call. A single bundle can replayed any number of
times, on different encoders. Constructing a render bundle lets `wgpu` validate
and analyze its commands up front, so that replaying a bundle can be more
efficient than simply re-recording its commands each time.

The path from nothing to using a render bundle consists of 3 phases.
One important property of render bundles is that the draw calls in a render
bundle depend solely on the pipeline and state established within the render
bundle itself. A draw call in a bundle will never use a vertex buffer, say, that
was set in the `RenderPass` before executing the bundle. We call this property
'isolation', in that a render bundle is somewhat isolated from the passes that
use it.

### Initial command encoding
Render passes are also isolated from the effects of bundles. After executing a
render bundle, a render pass's pipeline, bind groups, and vertex and index
buffers are are unset, so the bundle cannot affect later draw calls in the pass.

User creates a `RenderBundleEncoder` and populates it by issuing commands
from `bundle_ffi` module, just like with `RenderPass`, except that the
set of available commands is reduced. Everything is written into a `RawPass`.
Not all commands are available in bundles; for example, a render bundle may not
contain a [`RenderCommand::SetViewport`] command.

### Bundle baking
Most of `wgpu`'s backend graphics APIs have something like bundles. For example,
Vulkan calls them "secondary command buffers", and Metal calls them "indirect
command buffers". However, `wgpu`'s implementation of render bundles does not
take advantage of those underlying platform features. At the hal level, `wgpu`
render bundles just replay the commands.

Once the commands are encoded, user calls `render_bundle_encoder_finish`.
This is perhaps the most complex part of the logic. It consumes the
commands stored in `RawPass`, while validating everything, tracking the state,
and re-recording the commands into a separate `Vec<RenderCommand>`. It
doesn't actually execute any commands.
## Render Bundle Lifecycle

What's more important, is that the produced vector of commands is "normalized",
which means it can be executed verbatim without any state tracking. More
formally, "normalized" command stream guarantees that any state required by
a draw call is set explicitly by one of the commands between the draw call
and the last changing of the pipeline.
To create a render bundle:

1) Create a [`RenderBundleEncoder`] by calling
[`Global::device_create_render_bundle_encoder`][Gdcrbe].

2) Record commands in the `RenderBundleEncoder` using functions from the
[`bundle_ffi`] module.

3) Call [`Global::render_bundle_encoder_finish`][Grbef], which analyzes and cleans up
the command stream and returns a `RenderBundleId`.

4) Then, any number of times, call [`wgpu_render_pass_execute_bundles`][wrpeb] to
execute the bundle as part of some render pass.

## Implementation

The most complex part of render bundles is the "finish" step, mostly implemented
in [`RenderBundleEncoder::finish`]. This consumes the commands stored in the
encoder's [`BasePass`], while validating everything, tracking the state,
dropping redundant or unnecessary commands, and presenting the results as a new
[`RenderBundle`]. It doesn't actually execute any commands.

This step also enforces the 'isolation' property mentioned above: every draw
call is checked to ensure that the resources it uses on were established since
the last time the pipeline was set. This means the bundle can be executed
verbatim without any state tracking.

### Execution

When the bundle is used in an actual render pass, `RenderBundle::execute` is
called. It goes through the commands and issues them into the native command
buffer. Thanks to the "normalized" property, it doesn't track any bind group
invalidations or index format changes.
buffer. Thanks to isolation, it doesn't track any bind group invalidations or
index format changes.

[Gdcrbe]: crate::hub::Global::device_create_render_bundle_encoder
[Grbef]: crate::hub::Global::render_bundle_encoder_finish
[wrpeb]: crate::command::render_ffi::wgpu_render_pass_execute_bundles
!*/

#![allow(clippy::reversed_empty_ranges)]

use crate::{
Expand Down Expand Up @@ -227,6 +263,15 @@ impl RenderBundleEncoder {
} => {
let scope = PassErrorScope::SetBindGroup(bind_group_id);

let bind_group: &binding_model::BindGroup<A> = state
.trackers
.bind_groups
.add_single(&*bind_group_guard, bind_group_id)
.ok_or(RenderCommandError::InvalidBindGroup(bind_group_id))
.map_pass_err(scope)?;
self.check_valid_to_use(bind_group.device_id.value)
.map_pass_err(scope)?;

let max_bind_groups = device.limits.max_bind_groups;
if (index as u32) >= max_bind_groups {
return Err(RenderCommandError::BindGroupIndexOutOfRange {
Expand All @@ -243,12 +288,6 @@ impl RenderBundleEncoder {
next_dynamic_offset = offsets_range.end;
let offsets = &base.dynamic_offsets[offsets_range.clone()];

let bind_group: &binding_model::BindGroup<A> = state
.trackers
.bind_groups
.add_single(&*bind_group_guard, bind_group_id)
.ok_or(RenderCommandError::InvalidBindGroup(bind_group_id))
.map_pass_err(scope)?;
if bind_group.dynamic_binding_info.len() != offsets.len() {
return Err(RenderCommandError::InvalidDynamicOffsetCount {
actual: offsets.len(),
Expand Down Expand Up @@ -297,6 +336,8 @@ impl RenderBundleEncoder {
.add_single(&*pipeline_guard, pipeline_id)
.ok_or(RenderCommandError::InvalidPipeline(pipeline_id))
.map_pass_err(scope)?;
self.check_valid_to_use(pipeline.device_id.value)
.map_pass_err(scope)?;

self.context
.check_compatible(&pipeline.pass_context)
Expand Down Expand Up @@ -336,6 +377,8 @@ impl RenderBundleEncoder {
.buffers
.merge_single(&*buffer_guard, buffer_id, hal::BufferUses::INDEX)
.map_pass_err(scope)?;
self.check_valid_to_use(buffer.device_id.value)
.map_pass_err(scope)?;
check_buffer_usage(buffer.usage, wgt::BufferUsages::INDEX)
.map_pass_err(scope)?;

Expand Down Expand Up @@ -363,6 +406,8 @@ impl RenderBundleEncoder {
.buffers
.merge_single(&*buffer_guard, buffer_id, hal::BufferUses::VERTEX)
.map_pass_err(scope)?;
self.check_valid_to_use(buffer.device_id.value)
.map_pass_err(scope)?;
check_buffer_usage(buffer.usage, wgt::BufferUsages::VERTEX)
.map_pass_err(scope)?;

Expand Down Expand Up @@ -488,6 +533,8 @@ impl RenderBundleEncoder {
.buffers
.merge_single(&*buffer_guard, buffer_id, hal::BufferUses::INDIRECT)
.map_pass_err(scope)?;
self.check_valid_to_use(buffer.device_id.value)
.map_pass_err(scope)?;
check_buffer_usage(buffer.usage, wgt::BufferUsages::INDIRECT)
.map_pass_err(scope)?;

Expand Down Expand Up @@ -521,6 +568,8 @@ impl RenderBundleEncoder {
.buffers
.merge_single(&*buffer_guard, buffer_id, hal::BufferUses::INDIRECT)
.map_pass_err(scope)?;
self.check_valid_to_use(buffer.device_id.value)
.map_pass_err(scope)?;
check_buffer_usage(buffer.usage, wgt::BufferUsages::INDIRECT)
.map_pass_err(scope)?;

Expand Down Expand Up @@ -572,6 +621,17 @@ impl RenderBundleEncoder {
})
}

fn check_valid_to_use(
&self,
device_id: id::Valid<id::DeviceId>,
) -> Result<(), RenderBundleErrorInner> {
if device_id.0 != self.parent_id {
return Err(RenderBundleErrorInner::NotValidToUse);
}

Ok(())
}

pub fn set_index_buffer(
&mut self,
buffer_id: id::BufferId,
Expand Down Expand Up @@ -1238,6 +1298,8 @@ impl<A: HalApi> State<A> {
/// Error encountered when finishing recording a render bundle.
#[derive(Clone, Debug, Error)]
pub(super) enum RenderBundleErrorInner {
#[error("resource is not valid to use with this render bundle because the resource and the bundle come from different devices")]
NotValidToUse,
#[error(transparent)]
Device(#[from] DeviceError),
#[error(transparent)]
Expand Down