mirror of
https://github.com/gfx-rs/wgpu.git
synced 2024-11-25 08:13:27 +00:00
646 lines
19 KiB
Rust
646 lines
19 KiB
Rust
use naga::{valid, Expression, Function, Scalar};
|
|
|
|
/// Validation should fail if `AtomicResult` expressions are not
|
|
/// populated by `Atomic` statements.
|
|
#[test]
|
|
fn populate_atomic_result() {
|
|
use naga::{Module, Type, TypeInner};
|
|
|
|
/// Different variants of the test case that we want to exercise.
|
|
enum Variant {
|
|
/// An `AtomicResult` expression with an `Atomic` statement
|
|
/// that populates it: valid.
|
|
Atomic,
|
|
|
|
/// An `AtomicResult` expression visited by an `Emit`
|
|
/// statement: invalid.
|
|
Emit,
|
|
|
|
/// An `AtomicResult` expression visited by no statement at
|
|
/// all: invalid
|
|
None,
|
|
}
|
|
|
|
// Looking at uses of `variant` should make it easy to identify
|
|
// the differences between the test cases.
|
|
fn try_variant(
|
|
variant: Variant,
|
|
) -> Result<naga::valid::ModuleInfo, naga::WithSpan<naga::valid::ValidationError>> {
|
|
let span = naga::Span::default();
|
|
let mut module = Module::default();
|
|
let ty_u32 = module.types.insert(
|
|
Type {
|
|
name: Some("u32".into()),
|
|
inner: TypeInner::Scalar(Scalar::U32),
|
|
},
|
|
span,
|
|
);
|
|
let ty_atomic_u32 = module.types.insert(
|
|
Type {
|
|
name: Some("atomic<u32>".into()),
|
|
inner: TypeInner::Atomic(Scalar::U32),
|
|
},
|
|
span,
|
|
);
|
|
let var_atomic = module.global_variables.append(
|
|
naga::GlobalVariable {
|
|
name: Some("atomic_global".into()),
|
|
space: naga::AddressSpace::WorkGroup,
|
|
binding: None,
|
|
ty: ty_atomic_u32,
|
|
init: None,
|
|
},
|
|
span,
|
|
);
|
|
|
|
let mut fun = Function::default();
|
|
let ex_global = fun
|
|
.expressions
|
|
.append(Expression::GlobalVariable(var_atomic), span);
|
|
let ex_42 = fun
|
|
.expressions
|
|
.append(Expression::Literal(naga::Literal::U32(42)), span);
|
|
let ex_result = fun.expressions.append(
|
|
Expression::AtomicResult {
|
|
ty: ty_u32,
|
|
comparison: false,
|
|
},
|
|
span,
|
|
);
|
|
|
|
match variant {
|
|
Variant::Atomic => {
|
|
fun.body.push(
|
|
naga::Statement::Atomic {
|
|
pointer: ex_global,
|
|
fun: naga::AtomicFunction::Add,
|
|
value: ex_42,
|
|
result: Some(ex_result),
|
|
},
|
|
span,
|
|
);
|
|
}
|
|
Variant::Emit => {
|
|
fun.body.push(
|
|
naga::Statement::Emit(naga::Range::new_from_bounds(ex_result, ex_result)),
|
|
span,
|
|
);
|
|
}
|
|
Variant::None => {}
|
|
}
|
|
|
|
module.functions.append(fun, span);
|
|
|
|
valid::Validator::new(
|
|
valid::ValidationFlags::default(),
|
|
valid::Capabilities::all(),
|
|
)
|
|
.validate(&module)
|
|
}
|
|
|
|
try_variant(Variant::Atomic).expect("module should validate");
|
|
assert!(try_variant(Variant::Emit).is_err());
|
|
assert!(try_variant(Variant::None).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn populate_call_result() {
|
|
use naga::{Module, Type, TypeInner};
|
|
|
|
/// Different variants of the test case that we want to exercise.
|
|
enum Variant {
|
|
/// A `CallResult` expression with an `Call` statement that
|
|
/// populates it: valid.
|
|
Call,
|
|
|
|
/// A `CallResult` expression visited by an `Emit` statement:
|
|
/// invalid.
|
|
Emit,
|
|
|
|
/// A `CallResult` expression visited by no statement at all:
|
|
/// invalid
|
|
None,
|
|
}
|
|
|
|
// Looking at uses of `variant` should make it easy to identify
|
|
// the differences between the test cases.
|
|
fn try_variant(
|
|
variant: Variant,
|
|
) -> Result<naga::valid::ModuleInfo, naga::WithSpan<naga::valid::ValidationError>> {
|
|
let span = naga::Span::default();
|
|
let mut module = Module::default();
|
|
let ty_u32 = module.types.insert(
|
|
Type {
|
|
name: Some("u32".into()),
|
|
inner: TypeInner::Scalar(Scalar::U32),
|
|
},
|
|
span,
|
|
);
|
|
|
|
let mut fun_callee = Function {
|
|
result: Some(naga::FunctionResult {
|
|
ty: ty_u32,
|
|
binding: None,
|
|
}),
|
|
..Function::default()
|
|
};
|
|
let ex_42 = fun_callee
|
|
.expressions
|
|
.append(Expression::Literal(naga::Literal::U32(42)), span);
|
|
fun_callee
|
|
.body
|
|
.push(naga::Statement::Return { value: Some(ex_42) }, span);
|
|
let fun_callee = module.functions.append(fun_callee, span);
|
|
|
|
let mut fun_caller = Function::default();
|
|
let ex_result = fun_caller
|
|
.expressions
|
|
.append(Expression::CallResult(fun_callee), span);
|
|
|
|
match variant {
|
|
Variant::Call => {
|
|
fun_caller.body.push(
|
|
naga::Statement::Call {
|
|
function: fun_callee,
|
|
arguments: vec![],
|
|
result: Some(ex_result),
|
|
},
|
|
span,
|
|
);
|
|
}
|
|
Variant::Emit => {
|
|
fun_caller.body.push(
|
|
naga::Statement::Emit(naga::Range::new_from_bounds(ex_result, ex_result)),
|
|
span,
|
|
);
|
|
}
|
|
Variant::None => {}
|
|
}
|
|
|
|
module.functions.append(fun_caller, span);
|
|
|
|
valid::Validator::new(
|
|
valid::ValidationFlags::default(),
|
|
valid::Capabilities::all(),
|
|
)
|
|
.validate(&module)
|
|
}
|
|
|
|
try_variant(Variant::Call).expect("should validate");
|
|
assert!(try_variant(Variant::Emit).is_err());
|
|
assert!(try_variant(Variant::None).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn emit_workgroup_uniform_load_result() {
|
|
use naga::{Module, Type, TypeInner};
|
|
|
|
// We want to ensure that the *only* problem with the code is the
|
|
// use of an `Emit` statement instead of an `Atomic` statement. So
|
|
// validate two versions of the module varying only in that
|
|
// aspect.
|
|
//
|
|
// Looking at uses of the `wg_load` makes it easy to identify the
|
|
// differences between the two variants.
|
|
fn variant(
|
|
wg_load: bool,
|
|
) -> Result<naga::valid::ModuleInfo, naga::WithSpan<naga::valid::ValidationError>> {
|
|
let span = naga::Span::default();
|
|
let mut module = Module::default();
|
|
let ty_u32 = module.types.insert(
|
|
Type {
|
|
name: Some("u32".into()),
|
|
inner: TypeInner::Scalar(Scalar::U32),
|
|
},
|
|
span,
|
|
);
|
|
let var_workgroup = module.global_variables.append(
|
|
naga::GlobalVariable {
|
|
name: Some("workgroup_global".into()),
|
|
space: naga::AddressSpace::WorkGroup,
|
|
binding: None,
|
|
ty: ty_u32,
|
|
init: None,
|
|
},
|
|
span,
|
|
);
|
|
|
|
let mut fun = Function::default();
|
|
let ex_global = fun
|
|
.expressions
|
|
.append(Expression::GlobalVariable(var_workgroup), span);
|
|
let ex_result = fun
|
|
.expressions
|
|
.append(Expression::WorkGroupUniformLoadResult { ty: ty_u32 }, span);
|
|
|
|
if wg_load {
|
|
fun.body.push(
|
|
naga::Statement::WorkGroupUniformLoad {
|
|
pointer: ex_global,
|
|
result: ex_result,
|
|
},
|
|
span,
|
|
);
|
|
} else {
|
|
fun.body.push(
|
|
naga::Statement::Emit(naga::Range::new_from_bounds(ex_result, ex_result)),
|
|
span,
|
|
);
|
|
}
|
|
|
|
module.functions.append(fun, span);
|
|
|
|
valid::Validator::new(
|
|
valid::ValidationFlags::default(),
|
|
valid::Capabilities::all(),
|
|
)
|
|
.validate(&module)
|
|
}
|
|
|
|
variant(true).expect("module should validate");
|
|
assert!(variant(false).is_err());
|
|
}
|
|
|
|
#[cfg(feature = "wgsl-in")]
|
|
#[test]
|
|
fn bad_cross_builtin_args() {
|
|
// NOTE: Things we expect to actually compile are in the `cross` snapshot test.
|
|
let cases = [
|
|
(
|
|
"vec2(0., 1.)",
|
|
"\
|
|
error: Entry point main at Compute is invalid
|
|
┌─ wgsl:3:13
|
|
│
|
|
3 │ let a = cross(vec2(0., 1.), vec2(0., 1.));
|
|
│ ^^^^^ naga::Expression [6]
|
|
│
|
|
= Expression [6] is invalid
|
|
= Argument [0] to Cross as expression [2] has an invalid type.
|
|
|
|
",
|
|
),
|
|
(
|
|
"vec4(0., 1., 2., 3.)",
|
|
"\
|
|
error: Entry point main at Compute is invalid
|
|
┌─ wgsl:3:13
|
|
│
|
|
3 │ let a = cross(vec4(0., 1., 2., 3.), vec4(0., 1., 2., 3.));
|
|
│ ^^^^^ naga::Expression [10]
|
|
│
|
|
= Expression [10] is invalid
|
|
= Argument [0] to Cross as expression [4] has an invalid type.
|
|
|
|
",
|
|
),
|
|
];
|
|
|
|
for (invalid_arg, expected_err) in cases {
|
|
let source = format!(
|
|
"\
|
|
@compute @workgroup_size(1)
|
|
fn main() {{
|
|
let a = cross({invalid_arg}, {invalid_arg});
|
|
}}
|
|
"
|
|
);
|
|
let module = naga::front::wgsl::parse_str(&source).unwrap();
|
|
let err = valid::Validator::new(Default::default(), valid::Capabilities::all())
|
|
.validate_no_overrides(&module)
|
|
.expect_err("module should be invalid");
|
|
assert_eq!(err.emit_to_string(&source), expected_err);
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "wgsl-in")]
|
|
#[test]
|
|
fn incompatible_interpolation_and_sampling_types() {
|
|
use dummy_interpolation_shader::DummyInterpolationShader;
|
|
|
|
// NOTE: Things we expect to actually compile are in the `interpolate` snapshot test.
|
|
use itertools::Itertools;
|
|
|
|
let invalid_shader_module = |interpolation_and_sampling| {
|
|
let (interpolation, sampling) = interpolation_and_sampling;
|
|
|
|
let valid = matches!(
|
|
(interpolation, sampling),
|
|
(_, None)
|
|
| (
|
|
naga::Interpolation::Perspective | naga::Interpolation::Linear,
|
|
Some(
|
|
naga::Sampling::Center | naga::Sampling::Centroid | naga::Sampling::Sample
|
|
),
|
|
)
|
|
| (
|
|
naga::Interpolation::Flat,
|
|
Some(naga::Sampling::First | naga::Sampling::Either)
|
|
)
|
|
);
|
|
|
|
if valid {
|
|
None
|
|
} else {
|
|
let DummyInterpolationShader {
|
|
source,
|
|
module,
|
|
interpolate_attr,
|
|
entry_point: _,
|
|
} = DummyInterpolationShader::new(interpolation, sampling);
|
|
Some((
|
|
source,
|
|
module,
|
|
interpolation,
|
|
sampling.expect("default interpolation sampling should be valid"),
|
|
interpolate_attr,
|
|
))
|
|
}
|
|
};
|
|
|
|
let invalid_cases = [
|
|
naga::Interpolation::Flat,
|
|
naga::Interpolation::Linear,
|
|
naga::Interpolation::Perspective,
|
|
]
|
|
.into_iter()
|
|
.cartesian_product(
|
|
[
|
|
naga::Sampling::Either,
|
|
naga::Sampling::First,
|
|
naga::Sampling::Sample,
|
|
naga::Sampling::Center,
|
|
naga::Sampling::Centroid,
|
|
]
|
|
.into_iter()
|
|
.map(Some)
|
|
.chain([None]),
|
|
)
|
|
.filter_map(invalid_shader_module);
|
|
|
|
for (invalid_source, invalid_module, interpolation, sampling, interpolate_attr) in invalid_cases
|
|
{
|
|
let err = valid::Validator::new(Default::default(), valid::Capabilities::all())
|
|
.validate_no_overrides(&invalid_module)
|
|
.expect_err(&format!(
|
|
"module should be invalid for {interpolate_attr:?}"
|
|
));
|
|
assert!(dbg!(err.emit_to_string(&invalid_source)).contains(&dbg!(
|
|
naga::valid::VaryingError::InvalidInterpolationSamplingCombination {
|
|
interpolation,
|
|
sampling,
|
|
}
|
|
.to_string()
|
|
)),);
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "wgsl-in", feature = "glsl-out"))]
|
|
#[test]
|
|
fn no_flat_first_in_glsl() {
|
|
use dummy_interpolation_shader::DummyInterpolationShader;
|
|
|
|
let DummyInterpolationShader {
|
|
source: _,
|
|
module,
|
|
interpolate_attr,
|
|
entry_point,
|
|
} = DummyInterpolationShader::new(naga::Interpolation::Flat, Some(naga::Sampling::First));
|
|
|
|
let mut validator = naga::valid::Validator::new(Default::default(), Default::default());
|
|
let module_info = validator.validate(&module).unwrap();
|
|
|
|
let options = Default::default();
|
|
let pipeline_options = naga::back::glsl::PipelineOptions {
|
|
shader_stage: naga::ShaderStage::Fragment,
|
|
entry_point: entry_point.to_owned(),
|
|
multiview: None,
|
|
};
|
|
let mut glsl_writer = naga::back::glsl::Writer::new(
|
|
String::new(),
|
|
&module,
|
|
&module_info,
|
|
&options,
|
|
&pipeline_options,
|
|
Default::default(),
|
|
)
|
|
.unwrap();
|
|
|
|
let err = glsl_writer.write().expect_err(&format!(
|
|
"`{interpolate_attr}` should fail backend validation"
|
|
));
|
|
|
|
assert!(matches!(
|
|
err,
|
|
naga::back::glsl::Error::FirstSamplingNotSupported
|
|
));
|
|
}
|
|
|
|
#[cfg(all(test, feature = "wgsl-in"))]
|
|
mod dummy_interpolation_shader {
|
|
pub struct DummyInterpolationShader {
|
|
pub source: String,
|
|
pub module: naga::Module,
|
|
pub interpolate_attr: String,
|
|
pub entry_point: &'static str,
|
|
}
|
|
|
|
impl DummyInterpolationShader {
|
|
pub fn new(interpolation: naga::Interpolation, sampling: Option<naga::Sampling>) -> Self {
|
|
// NOTE: If you have to add variants below, make sure to add them to the
|
|
// `cartesian_product`'d combinations in tests around here!
|
|
let interpolation_str = match interpolation {
|
|
naga::Interpolation::Flat => "flat",
|
|
naga::Interpolation::Linear => "linear",
|
|
naga::Interpolation::Perspective => "perspective",
|
|
};
|
|
let sampling_str = match sampling {
|
|
None => String::new(),
|
|
Some(sampling) => format!(
|
|
", {}",
|
|
match sampling {
|
|
naga::Sampling::First => "first",
|
|
naga::Sampling::Either => "either",
|
|
naga::Sampling::Center => "center",
|
|
naga::Sampling::Centroid => "centroid",
|
|
naga::Sampling::Sample => "sample",
|
|
}
|
|
),
|
|
};
|
|
let member_type = match interpolation {
|
|
naga::Interpolation::Perspective | naga::Interpolation::Linear => "f32",
|
|
naga::Interpolation::Flat => "u32",
|
|
};
|
|
|
|
let interpolate_attr = format!("@interpolate({interpolation_str}{sampling_str})");
|
|
let source = format!(
|
|
"\
|
|
struct VertexOutput {{
|
|
@location(0) {interpolate_attr} member: {member_type},
|
|
}}
|
|
|
|
@fragment
|
|
fn main(input: VertexOutput) {{
|
|
// ...
|
|
}}
|
|
"
|
|
);
|
|
let module = naga::front::wgsl::parse_str(&source).unwrap();
|
|
|
|
Self {
|
|
source,
|
|
module,
|
|
interpolate_attr,
|
|
entry_point: "main",
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
struct BindingArrayFixture {
|
|
module: naga::Module,
|
|
span: naga::Span,
|
|
ty_u32: naga::Handle<naga::Type>,
|
|
ty_array: naga::Handle<naga::Type>,
|
|
ty_struct: naga::Handle<naga::Type>,
|
|
validator: naga::valid::Validator,
|
|
}
|
|
|
|
impl BindingArrayFixture {
|
|
fn new() -> Self {
|
|
let mut module = naga::Module::default();
|
|
let span = naga::Span::default();
|
|
let ty_u32 = module.types.insert(
|
|
naga::Type {
|
|
name: Some("u32".into()),
|
|
inner: naga::TypeInner::Scalar(naga::Scalar::U32),
|
|
},
|
|
span,
|
|
);
|
|
let ty_array = module.types.insert(
|
|
naga::Type {
|
|
name: Some("array<u32, 10>".into()),
|
|
inner: naga::TypeInner::Array {
|
|
base: ty_u32,
|
|
size: naga::ArraySize::Constant(std::num::NonZeroU32::new(10).unwrap()),
|
|
stride: 4,
|
|
},
|
|
},
|
|
span,
|
|
);
|
|
let ty_struct = module.types.insert(
|
|
naga::Type {
|
|
name: Some("S".into()),
|
|
inner: naga::TypeInner::Struct {
|
|
members: vec![naga::StructMember {
|
|
name: Some("m".into()),
|
|
ty: ty_u32,
|
|
binding: None,
|
|
offset: 0,
|
|
}],
|
|
span: 4,
|
|
},
|
|
},
|
|
span,
|
|
);
|
|
let validator = naga::valid::Validator::new(Default::default(), Default::default());
|
|
BindingArrayFixture {
|
|
module,
|
|
span,
|
|
ty_u32,
|
|
ty_array,
|
|
ty_struct,
|
|
validator,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn binding_arrays_hold_structs() {
|
|
let mut t = BindingArrayFixture::new();
|
|
let _binding_array = t.module.types.insert(
|
|
naga::Type {
|
|
name: Some("binding_array_of_struct".into()),
|
|
inner: naga::TypeInner::BindingArray {
|
|
base: t.ty_struct,
|
|
size: naga::ArraySize::Dynamic,
|
|
},
|
|
},
|
|
t.span,
|
|
);
|
|
|
|
assert!(t.validator.validate(&t.module).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn binding_arrays_cannot_hold_arrays() {
|
|
let mut t = BindingArrayFixture::new();
|
|
let _binding_array = t.module.types.insert(
|
|
naga::Type {
|
|
name: Some("binding_array_of_array".into()),
|
|
inner: naga::TypeInner::BindingArray {
|
|
base: t.ty_array,
|
|
size: naga::ArraySize::Dynamic,
|
|
},
|
|
},
|
|
t.span,
|
|
);
|
|
|
|
assert!(t.validator.validate(&t.module).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn binding_arrays_cannot_hold_scalars() {
|
|
let mut t = BindingArrayFixture::new();
|
|
let _binding_array = t.module.types.insert(
|
|
naga::Type {
|
|
name: Some("binding_array_of_scalar".into()),
|
|
inner: naga::TypeInner::BindingArray {
|
|
base: t.ty_u32,
|
|
size: naga::ArraySize::Dynamic,
|
|
},
|
|
},
|
|
t.span,
|
|
);
|
|
|
|
assert!(t.validator.validate(&t.module).is_err());
|
|
}
|
|
|
|
#[cfg(feature = "wgsl-in")]
|
|
#[test]
|
|
fn validation_error_messages() {
|
|
let cases = [(
|
|
r#"@group(0) @binding(0) var my_sampler: sampler;
|
|
|
|
fn foo(tex: texture_2d<f32>) -> vec4<f32> {
|
|
return textureSampleLevel(tex, my_sampler, vec2f(0, 0), 0.0);
|
|
}
|
|
|
|
fn main() {
|
|
foo();
|
|
}
|
|
"#,
|
|
"\
|
|
error: Function [1] 'main' is invalid
|
|
┌─ wgsl:7:17
|
|
│ \n7 │ ╭ fn main() {
|
|
8 │ │ foo();
|
|
│ │ ^^^^ invalid function call
|
|
│ ╰──────────────────────────^ naga::Function [1]
|
|
│ \n = Call to [0] is invalid
|
|
= Requires 1 arguments, but 0 are provided
|
|
|
|
",
|
|
)];
|
|
|
|
for (source, expected_err) in cases {
|
|
let module = naga::front::wgsl::parse_str(source).unwrap();
|
|
let err = valid::Validator::new(Default::default(), valid::Capabilities::all())
|
|
.validate_no_overrides(&module)
|
|
.expect_err("module should be invalid");
|
|
println!("{}", err.emit_to_string(source));
|
|
assert_eq!(err.emit_to_string(source), expected_err);
|
|
}
|
|
}
|