Skip to content

Commit 5889501

Browse files
authored
Remove lifetime dependency of ComputePass to its parent command encoder (#5620)
* lift encoder->computepass lifetime constraint and add now failing test * compute passes now take an arc to their parent command encoder, thus removing compile time dependency to it * Command encoder goes now into locked state while compute pass is open * changelog entry * share most of the code between get_encoder and lock_encoder
1 parent 071fb14 commit 5889501

File tree

12 files changed

+490
-88
lines changed

12 files changed

+490
-88
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,13 @@ TODO(wumpf): This is still work in progress. Should write a bit more about it. A
4747

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

50-
By @wumpf in [#5569](https://github.com/gfx-rs/wgpu/pull/5569), [#5575](https://github.com/gfx-rs/wgpu/pull/5575).
50+
Furthermore, `wgpu::ComputePass` no longer has a life time dependency on its parent `wgpu::CommandEncoder`.
51+
⚠️ 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`.
52+
Previously, this was statically enforced by a lifetime constraint.
53+
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`).
54+
Lifting this lifetime dependencies is very useful for library authors, but opens up an easy way for incorrect use.
55+
56+
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).
5157

5258
#### Querying shader compilation errors
5359

deno_webgpu/command_encoder.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,15 +261,14 @@ pub fn op_webgpu_command_encoder_begin_compute_pass(
261261
timestamp_writes: timestamp_writes.as_ref(),
262262
};
263263

264-
let compute_pass = gfx_select!(command_encoder => instance.command_encoder_create_compute_pass_dyn(*command_encoder, &descriptor));
265-
264+
let (compute_pass, error) = gfx_select!(command_encoder => instance.command_encoder_create_compute_pass_dyn(*command_encoder, &descriptor));
266265
let rid = state
267266
.resource_table
268267
.add(super::compute_pass::WebGpuComputePass(RefCell::new(
269268
compute_pass,
270269
)));
271270

272-
Ok(WebGpuResult::rid(rid))
271+
Ok(WebGpuResult::rid_err(rid, error))
273272
}
274273

275274
#[op2]

tests/tests/compute_pass_resource_ownership.rs renamed to tests/tests/compute_pass_ownership.rs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
//! Tests that compute passes take ownership of resources that are associated with.
22
//! I.e. once a resource is passed in to a compute pass, it can be dropped.
33
//!
4-
//! TODO: Test doesn't check on timestamp writes & pipeline statistics queries yet.
5-
//! (Not important as long as they are lifetime constrained to the command encoder,
6-
//! but once we lift this constraint, we should add tests for this as well!)
74
//! TODO: Also should test resource ownership for:
85
//! * write_timestamp
96
//! * begin_pipeline_statistics_query
107
118
use std::num::NonZeroU64;
129

1310
use wgpu::util::DeviceExt as _;
14-
use wgpu_test::{gpu_test, GpuTestConfiguration, TestParameters, TestingContext};
11+
use wgpu_test::{gpu_test, valid, GpuTestConfiguration, TestParameters, TestingContext};
1512

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

75+
#[gpu_test]
76+
static COMPUTE_PASS_KEEP_ENCODER_ALIVE: GpuTestConfiguration = GpuTestConfiguration::new()
77+
.parameters(TestParameters::default().test_features_limits())
78+
.run_async(compute_pass_keep_encoder_alive);
79+
80+
async fn compute_pass_keep_encoder_alive(ctx: TestingContext) {
81+
let ResourceSetup {
82+
gpu_buffer: _,
83+
cpu_buffer: _,
84+
buffer_size: _,
85+
indirect_buffer,
86+
bind_group,
87+
pipeline,
88+
} = resource_setup(&ctx);
89+
90+
let mut encoder = ctx
91+
.device
92+
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
93+
label: Some("encoder"),
94+
});
95+
96+
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
97+
label: Some("compute_pass"),
98+
timestamp_writes: None,
99+
});
100+
101+
// Now drop the encoder - it is kept alive by the compute pass.
102+
drop(encoder);
103+
ctx.async_poll(wgpu::Maintain::wait())
104+
.await
105+
.panic_on_timeout();
106+
107+
// Record some draw commands.
108+
cpass.set_pipeline(&pipeline);
109+
cpass.set_bind_group(0, &bind_group, &[]);
110+
cpass.dispatch_workgroups_indirect(&indirect_buffer, 0);
111+
112+
// Dropping the pass will still execute the pass, even though there's no way to submit it.
113+
// Ideally, this would log an error, but the encoder is not dropped until the compute pass is dropped,
114+
// making this a valid operation.
115+
// (If instead the encoder was explicitly destroyed or finished, this would be an error.)
116+
valid(&ctx.device, || drop(cpass));
117+
}
118+
78119
// Setup ------------------------------------------------------------
79120

80121
struct ResourceSetup {

tests/tests/encoder.rs

Lines changed: 229 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use wgpu_test::{fail, gpu_test, FailureCase, GpuTestConfiguration, TestParameters};
1+
use wgpu::util::DeviceExt;
2+
use wgpu::CommandEncoder;
3+
use wgpu_test::{
4+
fail, gpu_test, FailureCase, GpuTestConfiguration, TestParameters, TestingContext,
5+
};
26

37
#[gpu_test]
48
static DROP_ENCODER: GpuTestConfiguration = GpuTestConfiguration::new().run_sync(|ctx| {
@@ -72,3 +76,227 @@ static DROP_ENCODER_AFTER_ERROR: GpuTestConfiguration = GpuTestConfiguration::ne
7276
// The encoder is still open!
7377
drop(encoder);
7478
});
79+
80+
// TODO: This should also apply to render passes once the lifetime bound is lifted.
81+
#[gpu_test]
82+
static ENCODER_OPERATIONS_FAIL_WHILE_COMPUTE_PASS_ALIVE: GpuTestConfiguration =
83+
GpuTestConfiguration::new()
84+
.parameters(TestParameters::default().features(
85+
wgpu::Features::CLEAR_TEXTURE
86+
| wgpu::Features::TIMESTAMP_QUERY
87+
| wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS,
88+
))
89+
.run_sync(encoder_operations_fail_while_compute_pass_alive);
90+
91+
fn encoder_operations_fail_while_compute_pass_alive(ctx: TestingContext) {
92+
let buffer_source = ctx
93+
.device
94+
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
95+
label: None,
96+
contents: &[0u8; 4],
97+
usage: wgpu::BufferUsages::COPY_SRC,
98+
});
99+
let buffer_dest = ctx
100+
.device
101+
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
102+
label: None,
103+
contents: &[0u8; 4],
104+
usage: wgpu::BufferUsages::COPY_DST,
105+
});
106+
107+
let texture_desc = wgpu::TextureDescriptor {
108+
label: None,
109+
size: wgpu::Extent3d {
110+
width: 1,
111+
height: 1,
112+
depth_or_array_layers: 1,
113+
},
114+
mip_level_count: 1,
115+
sample_count: 1,
116+
dimension: wgpu::TextureDimension::D2,
117+
format: wgpu::TextureFormat::Rgba8Unorm,
118+
usage: wgpu::TextureUsages::COPY_DST,
119+
view_formats: &[],
120+
};
121+
let texture_dst = ctx.device.create_texture(&texture_desc);
122+
let texture_src = ctx.device.create_texture(&wgpu::TextureDescriptor {
123+
usage: wgpu::TextureUsages::COPY_SRC,
124+
..texture_desc
125+
});
126+
let query_set = ctx.device.create_query_set(&wgpu::QuerySetDescriptor {
127+
count: 1,
128+
ty: wgpu::QueryType::Timestamp,
129+
label: None,
130+
});
131+
132+
#[allow(clippy::type_complexity)]
133+
let recording_ops: Vec<(_, Box<dyn Fn(&mut CommandEncoder)>)> = vec![
134+
(
135+
"begin_compute_pass",
136+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
137+
encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
138+
}),
139+
),
140+
(
141+
"begin_render_pass",
142+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
143+
encoder.begin_render_pass(&wgpu::RenderPassDescriptor::default());
144+
}),
145+
),
146+
(
147+
"copy_buffer_to_buffer",
148+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
149+
encoder.copy_buffer_to_buffer(&buffer_source, 0, &buffer_dest, 0, 4);
150+
}),
151+
),
152+
(
153+
"copy_buffer_to_texture",
154+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
155+
encoder.copy_buffer_to_texture(
156+
wgpu::ImageCopyBuffer {
157+
buffer: &buffer_source,
158+
layout: wgpu::ImageDataLayout {
159+
offset: 0,
160+
bytes_per_row: Some(4),
161+
rows_per_image: None,
162+
},
163+
},
164+
texture_dst.as_image_copy(),
165+
texture_dst.size(),
166+
);
167+
}),
168+
),
169+
(
170+
"copy_texture_to_buffer",
171+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
172+
encoder.copy_texture_to_buffer(
173+
wgpu::ImageCopyTexture {
174+
texture: &texture_src,
175+
mip_level: 0,
176+
origin: wgpu::Origin3d::ZERO,
177+
aspect: wgpu::TextureAspect::All,
178+
},
179+
wgpu::ImageCopyBuffer {
180+
buffer: &buffer_dest,
181+
layout: wgpu::ImageDataLayout {
182+
offset: 0,
183+
bytes_per_row: Some(4),
184+
rows_per_image: None,
185+
},
186+
},
187+
texture_dst.size(),
188+
);
189+
}),
190+
),
191+
(
192+
"copy_texture_to_texture",
193+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
194+
encoder.copy_texture_to_texture(
195+
wgpu::ImageCopyTexture {
196+
texture: &texture_src,
197+
mip_level: 0,
198+
origin: wgpu::Origin3d::ZERO,
199+
aspect: wgpu::TextureAspect::All,
200+
},
201+
wgpu::ImageCopyTexture {
202+
texture: &texture_dst,
203+
mip_level: 0,
204+
origin: wgpu::Origin3d::ZERO,
205+
aspect: wgpu::TextureAspect::All,
206+
},
207+
texture_dst.size(),
208+
);
209+
}),
210+
),
211+
(
212+
"clear_texture",
213+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
214+
encoder.clear_texture(&texture_dst, &wgpu::ImageSubresourceRange::default());
215+
}),
216+
),
217+
(
218+
"clear_buffer",
219+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
220+
encoder.clear_buffer(&buffer_dest, 0, None);
221+
}),
222+
),
223+
(
224+
"insert_debug_marker",
225+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
226+
encoder.insert_debug_marker("marker");
227+
}),
228+
),
229+
(
230+
"push_debug_group",
231+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
232+
encoder.push_debug_group("marker");
233+
}),
234+
),
235+
(
236+
"pop_debug_group",
237+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
238+
encoder.pop_debug_group();
239+
}),
240+
),
241+
(
242+
"resolve_query_set",
243+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
244+
encoder.resolve_query_set(&query_set, 0..1, &buffer_dest, 0);
245+
}),
246+
),
247+
(
248+
"write_timestamp",
249+
Box::new(|encoder: &mut wgpu::CommandEncoder| {
250+
encoder.write_timestamp(&query_set, 0);
251+
}),
252+
),
253+
];
254+
255+
for (op_name, op) in recording_ops.iter() {
256+
let mut encoder = ctx
257+
.device
258+
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
259+
260+
let pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
261+
262+
ctx.device.push_error_scope(wgpu::ErrorFilter::Validation);
263+
264+
log::info!("Testing operation {} on a locked command encoder", op_name);
265+
fail(
266+
&ctx.device,
267+
|| op(&mut encoder),
268+
Some("Command encoder is locked"),
269+
);
270+
271+
// Drop the pass - this also fails now since the encoder is invalid:
272+
fail(
273+
&ctx.device,
274+
|| drop(pass),
275+
Some("Command encoder is invalid"),
276+
);
277+
// Also, it's not possible to create a new pass on the encoder:
278+
fail(
279+
&ctx.device,
280+
|| encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default()),
281+
Some("Command encoder is invalid"),
282+
);
283+
}
284+
285+
// Test encoder finishing separately since it consumes the encoder and doesn't fit above pattern.
286+
{
287+
let mut encoder = ctx
288+
.device
289+
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
290+
let pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
291+
fail(
292+
&ctx.device,
293+
|| encoder.finish(),
294+
Some("Command encoder is locked"),
295+
);
296+
fail(
297+
&ctx.device,
298+
|| drop(pass),
299+
Some("Command encoder is invalid"),
300+
);
301+
}
302+
}

tests/tests/root.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ mod buffer;
1111
mod buffer_copy;
1212
mod buffer_usages;
1313
mod clear_texture;
14-
mod compute_pass_resource_ownership;
14+
mod compute_pass_ownership;
1515
mod create_surface_error;
1616
mod device;
1717
mod encoder;

wgpu-core/src/command/clear.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ use wgt::{math::align_to, BufferAddress, BufferUsages, ImageSubresourceRange, Te
2626
pub enum ClearError {
2727
#[error("To use clear_texture the CLEAR_TEXTURE feature needs to be enabled")]
2828
MissingClearTextureFeature,
29-
#[error("Command encoder {0:?} is invalid")]
30-
InvalidCommandEncoder(CommandEncoderId),
3129
#[error("Device {0:?} is invalid")]
3230
InvalidDevice(DeviceId),
3331
#[error("Buffer {0:?} is invalid or destroyed")]
@@ -74,6 +72,8 @@ whereas subesource range specified start {subresource_base_array_layer} and coun
7472
},
7573
#[error(transparent)]
7674
Device(#[from] DeviceError),
75+
#[error(transparent)]
76+
CommandEncoderError(#[from] super::CommandEncoderError),
7777
}
7878

7979
impl Global {
@@ -89,8 +89,7 @@ impl Global {
8989

9090
let hub = A::hub(self);
9191

92-
let cmd_buf = CommandBuffer::get_encoder(hub, command_encoder_id)
93-
.map_err(|_| ClearError::InvalidCommandEncoder(command_encoder_id))?;
92+
let cmd_buf = CommandBuffer::get_encoder(hub, command_encoder_id)?;
9493
let mut cmd_buf_data = cmd_buf.data.lock();
9594
let cmd_buf_data = cmd_buf_data.as_mut().unwrap();
9695

@@ -183,8 +182,7 @@ impl Global {
183182

184183
let hub = A::hub(self);
185184

186-
let cmd_buf = CommandBuffer::get_encoder(hub, command_encoder_id)
187-
.map_err(|_| ClearError::InvalidCommandEncoder(command_encoder_id))?;
185+
let cmd_buf = CommandBuffer::get_encoder(hub, command_encoder_id)?;
188186
let mut cmd_buf_data = cmd_buf.data.lock();
189187
let cmd_buf_data = cmd_buf_data.as_mut().unwrap();
190188

0 commit comments

Comments
 (0)