Files
wgpu/tests/gpu-tests/vertex_indices/mod.rs
2025-02-23 14:06:34 -05:00

489 lines
17 KiB
Rust

//! Tests that vertex buffers, vertex indices, and instance indices are properly handled.
//!
//! We need tests for these as the backends use various schemes to work around the lack
//! of support for things like `gl_BaseInstance` in shaders.
use std::{num::NonZeroU64, ops::Range};
use itertools::Itertools;
use strum::IntoEnumIterator;
use wgpu::util::{BufferInitDescriptor, DeviceExt, RenderEncoder};
use wgpu::RenderBundleDescriptor;
use wgpu_test::{gpu_test, GpuTestConfiguration, TestParameters, TestingContext};
/// Generic struct representing a draw call
struct Draw {
vertex: Range<u32>,
instance: Range<u32>,
/// If present, is an indexed call
base_vertex: Option<i32>,
}
impl Draw {
/// Directly execute the draw call
fn execute(&self, rpass: &mut dyn RenderEncoder<'_>) {
if let Some(base_vertex) = self.base_vertex {
rpass.draw_indexed(self.vertex.clone(), base_vertex, self.instance.clone());
} else {
rpass.draw(self.vertex.clone(), self.instance.clone());
}
}
/// Add the draw call to the given indirect buffer
fn add_to_buffer(&self, bytes: &mut Vec<u8>, features: wgpu::Features) {
// The behavior of non-zero first_instance in indirect draw calls in currently undefined if INDIRECT_FIRST_INSTANCE is not supported.
let supports_first_instance = features.contains(wgpu::Features::INDIRECT_FIRST_INSTANCE);
let first_instance = if supports_first_instance {
self.instance.start
} else {
0
};
if let Some(base_vertex) = self.base_vertex {
bytes.extend_from_slice(
wgpu::util::DrawIndexedIndirectArgs {
index_count: self.vertex.end - self.vertex.start,
instance_count: self.instance.end - self.instance.start,
base_vertex,
first_index: self.vertex.start,
first_instance,
}
.as_bytes(),
)
} else {
bytes.extend_from_slice(
wgpu::util::DrawIndirectArgs {
vertex_count: self.vertex.end - self.vertex.start,
instance_count: self.instance.end - self.instance.start,
first_vertex: self.vertex.start,
first_instance,
}
.as_bytes(),
)
}
}
/// Execute the draw call from the given indirect buffer
fn execute_indirect<'rpass>(
&self,
rpass: &mut dyn RenderEncoder<'rpass>,
indirect: &'rpass wgpu::Buffer,
offset: &mut u64,
) {
if self.base_vertex.is_some() {
rpass.draw_indexed_indirect(indirect, *offset);
*offset += 20;
} else {
rpass.draw_indirect(indirect, *offset);
*offset += 16;
}
}
}
#[derive(Debug, Copy, Clone, strum::EnumIter)]
enum TestCase {
/// A single draw call with 6 vertices
Draw,
/// Two draw calls of 0..3 and 3..6 verts
DrawNonZeroFirstVertex,
/// A single draw call with 6 vertices and a vertex offset of 3
DrawBaseVertex,
/// A single draw call with 3 vertices and 2 instances
DrawInstanced,
/// Two draw calls with 3 vertices and 0..1 and 1..2 instances.
DrawNonZeroFirstInstance,
}
impl TestCase {
// Get the draw calls for this test case
fn draws(&self) -> &'static [Draw] {
match self {
TestCase::Draw => &[Draw {
vertex: 0..6,
instance: 0..1,
base_vertex: None,
}],
TestCase::DrawNonZeroFirstVertex => &[
Draw {
vertex: 0..3,
instance: 0..1,
base_vertex: None,
},
Draw {
vertex: 3..6,
instance: 0..1,
base_vertex: None,
},
],
TestCase::DrawBaseVertex => &[Draw {
vertex: 0..6,
instance: 0..1,
base_vertex: Some(3),
}],
TestCase::DrawInstanced => &[Draw {
vertex: 0..3,
instance: 0..2,
base_vertex: None,
}],
TestCase::DrawNonZeroFirstInstance => &[
Draw {
vertex: 0..3,
instance: 0..1,
base_vertex: None,
},
Draw {
vertex: 0..3,
instance: 1..2,
base_vertex: None,
},
],
}
}
}
#[derive(Debug, Copy, Clone, strum::EnumIter)]
enum IdSource {
/// Use buffers to load the vertex and instance index
Buffers,
/// Use builtins to load the vertex and instance index
Builtins,
}
#[derive(Debug, Copy, Clone, strum::EnumIter)]
enum DrawCallKind {
Direct,
Indirect,
}
#[derive(Debug, Copy, Clone, strum::EnumIter)]
enum EncoderKind {
RenderPass,
RenderBundle,
}
struct Test {
case: TestCase,
id_source: IdSource,
draw_call_kind: DrawCallKind,
encoder_kind: EncoderKind,
}
impl Test {
/// Get the expected result from this test, taking into account
/// the various features and capabilities that may be missing.
fn expectation(&self, ctx: &TestingContext) -> &'static [u32] {
let is_indirect = matches!(self.draw_call_kind, DrawCallKind::Indirect);
// Both of these failure modes require indirect rendering
// If this is false, the first instance will be ignored.
let non_zero_first_instance_supported = ctx
.adapter
.features()
.contains(wgpu::Features::INDIRECT_FIRST_INSTANCE)
|| !is_indirect;
// If this is false, it won't be ignored, but it won't show up in the shader
//
// If the IdSource is buffers, this doesn't apply
let first_vert_instance_supported = ctx.adapter_downlevel_capabilities.flags.contains(
wgpu::DownlevelFlags::VERTEX_AND_INSTANCE_INDEX_RESPECTS_RESPECTIVE_FIRST_VALUE_IN_INDIRECT_DRAW,
) || matches!(self.id_source, IdSource::Buffers)
|| !is_indirect;
match self.case {
TestCase::DrawBaseVertex => {
if !first_vert_instance_supported {
return &[0, 1, 2, 3, 4, 5];
}
&[0, 0, 0, 3, 4, 5, 6, 7, 8]
}
TestCase::Draw | TestCase::DrawInstanced => &[0, 1, 2, 3, 4, 5],
TestCase::DrawNonZeroFirstVertex => {
if !first_vert_instance_supported {
return &[0, 1, 2, 0, 0, 0];
}
&[0, 1, 2, 3, 4, 5]
}
TestCase::DrawNonZeroFirstInstance => {
if !first_vert_instance_supported || !non_zero_first_instance_supported {
return &[0, 1, 2, 0, 0, 0];
}
&[0, 1, 2, 3, 4, 5]
}
}
}
}
async fn vertex_index_common(ctx: TestingContext) {
let identity_buffer = ctx.device.create_buffer_init(&BufferInitDescriptor {
label: Some("identity buffer"),
contents: bytemuck::cast_slice(&[0u32, 1, 2, 3, 4, 5, 6, 7, 8]),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::INDEX,
});
let shader = ctx
.device
.create_shader_module(wgpu::include_wgsl!("draw.vert.wgsl"));
let bgl = ctx
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(4),
},
visibility: wgpu::ShaderStages::VERTEX,
count: None,
}],
});
let ppl = ctx
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&bgl],
push_constant_ranges: &[],
});
let mut pipeline_desc = wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&ppl),
vertex: wgpu::VertexState {
buffers: &[],
module: &shader,
entry_point: Some("vs_main_builtin"),
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview: None,
cache: None,
};
let builtin_pipeline = ctx.device.create_render_pipeline(&pipeline_desc);
pipeline_desc.vertex.entry_point = Some("vs_main_buffers");
pipeline_desc.vertex.buffers = &[
wgpu::VertexBufferLayout {
array_stride: 4,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array![0 => Uint32],
},
wgpu::VertexBufferLayout {
array_stride: 4,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![1 => Uint32],
},
];
let buffer_pipeline = ctx.device.create_render_pipeline(&pipeline_desc);
let dummy = ctx
.device
.create_texture_with_data(
&ctx.queue,
&wgpu::TextureDescriptor {
label: Some("dummy"),
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 | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
&[0, 0, 0, 1],
)
.create_view(&wgpu::TextureViewDescriptor::default());
let tests = TestCase::iter()
.cartesian_product(IdSource::iter())
.cartesian_product(DrawCallKind::iter())
.cartesian_product(EncoderKind::iter())
.map(|(((case, id_source), draw_call_kind), encoder_kind)| Test {
case,
id_source,
draw_call_kind,
encoder_kind,
})
.collect::<Vec<_>>();
let features = ctx.adapter.features();
let mut failed = false;
for test in tests {
let pipeline = match test.id_source {
IdSource::Buffers => &buffer_pipeline,
IdSource::Builtins => &builtin_pipeline,
};
let expected = test.expectation(&ctx);
let buffer_size = (size_of_val(&expected[0]) * expected.len()) as u64;
let cpu_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let gpu_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: buffer_size,
usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::STORAGE,
mapped_at_creation: false,
});
let bg = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: gpu_buffer.as_entire_binding(),
}],
});
let mut encoder1 = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
let render_bundle;
let indirect_buffer;
let mut rpass = encoder1.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
ops: wgpu::Operations::default(),
resolve_target: None,
view: &dummy,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
{
// Need to scope render_bundle_encoder since it's not Send and would otherwise
// infect the function if not going out of scope before an await call.
// (it is dropped via `take` + `finish` earlier, but compiler does not take this into account)
let mut render_bundle_encoder = match test.encoder_kind {
EncoderKind::RenderPass => None,
EncoderKind::RenderBundle => Some(ctx.device.create_render_bundle_encoder(
&wgpu::RenderBundleEncoderDescriptor {
label: Some("test renderbundle encoder"),
color_formats: &[Some(wgpu::TextureFormat::Rgba8Unorm)],
depth_stencil: None,
sample_count: 1,
multiview: None,
},
)),
};
let render_encoder: &mut dyn RenderEncoder = render_bundle_encoder
.as_mut()
.map(|r| r as &mut dyn RenderEncoder)
.unwrap_or(&mut rpass);
render_encoder.set_vertex_buffer(0, identity_buffer.slice(..));
render_encoder.set_vertex_buffer(1, identity_buffer.slice(..));
render_encoder.set_index_buffer(identity_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_encoder.set_pipeline(pipeline);
render_encoder.set_bind_group(0, Some(&bg), &[]);
let draws = test.case.draws();
match test.draw_call_kind {
DrawCallKind::Direct => {
for draw in draws {
draw.execute(render_encoder);
}
}
DrawCallKind::Indirect => {
let mut indirect_bytes = Vec::new();
for draw in draws {
draw.add_to_buffer(&mut indirect_bytes, features);
}
indirect_buffer = ctx.device.create_buffer_init(&BufferInitDescriptor {
label: Some("indirect"),
contents: &indirect_bytes,
usage: wgpu::BufferUsages::INDIRECT,
});
let mut offset = 0;
for draw in draws {
draw.execute_indirect(render_encoder, &indirect_buffer, &mut offset);
}
}
}
if let Some(render_bundle_encoder) = render_bundle_encoder.take() {
render_bundle = render_bundle_encoder.finish(&RenderBundleDescriptor {
label: Some("test renderbundle"),
});
rpass.execute_bundles([&render_bundle]);
}
}
drop(rpass);
let mut encoder2 = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
encoder2.copy_buffer_to_buffer(&gpu_buffer, 0, &cpu_buffer, 0, buffer_size);
// See https://github.com/gfx-rs/wgpu/issues/4732 for why this is split between two submissions
// with a hard wait in between.
ctx.queue.submit([encoder1.finish()]);
ctx.async_poll(wgpu::PollType::wait()).await.unwrap();
ctx.queue.submit([encoder2.finish()]);
let slice = cpu_buffer.slice(..);
slice.map_async(wgpu::MapMode::Read, |_| ());
ctx.async_poll(wgpu::PollType::wait()).await.unwrap();
let data: Vec<u32> = bytemuck::cast_slice(&slice.get_mapped_range()).to_vec();
let case_name = format!(
"Case {:?} getting indices from {:?} using {:?} draw calls, encoded with a {:?}",
test.case, test.id_source, test.draw_call_kind, test.encoder_kind
);
if data != expected {
eprintln!("Failed: Got: {data:?} Expected: {expected:?} - {case_name}",);
failed = true;
} else {
eprintln!("Passed: {case_name}");
}
}
assert!(!failed);
}
#[gpu_test]
static VERTEX_INDICES: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(
TestParameters::default()
.test_features_limits()
.features(wgpu::Features::VERTEX_WRITABLE_STORAGE)
.features(wgpu::Features::INDIRECT_FIRST_INSTANCE),
)
.run_async(vertex_index_common);