wgpu/tests/gpu-tests/device.rs
Kevin Reid 326ad03ce1 Move trace_dir/trace_path to a custom enum inside DeviceDescriptor.
This allows `wgpu` to not unconditionally depend on `std::path::Path`.
It’s also, in my opinion, more user-friendly, because the feature which
most users will not use (and is not currently functional) is now a
defaultable struct field instead of a required parameter.

The disadvantage is that `wgpu-types` now has to know about tracing.
2025-03-10 22:17:06 -04:00

829 lines
30 KiB
Rust

use std::sync::atomic::AtomicBool;
use wgpu_test::{
fail, gpu_test, FailureCase, GpuTestConfiguration, TestParameters, TestingContext,
};
#[gpu_test]
static CROSS_DEVICE_BIND_GROUP_USAGE: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(TestParameters::default().expect_fail(FailureCase::always()))
.run_async(|ctx| async move {
// Create a bind group using a layout from another device. This should be a validation
// error but currently crashes.
let (device2, _) =
pollster::block_on(ctx.adapter.request_device(&Default::default())).unwrap();
{
let bind_group_layout =
device2.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[],
});
let _bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &bind_group_layout,
entries: &[],
});
}
ctx.async_poll(wgpu::PollType::Poll).await.unwrap();
});
#[cfg(not(all(target_arch = "wasm32", not(target_os = "emscripten"))))]
#[gpu_test]
static DEVICE_LIFETIME_CHECK: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(TestParameters::default())
.run_sync(|ctx| {
ctx.instance.poll_all(false);
let pre_report = ctx.instance.generate_report().unwrap();
let TestingContext {
instance,
device,
queue,
..
} = ctx;
drop(queue);
drop(device);
let post_report = instance.generate_report().unwrap();
assert_ne!(
pre_report, post_report,
"Queue and Device has not been dropped as expected"
);
});
#[cfg(not(all(target_arch = "wasm32", not(target_os = "emscripten"))))]
#[gpu_test]
static MULTIPLE_DEVICES: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(TestParameters::default())
.run_sync(|ctx| {
use pollster::FutureExt as _;
ctx.adapter
.request_device(&wgpu::DeviceDescriptor::default())
.block_on()
.expect("failed to create device");
ctx.adapter
.request_device(&wgpu::DeviceDescriptor::default())
.block_on()
.expect("failed to create device");
});
#[cfg(not(all(target_arch = "wasm32", not(target_os = "emscripten"))))]
#[gpu_test]
static REQUEST_DEVICE_ERROR_MESSAGE_NATIVE: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters({
let default = TestParameters::default();
// On CI, we have the vulkan SDK installed and this test initializes the Vulkan backend when all
// normal tests do not, so we get these weird error messages. This only actually gets hooked up
// on Windows, because that's the only platform where the actual runtime gets hooked up and there
// are no drivers.
if std::env::var("WGPU_CI").is_ok() && cfg!(windows) {
default.expect_fail(
FailureCase::always()
.validation_error("Registry lookup failed to get ICD manifest files. Possibly missing Vulkan driver?")
.validation_error("vkCreateInstance: Found no drivers!")
)
} else {
default
}
})
.run_async(|_ctx| request_device_error_message());
/// Check that `RequestDeviceError`s produced have some diagnostic information.
///
/// Note: this is a wasm *and* native test. On wasm it is run directly; on native, indirectly
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
async fn request_device_error_message() {
// Not using initialize_test() because that doesn't let us catch the error
// nor .await anything
let (_instance, adapter, _surface_guard) = wgpu_test::initialize_adapter(None, false).await;
let device_error = adapter
.request_device(&wgpu::DeviceDescriptor {
// Force a failure by requesting absurd limits.
required_features: wgpu::Features::all(),
required_limits: wgpu::Limits {
max_texture_dimension_1d: u32::MAX,
max_texture_dimension_2d: u32::MAX,
max_texture_dimension_3d: u32::MAX,
max_bind_groups: u32::MAX,
max_push_constant_size: u32::MAX,
..Default::default()
},
..Default::default()
})
.await
.unwrap_err();
let device_error = device_error.to_string();
cfg_if::cfg_if! {
if #[cfg(all(target_arch = "wasm32", not(feature = "webgl")))] {
// On WebGPU, so the error we get will be from the browser WebGPU API.
// Per the WebGPU specification this should be a `TypeError` when features are not
// available, <https://gpuweb.github.io/gpuweb/#dom-gpuadapter-requestdevice>,
// and the stringification it goes through for Rust should put that in the message.
let expected = "TypeError";
} else {
// This message appears whenever wgpu-core is used as the implementation.
let expected = "Unsupported features were requested: Features {";
}
}
assert!(device_error.contains(expected), "{device_error}");
}
// This is a test of device behavior after device.destroy. Specifically, all operations
// should trigger errors since the device is lost.
#[gpu_test]
static DEVICE_DESTROY_THEN_MORE: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(TestParameters::default().features(wgpu::Features::CLEAR_TEXTURE))
.run_sync(|ctx| {
// Create some resources on the device that we will attempt to use *after* losing
// the device.
// Create some 512 x 512 2D textures.
let texture_extent = wgpu::Extent3d {
width: 512,
height: 512,
depth_or_array_layers: 1,
};
let texture_for_view = ctx.device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: texture_extent,
mip_level_count: 2,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rg8Uint,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let target_view = texture_for_view.create_view(&wgpu::TextureViewDescriptor::default());
let texture_for_read = ctx.device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: texture_extent,
mip_level_count: 2,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rg8Uint,
usage: wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let texture_for_write = ctx.device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: texture_extent,
mip_level_count: 2,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rg8Uint,
usage: wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
// Create some buffers.
let buffer_source = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 256,
usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let buffer_dest = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 256,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let buffer_for_map = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 256,
usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let buffer_for_unmap = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 256,
usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: true,
});
// Create a bind group layout.
let bind_group_layout =
ctx.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[],
});
// Create a shader module.
let shader_module = ctx
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed("")),
});
// Create some command encoders.
let mut encoder_for_clear = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
let mut encoder_for_compute_pass = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
let mut encoder_for_render_pass = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
let mut encoder_for_buffer_buffer_copy = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
let mut encoder_for_buffer_texture_copy = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
let mut encoder_for_texture_buffer_copy = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
let mut encoder_for_texture_texture_copy = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
// Destroy the device. This will cause all other requests to return some variation of
// a device invalid error.
ctx.device.destroy();
// TODO: verify the following operations will return an invalid device error:
// * Run a compute or render pass
// * Finish a render bundle encoder
// * Create a texture from HAL
// * Create a buffer from HAL
// * Create a sampler
// * Validate a surface configuration
// * Start or stop capture
// * Get or set buffer sub data
// TODO: figure out how to structure a test around these operations which panic when
// the device is invalid:
// * device.features()
// * device.limits()
// * device.downlevel_properties()
// * device.create_query_set()
// TODO: change these fail calls to check for the specific errors which indicate that
// the device is not valid.
// Creating a command encoder should fail.
fail(
&ctx.device,
|| {
let _ = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
},
Some("device with '' label is invalid"),
);
// Creating a buffer should fail.
fail(
&ctx.device,
|| {
let _ = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 256,
usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
},
Some("device with '' label is invalid"),
);
// Creating a texture should fail.
fail(
&ctx.device,
|| {
let _ = ctx.device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 512,
height: 512,
depth_or_array_layers: 1,
},
mip_level_count: 2,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rg8Uint,
usage: wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
},
Some("device with '' label is invalid"),
);
// Texture clear should fail.
fail(
&ctx.device,
|| {
encoder_for_clear.clear_texture(
&texture_for_write,
&wgpu::ImageSubresourceRange {
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
},
);
},
Some("device with '' label is invalid"),
);
// Creating a compute pass should fail.
fail(
&ctx.device,
|| {
encoder_for_compute_pass.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: None,
timestamp_writes: None,
});
},
Some("device with '' label is invalid"),
);
// Creating a render pass should fail.
fail(
&ctx.device,
|| {
encoder_for_render_pass.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
ops: wgpu::Operations::default(),
resolve_target: None,
view: &target_view,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
},
Some("device with '' label is invalid"),
);
// Copying a buffer to a buffer should fail.
fail(
&ctx.device,
|| {
encoder_for_buffer_buffer_copy.copy_buffer_to_buffer(
&buffer_source,
0,
&buffer_dest,
0,
256,
);
},
Some("device with '' label is invalid"),
);
// Copying a buffer to a texture should fail.
fail(
&ctx.device,
|| {
encoder_for_buffer_texture_copy.copy_buffer_to_texture(
wgpu::TexelCopyBufferInfo {
buffer: &buffer_source,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4),
rows_per_image: None,
},
},
texture_for_write.as_image_copy(),
texture_extent,
);
},
Some("device with '' label is invalid"),
);
// Copying a texture to a buffer should fail.
fail(
&ctx.device,
|| {
encoder_for_texture_buffer_copy.copy_texture_to_buffer(
texture_for_read.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &buffer_source,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4),
rows_per_image: None,
},
},
texture_extent,
);
},
Some("device with '' label is invalid"),
);
// Copying a texture to a texture should fail.
fail(
&ctx.device,
|| {
encoder_for_texture_texture_copy.copy_texture_to_texture(
texture_for_read.as_image_copy(),
texture_for_write.as_image_copy(),
texture_extent,
);
},
Some("device with '' label is invalid"),
);
// Creating a bind group layout should fail.
fail(
&ctx.device,
|| {
let _ = ctx
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[],
});
},
Some("device with '' label is invalid"),
);
// Creating a bind group should fail.
fail(
&ctx.device,
|| {
let _ = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
buffer_source.as_entire_buffer_binding(),
),
}],
});
},
Some("device with '' label is invalid"),
);
// Creating a pipeline layout should fail.
fail(
&ctx.device,
|| {
let _ = ctx
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[],
push_constant_ranges: &[],
});
},
Some("device with '' label is invalid"),
);
// Creating a shader module should fail.
fail(
&ctx.device,
|| {
let _ = ctx
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed("")),
});
},
Some("device with '' label is invalid"),
);
// Creating a shader module spirv should fail.
fail(
&ctx.device,
|| unsafe {
let _ = ctx
.device
.create_shader_module_spirv(&wgpu::ShaderModuleDescriptorSpirV {
label: None,
source: std::borrow::Cow::Borrowed(&[]),
});
},
Some("device with '' label is invalid"),
);
// Creating a render pipeline should fail.
fail(
&ctx.device,
|| {
let _ = ctx
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: None,
vertex: wgpu::VertexState {
module: &shader_module,
entry_point: Some(""),
compilation_options: Default::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: None,
multiview: None,
cache: None,
});
},
Some("device with '' label is invalid"),
);
// Creating a compute pipeline should fail.
fail(
&ctx.device,
|| {
let _ = ctx
.device
.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: None,
layout: None,
module: &shader_module,
entry_point: None,
compilation_options: Default::default(),
cache: None,
});
},
Some("device with '' label is invalid"),
);
// Creating a compute pipeline should fail.
fail(
&ctx.device,
|| {
let _ = ctx
.device
.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: None,
layout: None,
module: &shader_module,
entry_point: None,
compilation_options: Default::default(),
cache: None,
});
},
Some("device with '' label is invalid"),
);
// Buffer map should fail.
fail(
&ctx.device,
|| {
buffer_for_map
.slice(..)
.map_async(wgpu::MapMode::Write, |_| ());
},
Some("device with '' label is invalid"),
);
// Buffer unmap should fail.
fail(
&ctx.device,
|| {
buffer_for_unmap.unmap();
},
Some("device with '' label is invalid"),
);
});
#[gpu_test]
static DEVICE_DESTROY_THEN_LOST: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(TestParameters::default())
.run_async(|ctx| async move {
// This test checks that when device.destroy is called, the provided
// DeviceLostClosure is called with reason DeviceLostReason::Destroyed.
static WAS_CALLED: AtomicBool = AtomicBool::new(false);
// Set a LoseDeviceCallback on the device.
let callback = Box::new(|reason, _m| {
WAS_CALLED.store(true, std::sync::atomic::Ordering::SeqCst);
assert!(
matches!(reason, wgpu::DeviceLostReason::Destroyed),
"Device lost info reason should match DeviceLostReason::Destroyed."
);
});
ctx.device.set_device_lost_callback(callback);
// Destroy the device.
ctx.device.destroy();
// Make sure the device queues are empty, which ensures that the closure
// has been called.
assert!(ctx
.async_poll(wgpu::PollType::wait())
.await
.unwrap()
.is_queue_empty());
assert!(
WAS_CALLED.load(std::sync::atomic::Ordering::SeqCst),
"Device lost callback should have been called."
);
});
#[gpu_test]
static DIFFERENT_BGL_ORDER_BW_SHADER_AND_API: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(TestParameters::default())
.run_sync(|ctx| {
// This test addresses a bug found in multiple backends where `wgpu_core` and `wgpu_hal`
// backends made different assumptions about the element order of vectors of bind group
// layout entries and bind group resource bindings.
//
// Said bug was exposed originally by:
//
// 1. Shader-declared bindings having a different order than resource bindings provided to
// `Device::create_bind_group`.
// 2. Having more of one type of resource in the bind group than another.
//
// …such that internals would accidentally attempt to use an out-of-bounds index (of one
// resource type) in the wrong list of a different resource type. Let's reproduce that
// here.
let trivial_shaders_with_some_reversed_bindings = concat!(
"@group(0) @binding(3) var myTexture2: texture_2d<f32>;\n",
"@group(0) @binding(2) var myTexture1: texture_2d<f32>;\n",
"@group(0) @binding(1) var mySampler: sampler;\n",
"\n",
"@fragment\n",
"fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4f {\n",
" return textureSample(myTexture1, mySampler, pos.xy) \n",
" + textureSample(myTexture2, mySampler, pos.xy);\n",
"}\n",
"\n",
"@vertex\n",
"fn vs_main() -> @builtin(position) vec4<f32> {\n",
" return vec4<f32>(0.0, 0.0, 0.0, 1.0);\n",
"}\n",
);
let trivial_shaders_with_some_reversed_bindings =
ctx.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(
trivial_shaders_with_some_reversed_bindings.into(),
),
});
let my_texture = ctx.device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 1024,
height: 512,
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 | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let my_texture_view = my_texture.create_view(&wgpu::TextureViewDescriptor {
label: None,
format: None,
dimension: None,
usage: None,
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
});
let my_sampler = ctx
.device
.create_sampler(&wgpu::SamplerDescriptor::default());
let render_pipeline = ctx
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
fragment: Some(wgpu::FragmentState {
module: &trivial_shaders_with_some_reversed_bindings,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Bgra8Unorm,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
layout: None,
// Other fields below aren't interesting for this text.
label: None,
vertex: wgpu::VertexState {
module: &trivial_shaders_with_some_reversed_bindings,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
// fail(&ctx.device, || {
// }, "");
let _ = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &render_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&my_sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&my_texture_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(&my_texture_view),
},
],
});
});
#[gpu_test]
static DEVICE_DESTROY_THEN_BUFFER_CLEANUP: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(TestParameters::default())
.run_sync(|ctx| {
// When a device is destroyed, its resources should be released,
// without causing a deadlock.
// Create a buffer to be left around until the device is destroyed.
let _buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 256,
usage: wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
// Create a texture to be left around until the device is destroyed.
let texture_extent = wgpu::Extent3d {
width: 512,
height: 512,
depth_or_array_layers: 1,
};
let _texture = ctx.device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: texture_extent,
mip_level_count: 2,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rg8Uint,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
// Destroy the device.
ctx.device.destroy();
// Poll the device, which should try to clean up its resources.
ctx.instance.poll_all(true);
});
#[gpu_test]
static DEVICE_AND_QUEUE_HAVE_DIFFERENT_IDS: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(TestParameters::default())
.run_async(|ctx| async move {
let TestingContext {
adapter,
device_features,
device_limits,
device,
queue,
..
} = ctx;
drop(device);
let (device2, queue2) =
wgpu_test::initialize_device(&adapter, device_features, device_limits).await;
drop(queue);
drop(device2);
drop(queue2); // this would previously panic since we would try to use the Device ID to drop the Queue
});