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

Remove lifetime dependency of ComputePass to its parent command encoder #5620

Merged
merged 5 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ TODO(wumpf): This is still work in progress. Should write a bit more about it. A

`wgpu::ComputePass` recording methods (e.g. `wgpu::ComputePass:set_render_pipeline`) no longer impose a lifetime constraint passed in resources.

By @wumpf in [#5569](https://github.com/gfx-rs/wgpu/pull/5569), [#5575](https://github.com/gfx-rs/wgpu/pull/5575).
Furthermore, `wgpu::ComputePass` no longer has a life time dependency on its parent `wgpu::CommandEncoder`.
⚠️ As long as a `wgpu::ComputePass` is pending for a given `wgpu::CommandEncoder`, creation of a compute or render pass is an error and invalidates the `wgpu::CommandEncoder`.
Previously, this was statically enforced by a lifetime constraint.
TODO(wumpf): There was some discussion on whether to make this life time constraint opt-in or opt-out (entirely on `wgpu` side, no changes to `wgpu-core`).
Lifting this lifetime dependencies is very useful for library authors, but opens up an easy way for incorrect use.

By @wumpf in [#5569](https://github.com/gfx-rs/wgpu/pull/5569), [#5575](https://github.com/gfx-rs/wgpu/pull/5575), [#5620](https://github.com/gfx-rs/wgpu/pull/5620).

#### Querying shader compilation errors

Expand Down
5 changes: 2 additions & 3 deletions deno_webgpu/command_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,15 +261,14 @@ pub fn op_webgpu_command_encoder_begin_compute_pass(
timestamp_writes: timestamp_writes.as_ref(),
};

let compute_pass = gfx_select!(command_encoder => instance.command_encoder_create_compute_pass_dyn(*command_encoder, &descriptor));

let (compute_pass, error) = gfx_select!(command_encoder => instance.command_encoder_create_compute_pass_dyn(*command_encoder, &descriptor));
let rid = state
.resource_table
.add(super::compute_pass::WebGpuComputePass(RefCell::new(
compute_pass,
)));

Ok(WebGpuResult::rid(rid))
Ok(WebGpuResult::rid_err(rid, error))
}

#[op2]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
//! Tests that compute passes take ownership of resources that are associated with.
//! I.e. once a resource is passed in to a compute pass, it can be dropped.
//!
//! TODO: Test doesn't check on timestamp writes & pipeline statistics queries yet.
//! (Not important as long as they are lifetime constrained to the command encoder,
//! but once we lift this constraint, we should add tests for this as well!)
//! TODO: Also should test resource ownership for:
//! * write_timestamp
//! * begin_pipeline_statistics_query

use std::num::NonZeroU64;

use wgpu::util::DeviceExt as _;
use wgpu_test::{gpu_test, GpuTestConfiguration, TestParameters, TestingContext};
use wgpu_test::{gpu_test, valid, GpuTestConfiguration, TestParameters, TestingContext};

const SHADER_SRC: &str = "
@group(0) @binding(0)
Expand Down Expand Up @@ -75,6 +72,50 @@ async fn compute_pass_resource_ownership(ctx: TestingContext) {
assert_eq!(floats, [2.0, 4.0, 6.0, 8.0]);
}

#[gpu_test]
static COMPUTE_PASS_KEEP_ENCODER_ALIVE: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(TestParameters::default().test_features_limits())
.run_async(compute_pass_keep_encoder_alive);

async fn compute_pass_keep_encoder_alive(ctx: TestingContext) {
let ResourceSetup {
gpu_buffer: _,
cpu_buffer: _,
buffer_size: _,
indirect_buffer,
bind_group,
pipeline,
} = resource_setup(&ctx);

let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("encoder"),
});

let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("compute_pass"),
timestamp_writes: None,
});

// Now drop the encoder - it is kept alive by the compute pass.
drop(encoder);
ctx.async_poll(wgpu::Maintain::wait())
.await
.panic_on_timeout();

// Record some draw commands.
cpass.set_pipeline(&pipeline);
cpass.set_bind_group(0, &bind_group, &[]);
cpass.dispatch_workgroups_indirect(&indirect_buffer, 0);

// Dropping the pass will still execute the pass, even though there's no way to submit it.
// Ideally, this would log an error, but the encoder is not dropped until the compute pass is dropped,
// making this a valid operation.
// (If instead the encoder was explicitly destroyed or finished, this would be an error.)
valid(&ctx.device, || drop(cpass));
}

// Setup ------------------------------------------------------------

struct ResourceSetup {
Expand Down
230 changes: 229 additions & 1 deletion tests/tests/encoder.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use wgpu_test::{fail, gpu_test, FailureCase, GpuTestConfiguration, TestParameters};
use wgpu::util::DeviceExt;
use wgpu::CommandEncoder;
use wgpu_test::{
fail, gpu_test, FailureCase, GpuTestConfiguration, TestParameters, TestingContext,
};

#[gpu_test]
static DROP_ENCODER: GpuTestConfiguration = GpuTestConfiguration::new().run_sync(|ctx| {
Expand Down Expand Up @@ -72,3 +76,227 @@ static DROP_ENCODER_AFTER_ERROR: GpuTestConfiguration = GpuTestConfiguration::ne
// The encoder is still open!
drop(encoder);
});

// TODO: This should also apply to render passes once the lifetime bound is lifted.
#[gpu_test]
static ENCODER_OPERATIONS_FAIL_WHILE_COMPUTE_PASS_ALIVE: GpuTestConfiguration =
GpuTestConfiguration::new()
.parameters(TestParameters::default().features(
wgpu::Features::CLEAR_TEXTURE
| wgpu::Features::TIMESTAMP_QUERY
| wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS,
))
.run_sync(encoder_operations_fail_while_compute_pass_alive);

fn encoder_operations_fail_while_compute_pass_alive(ctx: TestingContext) {
let buffer_source = ctx
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: &[0u8; 4],
usage: wgpu::BufferUsages::COPY_SRC,
});
let buffer_dest = ctx
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: &[0u8; 4],
usage: wgpu::BufferUsages::COPY_DST,
});

let texture_desc = wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::COPY_DST,
view_formats: &[],
};
let texture_dst = ctx.device.create_texture(&texture_desc);
let texture_src = ctx.device.create_texture(&wgpu::TextureDescriptor {
usage: wgpu::TextureUsages::COPY_SRC,
..texture_desc
});
let query_set = ctx.device.create_query_set(&wgpu::QuerySetDescriptor {
count: 1,
ty: wgpu::QueryType::Timestamp,
label: None,
});

#[allow(clippy::type_complexity)]
let recording_ops: Vec<(_, Box<dyn Fn(&mut CommandEncoder)>)> = vec![
(
"begin_compute_pass",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
}),
),
(
"begin_render_pass",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.begin_render_pass(&wgpu::RenderPassDescriptor::default());
}),
),
(
"copy_buffer_to_buffer",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.copy_buffer_to_buffer(&buffer_source, 0, &buffer_dest, 0, 4);
}),
),
(
"copy_buffer_to_texture",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.copy_buffer_to_texture(
wgpu::ImageCopyBuffer {
buffer: &buffer_source,
layout: wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4),
rows_per_image: None,
},
},
texture_dst.as_image_copy(),
texture_dst.size(),
);
}),
),
(
"copy_texture_to_buffer",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.copy_texture_to_buffer(
wgpu::ImageCopyTexture {
texture: &texture_src,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::ImageCopyBuffer {
buffer: &buffer_dest,
layout: wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4),
rows_per_image: None,
},
},
texture_dst.size(),
);
}),
),
(
"copy_texture_to_texture",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.copy_texture_to_texture(
wgpu::ImageCopyTexture {
texture: &texture_src,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::ImageCopyTexture {
texture: &texture_dst,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
texture_dst.size(),
);
}),
),
(
"clear_texture",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.clear_texture(&texture_dst, &wgpu::ImageSubresourceRange::default());
}),
),
(
"clear_buffer",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.clear_buffer(&buffer_dest, 0, None);
}),
),
(
"insert_debug_marker",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.insert_debug_marker("marker");
}),
),
(
"push_debug_group",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.push_debug_group("marker");
}),
),
(
"pop_debug_group",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.pop_debug_group();
}),
),
(
"resolve_query_set",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.resolve_query_set(&query_set, 0..1, &buffer_dest, 0);
}),
),
(
"write_timestamp",
Box::new(|encoder: &mut wgpu::CommandEncoder| {
encoder.write_timestamp(&query_set, 0);
}),
),
];

for (op_name, op) in recording_ops.iter() {
let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());

let pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());

ctx.device.push_error_scope(wgpu::ErrorFilter::Validation);

log::info!("Testing operation {} on a locked command encoder", op_name);
fail(
&ctx.device,
|| op(&mut encoder),
Some("Command encoder is locked"),
);

// Drop the pass - this also fails now since the encoder is invalid:
fail(
&ctx.device,
|| drop(pass),
Some("Command encoder is invalid"),
);
// Also, it's not possible to create a new pass on the encoder:
fail(
&ctx.device,
|| encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default()),
Some("Command encoder is invalid"),
);
}

// Test encoder finishing separately since it consumes the encoder and doesn't fit above pattern.
{
let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
let pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
fail(
&ctx.device,
|| encoder.finish(),
Some("Command encoder is locked"),
);
fail(
&ctx.device,
|| drop(pass),
Some("Command encoder is invalid"),
);
}
}
2 changes: 1 addition & 1 deletion tests/tests/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ mod buffer;
mod buffer_copy;
mod buffer_usages;
mod clear_texture;
mod compute_pass_resource_ownership;
mod compute_pass_ownership;
mod create_surface_error;
mod device;
mod encoder;
Expand Down
10 changes: 4 additions & 6 deletions wgpu-core/src/command/clear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ use wgt::{math::align_to, BufferAddress, BufferUsages, ImageSubresourceRange, Te
pub enum ClearError {
#[error("To use clear_texture the CLEAR_TEXTURE feature needs to be enabled")]
MissingClearTextureFeature,
#[error("Command encoder {0:?} is invalid")]
InvalidCommandEncoder(CommandEncoderId),
#[error("Device {0:?} is invalid")]
InvalidDevice(DeviceId),
#[error("Buffer {0:?} is invalid or destroyed")]
Expand Down Expand Up @@ -74,6 +72,8 @@ whereas subesource range specified start {subresource_base_array_layer} and coun
},
#[error(transparent)]
Device(#[from] DeviceError),
#[error(transparent)]
CommandEncoderError(#[from] super::CommandEncoderError),
}

impl Global {
Expand All @@ -89,8 +89,7 @@ impl Global {

let hub = A::hub(self);

let cmd_buf = CommandBuffer::get_encoder(hub, command_encoder_id)
.map_err(|_| ClearError::InvalidCommandEncoder(command_encoder_id))?;
let cmd_buf = CommandBuffer::get_encoder(hub, command_encoder_id)?;
let mut cmd_buf_data = cmd_buf.data.lock();
let cmd_buf_data = cmd_buf_data.as_mut().unwrap();

Expand Down Expand Up @@ -183,8 +182,7 @@ impl Global {

let hub = A::hub(self);

let cmd_buf = CommandBuffer::get_encoder(hub, command_encoder_id)
.map_err(|_| ClearError::InvalidCommandEncoder(command_encoder_id))?;
let cmd_buf = CommandBuffer::get_encoder(hub, command_encoder_id)?;
let mut cmd_buf_data = cmd_buf.data.lock();
let cmd_buf_data = cmd_buf_data.as_mut().unwrap();

Expand Down