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

426 lines
15 KiB
Rust

//! Tests that vertex formats pass through to vertex shaders accurately.
use std::num::NonZeroU64;
use wgpu::util::{BufferInitDescriptor, DeviceExt};
use wgpu_test::{gpu_test, FailureCase, GpuTestConfiguration, TestParameters, TestingContext};
#[derive(Debug, Copy, Clone)]
enum TestCase {
UnormsAndSnorms,
UintsAndSintsSmall,
UintsBig,
SintsBig,
Floats,
Unorm1010102,
SingleSmallNormsAndInts,
Unorm8x4Bgra,
}
struct Test<'a> {
case: TestCase,
entry_point: &'a str,
attributes: &'a [wgpu::VertexAttribute],
input: &'a [u8],
checksums: &'a [f32],
}
async fn vertex_formats_all(ctx: TestingContext) {
let attributes_block_0 = &wgpu::vertex_attr_array![
0 => Unorm8x4,
1 => Unorm16x2,
2 => Unorm16x4,
3 => Snorm8x4,
4 => Snorm16x2,
5 => Snorm16x4,
6 => Unorm8x2,
7 => Snorm8x2,
];
let attributes_block_1 = &wgpu::vertex_attr_array![
0 => Uint8x4,
1 => Uint16x2,
2 => Uint16x4,
3 => Sint8x4,
4 => Sint16x2,
5 => Sint16x4,
6 => Uint8x2,
7 => Sint8x2,
];
let attributes_block_2 = &wgpu::vertex_attr_array![
0 => Uint32,
1 => Uint32x2,
2 => Uint32x3,
3 => Uint32x4,
];
let attributes_block_3 = &wgpu::vertex_attr_array![
0 => Sint32,
1 => Sint32x2,
2 => Sint32x3,
3 => Sint32x4,
];
let attributes_block_4 = &wgpu::vertex_attr_array![
0 => Float32,
1 => Float32x2,
2 => Float32x3,
3 => Float32x4,
4 => Float16x2,
5 => Float16x4,
6 => Float16,
];
let attributes_block_6 = &wgpu::vertex_attr_array![
0 => Uint16,
1 => Sint16,
2 => Unorm16,
3 => Snorm16,
4 => Uint8,
5 => Sint8,
6 => Unorm8,
7 => Snorm8,
];
let attributes_block_7 = &wgpu::vertex_attr_array![
0 => Unorm8x4Bgra,
];
let tests = vec![
Test {
case: TestCase::UnormsAndSnorms,
entry_point: "vertex_block_0",
attributes: attributes_block_0,
input: &[
128u8, 128u8, 128u8, 128u8, // Unorm8x4 (0.5, 0.5, 0.5, 0.5)
0u8, 128u8, 0u8, 128u8, // Unorm16x2 (0.5, 0.5)
0u8, 64u8, 0u8, 64u8, 0u8, 64u8, 0u8,
64u8, // Unorm16x4 (0.25, 0.25, 0.25, 0.25)
127u8, 127u8, 127u8, 127u8, // Snorm8x4 (1, 1, 1, 1)
0u8, 128u8, 0u8, 128u8, // Snorm16x2 (-1, -1)
255u8, 127u8, 255u8, 127u8, 255u8, 127u8, 255u8,
127u8, // Snorm16x4 (1, 1, 1, 1)
255u8, 255u8, // Unorm8x2 (1, 1)
128u8, 128u8, // Snorm8x2 (-1, -1)
],
checksums: &[0.0, 0.0, 6.0, 4.0, 0.0, 0.0],
},
Test {
case: TestCase::UintsAndSintsSmall,
entry_point: "vertex_block_1",
attributes: attributes_block_1,
input: &[
4u8, 8u8, 16u8, 32u8, // Uint8x4 (4, 8, 16, 32)
64u8, 0u8, 128u8, 0u8, // Uint16x2 (64, 128)
0u8, 1u8, 0u8, 2u8, 0u8, 4u8, 0u8, 8u8, // Uint16x4 (256, 512, 1024, 2048)
127u8, 127u8, 2u8, 0u8, // Sint8x4 (127, 127, 2, 0)
255u8, 255u8, 1u8, 0u8, // Sint16x2 (-1, 1)
128u8, 255u8, 128u8, 255u8, 0u8, 1u8, 240u8,
255u8, // Sint16x4 (-128, -128, 256, -16)
1u8, 2u8, // Uint8x2 (1, 2)
128u8, 128u8, // Sint8x2 (-128, -128)
],
checksums: &[4095.0, -16.0, 0.0, 0.0, 0.0, 0.0],
},
Test {
case: TestCase::UintsBig,
entry_point: "vertex_block_2",
attributes: attributes_block_2,
input: &[
1u8, 0u8, 0u8, 0u8, // Uint32x2 (1)
2u8, 0u8, 0u8, 0u8, 4u8, 0u8, 0u8, 0u8, // Uint32x2 (2, 4)
8u8, 0u8, 0u8, 0u8, 16u8, 0u8, 0u8, 0u8, 32u8, 0u8, 0u8,
0u8, // Uint32x3 (8, 16, 32)
64u8, 0u8, 0u8, 0u8, 128u8, 0u8, 0u8, 0u8, 0u8, 1u8, 0u8, 0u8, 0u8, 2u8, 0u8,
0u8, // Uint32x4 (64, 128, 256, 512)
],
checksums: &[1023.0, 0.0, 0.0, 0.0, 0.0, 0.0],
},
Test {
case: TestCase::SintsBig,
entry_point: "vertex_block_3",
attributes: attributes_block_3,
input: &[
128u8, 255u8, 255u8, 255u8, // Sint32 (-128)
120u8, 0u8, 0u8, 0u8, 8u8, 0u8, 0u8, 0u8, // Sint32x2 (120, 8)
252u8, 255u8, 255u8, 255u8, 2u8, 0u8, 0u8, 0u8, 2u8, 0u8, 0u8,
0u8, // Sint32x3 (-4, 2, 2)
24u8, 252u8, 255u8, 255u8, 88u8, 2u8, 0u8, 0u8, 44u8, 1u8, 0u8, 0u8, 99u8, 0u8,
0u8, 0u8, // Sint32x4 (-1000, 600, 300, 99)
],
checksums: &[0.0, -1.0, 0.0, 0.0, 0.0, 0.0],
},
Test {
case: TestCase::Floats,
entry_point: "vertex_block_4",
attributes: attributes_block_4,
input: &[
0u8, 0u8, 0u8, 63u8, // Float32 (0.5)
0u8, 0u8, 0u8, 191u8, 0u8, 0u8, 128u8, 64u8, // Float32x2 (-0.5, 4.0)
0u8, 0u8, 0u8, 192u8, 0u8, 0u8, 204u8, 194u8, 0u8, 0u8, 200u8,
66u8, // Float32x3 (-2.0, -102.0, 100.0)
0u8, 0u8, 92u8, 66u8, 0u8, 0u8, 72u8, 194u8, 0u8, 0u8, 32u8, 65u8, 0u8, 0u8, 128u8,
63u8, // Float32x4 (55.0, -50.0, 10.0, 1.0)
0u8, 68u8, // Float16 (4.0)
0u8, 60u8, 72u8, 53u8, // Float16x2 (1.0, 0.33)
72u8, 57u8, 0u8, 192u8, 0u8, 188u8, 0u8,
184u8, // Float16x4 (0.66, -2.0, -1.0, -0.5)
],
checksums: &[0.0, 0.0, 0.0, 0.0, 2.5, 16.0],
},
Test {
case: TestCase::SingleSmallNormsAndInts,
entry_point: "vertex_block_6",
attributes: attributes_block_6,
input: &[
1u8, 2u8, // Uint16 (513)
1u8, 2u8, // Sint16 (513)
0u8, 64u8, // Unorm16 (0.25)
0u8, 64u8, // Snorm16 (0.5)
32u8, // Uint8 (32)
255u8, // Sint8 (-1)
128u8, // Unorm8 (0.5)
128u8, // Snorm8 (-1)
],
checksums: &[513.0 + 32.0, 513.0 - 1.0, 0.25 + 0.5, 0.5 - 1.0, 0.0, 0.0],
},
Test {
case: TestCase::Unorm8x4Bgra,
entry_point: "vertex_block_7",
attributes: attributes_block_7,
input: &[
128u8, 85u8, 170u8, 64u8, // Unorm8x4Bgra (0.67, 0.33, 0.5, 0.25)
],
checksums: &[0.0, 0.0, 1.75, 0.0, 0.0, 0.0],
},
];
vertex_formats_common(ctx, &tests).await;
}
async fn vertex_formats_10_10_10_2(ctx: TestingContext) {
let attributes_block_5 = &wgpu::vertex_attr_array![
0 => Unorm10_10_10_2,
];
let tests = vec![Test {
case: TestCase::Unorm1010102,
entry_point: "vertex_block_5",
attributes: attributes_block_5,
input: &[
// We are aiming for rgba of (0.5, 0.5, 0.5, 0.66)
// Packing AA BB BBBB BBBB GGGG GGGG GG RR RRRR RRRR
// Binary 10 10 0000 0000 1000 0000 00 10 0000 0000
// Hex A0 08 02 00
// Decimal 160 8 2 0
// unorm 0.66 0.5 0.5 0.5 = 2.16
0u8, 2u8, 8u8, 160u8, // Unorm10_10_10_2
],
checksums: &[0.0, 0.0, 2.16, 0.0, 0.0, 0.0],
}];
vertex_formats_common(ctx, &tests).await;
}
async fn vertex_formats_common(ctx: TestingContext, tests: &[Test<'_>]) {
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 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 mut failed = false;
for test in tests {
let buffer_input = ctx.device.create_buffer_init(&BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(test.input),
usage: wgpu::BufferUsages::VERTEX,
});
let pipeline_desc = wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&ppl),
vertex: wgpu::VertexState {
buffers: &[wgpu::VertexBufferLayout {
array_stride: 0, // Calculate, please!
step_mode: wgpu::VertexStepMode::Vertex,
attributes: test.attributes,
}],
module: &shader,
entry_point: Some(test.entry_point),
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fragment_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 pipeline = ctx.device.create_render_pipeline(&pipeline_desc);
let expected = test.checksums;
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 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,
});
rpass.set_vertex_buffer(0, buffer_input.slice(..));
rpass.set_pipeline(&pipeline);
rpass.set_bind_group(0, &bg, &[]);
// Draw three vertices and no instance, which is enough to generate the
// checksums.
rpass.draw(0..3, 0..1);
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<f32> = bytemuck::cast_slice(&slice.get_mapped_range()).to_vec();
let case_name = format!("Case {:?}", test.case);
// Calculate the difference between data and expected. Since the data is
// a bunch of float checksums, we allow a fairly large epsilon, which helps
// with the accumulation of float rounding errors.
const EPSILON: f32 = 0.01;
let mut deltas = data.iter().zip(expected.iter()).map(|(d, e)| (d - e).abs());
if deltas.any(|x| x > EPSILON) {
eprintln!("Failed: Got: {data:?} Expected: {expected:?} - {case_name}",);
failed = true;
continue;
}
eprintln!("Passed: {case_name}");
}
assert!(!failed);
}
#[gpu_test]
static VERTEX_FORMATS_ALL: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(
TestParameters::default()
.test_features_limits()
.features(wgpu::Features::VERTEX_WRITABLE_STORAGE),
)
.run_async(vertex_formats_all);
// Some backends can handle Unorm-10-10-2, but GL backends seem to throw this error:
// Validation Error: GL_INVALID_ENUM in glVertexAttribFormat(type = GL_UNSIGNED_INT_10_10_10_2)
#[gpu_test]
static VERTEX_FORMATS_10_10_10_2: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(
TestParameters::default()
.expect_fail(FailureCase::backend(wgpu::Backends::GL))
.test_features_limits()
.features(wgpu::Features::VERTEX_WRITABLE_STORAGE),
)
.run_async(vertex_formats_10_10_10_2);