test: Additional "life cycle" tests

This commit is contained in:
Andy Leiserson
2026-05-22 12:18:02 -07:00
committed by Teodor Tanasoaia
parent fc1aa76bad
commit 8f4def424a

View File

@@ -1,5 +1,7 @@
use wgpu::util::DeviceExt;
use wgpu_test::{fail, gpu_test, GpuTestConfiguration, GpuTestInitializer, TestParameters};
use wgpu_test::{
fail, gpu_test, GpuTestConfiguration, GpuTestInitializer, TestParameters, TestingContext,
};
pub fn all_tests(vec: &mut Vec<GpuTestInitializer>) {
vec.extend([
@@ -7,6 +9,8 @@ pub fn all_tests(vec: &mut Vec<GpuTestInitializer>) {
TEXTURE_DESTROY,
BUFFER_DESTROY_BEFORE_SUBMIT,
TEXTURE_DESTROY_BEFORE_SUBMIT,
EXTERNAL_TEXTURE_DESTROY_BEFORE_SUBMIT,
REPLACED_BIND_GROUP,
]);
}
@@ -126,12 +130,195 @@ static TEXTURE_DESTROY: GpuTestConfiguration = GpuTestConfiguration::new()
texture.destroy();
});
// Test that destroying a buffer between command buffer recording and
// submission fails gracefully.
#[gpu_test]
static BUFFER_DESTROY_BEFORE_SUBMIT: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(TestParameters::default().enable_noop())
.run_sync(|ctx| {
#[derive(Copy, Clone)]
enum UsageKind {
Direct,
RenderPass,
ComputePass,
RenderBundle,
}
const BUFFER_RENDER_SHADER: &str = "\
@group(0) @binding(0) var<uniform> buf: vec4<f32>;
@vertex fn vs() -> @builtin(position) vec4<f32> { return buf; }
@fragment fn fs() -> @location(0) vec4<f32> { return vec4<f32>(0); }";
const BUFFER_COMPUTE_SHADER: &str = "\
@group(0) @binding(0) var<uniform> buf: vec4<f32>;
@compute @workgroup_size(1) fn main() { _ = buf; }";
const TEXTURE_RENDER_SHADER: &str = "\
@group(0) @binding(0) var tex: texture_2d<f32>;
@vertex fn vs() -> @builtin(position) vec4<f32> { return vec4<f32>(0); }
@fragment fn fs() -> @location(0) vec4<f32> { return textureLoad(tex, vec2(0), 0); }";
const TEXTURE_COMPUTE_SHADER: &str = "\
@group(0) @binding(0) var tex: texture_2d<f32>;
@compute @workgroup_size(1) fn main() { _ = textureLoad(tex, vec2(0), 0); }";
const EXTERNAL_TEXTURE_RENDER_SHADER: &str = "\
@group(0) @binding(0) var tex: texture_external;
@vertex fn vs() -> @builtin(position) vec4<f32> { return vec4<f32>(0); }
@fragment fn fs() -> @location(0) vec4<f32> { return textureLoad(tex, vec2(0)); }";
const EXTERNAL_TEXTURE_COMPUTE_SHADER: &str = "\
@group(0) @binding(0) var tex: texture_external;
@compute @workgroup_size(1) fn main() { _ = textureLoad(tex, vec2(0)); }";
fn create_render_target(device: &wgpu::Device) -> (wgpu::Texture, wgpu::TextureView) {
let texture = device.create_texture(&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::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
(texture, view)
}
fn create_render_pipeline(device: &wgpu::Device, shader_src: &str) -> wgpu::RenderPipeline {
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(shader_src.into()),
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: None,
vertex: wgpu::VertexState {
module: &module,
entry_point: None,
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: None,
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
}
fn create_compute_pipeline(device: &wgpu::Device, shader_src: &str) -> wgpu::ComputePipeline {
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(shader_src.into()),
});
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: None,
layout: None,
module: &module,
entry_point: None,
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
})
}
/// Records a bind group usage into an encoder and returns the encoder.
fn record_encoder_with_resource(
ctx: &TestingContext,
usage: UsageKind,
resource: wgpu::BindingResource<'_>,
render_shader: &str,
compute_shader: &str,
) -> wgpu::CommandEncoder {
let (_render_target, rt_view) = create_render_target(&ctx.device);
let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
match usage {
UsageKind::Direct => unreachable!(),
UsageKind::RenderPass | UsageKind::RenderBundle => {
let pipeline = create_render_pipeline(&ctx.device, render_shader);
let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &pipeline.get_bind_group_layout(0),
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource,
}],
});
let color_attachment = [Some(wgpu::RenderPassColorAttachment {
view: &rt_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})];
if matches!(usage, UsageKind::RenderPass) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &color_attachment,
..Default::default()
});
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..0, 0..0);
} else {
let mut rbe =
ctx.device
.create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
label: None,
color_formats: &[Some(wgpu::TextureFormat::Rgba8Unorm)],
depth_stencil: None,
sample_count: 1,
multiview: None,
});
rbe.set_pipeline(&pipeline);
rbe.set_bind_group(0, &bind_group, &[]);
rbe.draw(0..0, 0..0);
let bundle = rbe.finish(&wgpu::RenderBundleDescriptor::default());
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &color_attachment,
..Default::default()
});
pass.execute_bundles([&bundle]);
}
}
UsageKind::ComputePass => {
let pipeline = create_compute_pipeline(&ctx.device, compute_shader);
let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &pipeline.get_bind_group_layout(0),
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource,
}],
});
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.dispatch_workgroups(0, 0, 0);
}
}
encoder
}
fn test_buffer_destroy_before_submit(ctx: &TestingContext, usage: UsageKind) {
if matches!(usage, UsageKind::Direct) {
let buffer_source = ctx
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@@ -154,21 +341,55 @@ static BUFFER_DESTROY_BEFORE_SUBMIT: GpuTestConfiguration = GpuTestConfiguration
buffer_source.destroy();
buffer_dest.destroy();
let cmd_buffer = encoder.finish();
fail(
&ctx.device,
|| ctx.queue.submit([cmd_buffer]),
|| ctx.queue.submit([encoder.finish()]),
Some("Buffer with '' label has been destroyed"),
);
return;
}
let buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 16,
usage: wgpu::BufferUsages::UNIFORM,
mapped_at_creation: false,
});
// Test that destroying a texture between command buffer recording and
// submission fails gracefully.
let encoder = record_encoder_with_resource(
ctx,
usage,
buffer.as_entire_binding(),
BUFFER_RENDER_SHADER,
BUFFER_COMPUTE_SHADER,
);
buffer.destroy();
fail(
&ctx.device,
|| ctx.queue.submit([encoder.finish()]),
Some("Buffer with '' label has been destroyed"),
);
}
// Test that destroying a buffer between command encoding and submission fails gracefully.
#[gpu_test]
static TEXTURE_DESTROY_BEFORE_SUBMIT: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(TestParameters::default().enable_noop())
static BUFFER_DESTROY_BEFORE_SUBMIT: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(
TestParameters::default()
.test_features_limits()
.enable_noop(),
)
.run_sync(|ctx| {
test_buffer_destroy_before_submit(&ctx, UsageKind::Direct);
test_buffer_destroy_before_submit(&ctx, UsageKind::RenderPass);
test_buffer_destroy_before_submit(&ctx, UsageKind::ComputePass);
test_buffer_destroy_before_submit(&ctx, UsageKind::RenderBundle);
});
fn test_texture_destroy_before_submit(ctx: &TestingContext, usage: UsageKind) {
if matches!(usage, UsageKind::Direct) {
let descriptor = wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
@@ -177,12 +398,10 @@ static TEXTURE_DESTROY_BEFORE_SUBMIT: GpuTestConfiguration = GpuTestConfiguratio
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1, // multisampling is not supported for clear
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Snorm,
usage: wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::TEXTURE_BINDING,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
};
@@ -215,11 +434,262 @@ static TEXTURE_DESTROY_BEFORE_SUBMIT: GpuTestConfiguration = GpuTestConfiguratio
texture_1.destroy();
texture_2.destroy();
let cmd_buffer = encoder.finish();
fail(
&ctx.device,
|| ctx.queue.submit([cmd_buffer]),
|| ctx.queue.submit([encoder.finish()]),
Some("Texture with '' label has been destroyed"),
);
return;
}
let texture = ctx.device.create_texture(&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::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let encoder = record_encoder_with_resource(
ctx,
usage,
wgpu::BindingResource::TextureView(&view),
TEXTURE_RENDER_SHADER,
TEXTURE_COMPUTE_SHADER,
);
texture.destroy();
fail(
&ctx.device,
|| ctx.queue.submit([encoder.finish()]),
Some("Texture with '' label has been destroyed"),
);
}
// Test that destroying a texture between command encoding and submission fails gracefully.
#[gpu_test]
static TEXTURE_DESTROY_BEFORE_SUBMIT: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(
TestParameters::default()
.test_features_limits()
.enable_noop()
.features(wgpu::Features::CLEAR_TEXTURE),
)
.run_sync(|ctx| {
test_texture_destroy_before_submit(&ctx, UsageKind::Direct);
test_texture_destroy_before_submit(&ctx, UsageKind::RenderPass);
test_texture_destroy_before_submit(&ctx, UsageKind::ComputePass);
test_texture_destroy_before_submit(&ctx, UsageKind::RenderBundle);
});
fn test_external_texture_destroy_before_submit(ctx: &TestingContext, usage: UsageKind) {
let plane_texture = ctx.device.create_texture(&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::TEXTURE_BINDING,
view_formats: &[],
});
let external_texture = ctx.device.create_external_texture(
&wgpu::ExternalTextureDescriptor {
label: None,
width: 1,
height: 1,
format: wgpu::ExternalTextureFormat::Rgba,
yuv_conversion_matrix: [0.0; 16],
gamut_conversion_matrix: [0.0; 9],
src_transfer_function: Default::default(),
dst_transfer_function: Default::default(),
sample_transform: [0.0; 6],
load_transform: [0.0; 6],
},
&[&plane_texture.create_view(&wgpu::TextureViewDescriptor::default())],
);
let encoder = record_encoder_with_resource(
ctx,
usage,
wgpu::BindingResource::ExternalTexture(&external_texture),
EXTERNAL_TEXTURE_RENDER_SHADER,
EXTERNAL_TEXTURE_COMPUTE_SHADER,
);
plane_texture.destroy();
external_texture.destroy();
// External textures use a buffer and several textures internally. We consider which one
// triggers the error to be an implementation detail and match either.
fail(
&ctx.device,
|| ctx.queue.submit([encoder.finish()]),
Some("with '' label has been destroyed"),
);
}
// Test that destroying an external texture between command encoding and submission fails
// gracefully.
#[gpu_test]
static EXTERNAL_TEXTURE_DESTROY_BEFORE_SUBMIT: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(
TestParameters::default()
.test_features_limits()
.enable_noop()
.features(wgpu::Features::EXTERNAL_TEXTURE | wgpu::Features::CLEAR_TEXTURE),
)
.run_sync(|ctx| {
// UsageKind::Direct does not apply because external textures only support TEXTURE_BINDING.
test_external_texture_destroy_before_submit(&ctx, UsageKind::RenderPass);
test_external_texture_destroy_before_submit(&ctx, UsageKind::ComputePass);
test_external_texture_destroy_before_submit(&ctx, UsageKind::RenderBundle);
});
fn test_replaced_bind_group(ctx: &TestingContext, usage: UsageKind) {
let buffer_a = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 16,
usage: wgpu::BufferUsages::UNIFORM,
mapped_at_creation: false,
});
let buffer_b = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 16,
usage: wgpu::BufferUsages::UNIFORM,
mapped_at_creation: false,
});
let (_render_target, rt_view) = create_render_target(&ctx.device);
let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
match usage {
UsageKind::RenderPass | UsageKind::RenderBundle => {
let pipeline = create_render_pipeline(&ctx.device, BUFFER_RENDER_SHADER);
let layout = pipeline.get_bind_group_layout(0);
let bind_group_a = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer_a.as_entire_binding(),
}],
});
let bind_group_b = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer_b.as_entire_binding(),
}],
});
let color_attachment = [Some(wgpu::RenderPassColorAttachment {
view: &rt_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})];
if matches!(usage, UsageKind::RenderPass) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &color_attachment,
..Default::default()
});
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &bind_group_a, &[]);
pass.set_bind_group(0, &bind_group_b, &[]);
pass.draw(0..0, 0..0);
} else {
let mut rbe =
ctx.device
.create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
label: None,
color_formats: &[Some(wgpu::TextureFormat::Rgba8Unorm)],
depth_stencil: None,
sample_count: 1,
multiview: None,
});
rbe.set_pipeline(&pipeline);
rbe.set_bind_group(0, &bind_group_a, &[]);
rbe.set_bind_group(0, &bind_group_b, &[]);
rbe.draw(0..0, 0..0);
let bundle = rbe.finish(&wgpu::RenderBundleDescriptor::default());
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &color_attachment,
..Default::default()
});
pass.execute_bundles([&bundle]);
}
}
UsageKind::ComputePass => {
let pipeline = create_compute_pipeline(&ctx.device, BUFFER_COMPUTE_SHADER);
let layout = pipeline.get_bind_group_layout(0);
let bind_group_a = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer_a.as_entire_binding(),
}],
});
let bind_group_b = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer_b.as_entire_binding(),
}],
});
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &bind_group_a, &[]);
pass.set_bind_group(0, &bind_group_b, &[]);
pass.dispatch_workgroups(0, 0, 0);
}
UsageKind::Direct => unreachable!(),
}
buffer_a.destroy();
fail(
&ctx.device,
|| ctx.queue.submit([encoder.finish()]),
Some("Buffer with '' label has been destroyed"),
);
}
/// Test that bind groups that are replaced before use in a draw/dispatch are still
/// considered in submit-time liveness checks.
#[gpu_test]
static REPLACED_BIND_GROUP: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(
TestParameters::default()
.test_features_limits()
.enable_noop(),
)
.run_sync(|ctx| {
test_replaced_bind_group(&ctx, UsageKind::RenderPass);
test_replaced_bind_group(&ctx, UsageKind::ComputePass);
test_replaced_bind_group(&ctx, UsageKind::RenderBundle);
});