diff --git a/CHANGELOG.md b/CHANGELOG.md index c66b4dc5efb..fe9c74127b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Bottom level categories: ### New Features - Added support for cooperative load/store operations in shaders. Currently only WGSL on the input and SPIR-V, METAL, and WGSL on the output are supported. By @kvark in [#8251](https://github.com/gfx-rs/wgpu/issues/8251). +- Added support for per-vertex attributes in fragment shaders. Currently only WGSL input is supported, and only SPIR-V or WGSL output is supported. By @atlv24 in [#8821](https://github.com/gfx-rs/wgpu/issues/8821). - Added support for no-perspective barycentric coordinates. By @atlv24 in [#8852](https://github.com/gfx-rs/wgpu/issues/8852). - Added support for obtaining `AdapterInfo` from `Device`. By @sagudev in [#8807](https://github.com/gfx-rs/wgpu/pull/8807). - Added `Limits::or_worse_values_from`. By @atlv24 in [#8870](https://github.com/gfx-rs/wgpu/pull/8870). diff --git a/naga/src/back/glsl/conv.rs b/naga/src/back/glsl/conv.rs index ce3da767a9d..f9393dfeed7 100644 --- a/naga/src/back/glsl/conv.rs +++ b/naga/src/back/glsl/conv.rs @@ -160,6 +160,7 @@ pub(in crate::back::glsl) const fn glsl_interpolation( I::Perspective => "smooth", I::Linear => "noperspective", I::Flat => "flat", + I::PerVertex => unreachable!(), } } diff --git a/naga/src/back/hlsl/conv.rs b/naga/src/back/hlsl/conv.rs index b8bbbc61afa..47532b9a3fc 100644 --- a/naga/src/back/hlsl/conv.rs +++ b/naga/src/back/hlsl/conv.rs @@ -205,6 +205,7 @@ impl crate::Interpolation { Self::Perspective => None, Self::Linear => Some("noperspective"), Self::Flat => Some("nointerpolation"), + Self::PerVertex => unreachable!(), } } } diff --git a/naga/src/back/spv/writer.rs b/naga/src/back/spv/writer.rs index c62cf295269..8be07a2fe1f 100644 --- a/naga/src/back/spv/writer.rs +++ b/naga/src/back/spv/writer.rs @@ -2987,6 +2987,14 @@ impl Writer { Some(crate::Interpolation::Linear) => { others.push(Decoration::NoPerspective); } + Some(crate::Interpolation::PerVertex) => { + others.push(Decoration::PerVertexKHR); + self.require_any( + "`per_vertex` interpolation", + &[spirv::Capability::FragmentBarycentricKHR], + )?; + self.use_extension("SPV_KHR_fragment_shader_barycentric"); + } } match sampling { // Center sampling is the default in SPIR-V. diff --git a/naga/src/common/wgsl/to_wgsl.rs b/naga/src/common/wgsl/to_wgsl.rs index 1c1260b6ba7..c434c657715 100644 --- a/naga/src/common/wgsl/to_wgsl.rs +++ b/naga/src/common/wgsl/to_wgsl.rs @@ -211,6 +211,7 @@ impl ToWgsl for crate::Interpolation { crate::Interpolation::Perspective => "perspective", crate::Interpolation::Linear => "linear", crate::Interpolation::Flat => "flat", + crate::Interpolation::PerVertex => "per_vertex", } } } diff --git a/naga/src/front/spv/mod.rs b/naga/src/front/spv/mod.rs index c677e76737e..340d0594a6c 100644 --- a/naga/src/front/spv/mod.rs +++ b/naga/src/front/spv/mod.rs @@ -96,6 +96,7 @@ pub const SUPPORTED_EXTENSIONS: &[&str] = &[ "SPV_EXT_descriptor_indexing", "SPV_EXT_shader_atomic_float_add", "SPV_KHR_16bit_storage", + "SPV_KHR_fragment_shader_barycentric", ]; pub const SUPPORTED_EXT_SETS: &[&str] = &["GLSL.std.450"]; @@ -778,6 +779,9 @@ impl> Frontend { spirv::Decoration::Flat => { dec.interpolation = Some(crate::Interpolation::Flat); } + spirv::Decoration::PerVertexKHR => { + dec.interpolation = Some(crate::Interpolation::PerVertex); + } spirv::Decoration::Centroid => { dec.sampling = Some(crate::Sampling::Centroid); } diff --git a/naga/src/front/wgsl/parse/conv.rs b/naga/src/front/wgsl/parse/conv.rs index ca97554e708..918aa9f04dc 100644 --- a/naga/src/front/wgsl/parse/conv.rs +++ b/naga/src/front/wgsl/parse/conv.rs @@ -112,6 +112,7 @@ pub fn map_interpolation(word: &str, span: Span) -> Result<'_, crate::Interpolat "linear" => Ok(crate::Interpolation::Linear), "flat" => Ok(crate::Interpolation::Flat), "perspective" => Ok(crate::Interpolation::Perspective), + "per_vertex" => Ok(crate::Interpolation::PerVertex), _ => Err(Box::new(Error::UnknownAttribute(span))), } } diff --git a/naga/src/ir/mod.rs b/naga/src/ir/mod.rs index ca195c5b6ce..adf22ca6784 100644 --- a/naga/src/ir/mod.rs +++ b/naga/src/ir/mod.rs @@ -588,6 +588,9 @@ pub enum Interpolation { Linear, /// Indicates that no interpolation will be performed. Flat, + /// Indicates the fragment input binding holds an array of per-vertex values. + /// This is typically used with barycentrics. + PerVertex, } /// The sampling qualifiers of a binding or struct field. diff --git a/naga/src/valid/interface.rs b/naga/src/valid/interface.rs index d1e8864efdb..51b5f83571c 100644 --- a/naga/src/valid/interface.rs +++ b/naga/src/valid/interface.rs @@ -58,6 +58,8 @@ pub enum VaryingError { NotIOShareableType(Handle), #[error("Interpolation is not valid")] InvalidInterpolation, + #[error("Interpolation {0:?} is only valid for stage {1:?}")] + InvalidInterpolationInStage(crate::Interpolation, crate::ShaderStage), #[error("Cannot combine {interpolation:?} interpolation with the {sampling:?} sample type")] InvalidInterpolationSamplingCombination { interpolation: crate::Interpolation, @@ -100,6 +102,8 @@ pub enum VaryingError { InvalidPerPrimitive, #[error("Non-builtin members of a mesh primitive output struct must be decorated with `@per_primitive`")] MissingPerPrimitive, + #[error("Per vertex fragment inputs must be an array of length 3.")] + PerVertexNotArrayOfThree, } #[derive(Clone, Debug, thiserror::Error)] @@ -447,6 +451,33 @@ impl VaryingContext<'_> { Capabilities::MESH_SHADER, )); } + if interpolation == Some(crate::Interpolation::PerVertex) { + if self.stage != crate::ShaderStage::Fragment { + return Err(VaryingError::InvalidInterpolationInStage( + crate::Interpolation::PerVertex, + crate::ShaderStage::Fragment, + )); + } + if !self.capabilities.contains(Capabilities::PER_VERTEX) { + return Err(VaryingError::UnsupportedCapability( + Capabilities::PER_VERTEX, + )); + } + } + // If this is per-vertex, we change the type we validate to the inner type, otherwise we leave it be. + // This lets all validation be done on the inner type once we've ensured the per-vertex is array + let (ty, ty_inner) = if interpolation == Some(crate::Interpolation::PerVertex) { + let three = crate::ArraySize::Constant(core::num::NonZeroU32::new(3).unwrap()); + match ty_inner { + &Ti::Array { base, size, .. } if size == three => { + (base, &self.types[base].inner) + } + _ => return Err(VaryingError::PerVertexNotArrayOfThree), + } + } else { + (ty, ty_inner) + }; + // Only IO-shareable types may be stored in locations. if !self.type_info[ty.index()] .flags @@ -554,19 +585,22 @@ impl VaryingContext<'_> { return Err(VaryingError::UnsupportedCapability(required)); } - match ty_inner.scalar_kind() { - Some(crate::ScalarKind::Float) => { - if needs_interpolation && interpolation.is_none() { - return Err(VaryingError::MissingInterpolation); + if interpolation != Some(crate::Interpolation::PerVertex) { + match ty_inner.scalar_kind() { + Some(crate::ScalarKind::Float) => { + if needs_interpolation && interpolation.is_none() { + return Err(VaryingError::MissingInterpolation); + } } - } - Some(_) => { - if needs_interpolation && interpolation != Some(crate::Interpolation::Flat) - { - return Err(VaryingError::InvalidInterpolation); + Some(_) => { + if needs_interpolation + && interpolation != Some(crate::Interpolation::Flat) + { + return Err(VaryingError::InvalidInterpolation); + } } + None => return Err(VaryingError::InvalidType(ty)), } - None => return Err(VaryingError::InvalidType(ty)), } } } diff --git a/naga/src/valid/mod.rs b/naga/src/valid/mod.rs index 984a47262b5..348d4c2d27b 100644 --- a/naga/src/valid/mod.rs +++ b/naga/src/valid/mod.rs @@ -203,6 +203,8 @@ bitflags::bitflags! { const STORAGE_BUFFER_BINDING_ARRAY_NON_UNIFORM_INDEXING = 1 << 35; /// Support for cooperative matrix types and operations const COOPERATIVE_MATRIX = 1 << 36; + /// Support for per-vertex fragment input. + const PER_VERTEX = 1 << 37; } } diff --git a/naga/tests/in/spv/per-vertex.spvasm b/naga/tests/in/spv/per-vertex.spvasm new file mode 100644 index 00000000000..81abac09cee --- /dev/null +++ b/naga/tests/in/spv/per-vertex.spvasm @@ -0,0 +1,39 @@ +; SPIR-V +; Version: 1.1 +; Generator: rspirv +; Bound: 22 +OpCapability Shader +OpCapability FragmentBarycentricKHR +OpExtension "SPV_KHR_fragment_shader_barycentric" +%1 = OpExtInstImport "GLSL.std.450" +OpMemoryModel Logical GLSL450 +OpEntryPoint Fragment %14 "fs_main" %9 %12 +OpExecutionMode %14 OriginUpperLeft +OpDecorate %4 ArrayStride 4 +OpDecorate %9 Location 0 +OpDecorate %9 PerVertexKHR +OpDecorate %12 Location 0 +%2 = OpTypeVoid +%3 = OpTypeFloat 32 +%6 = OpTypeInt 32 0 +%5 = OpConstant %6 3 +%4 = OpTypeArray %3 %5 +%7 = OpTypeVector %3 4 +%10 = OpTypePointer Input %4 +%9 = OpVariable %10 Input +%13 = OpTypePointer Output %7 +%12 = OpVariable %13 Output +%15 = OpTypeFunction %2 +%16 = OpConstant %3 1 +%14 = OpFunction %2 None %15 +%8 = OpLabel +%11 = OpLoad %4 %9 +OpBranch %17 +%17 = OpLabel +%18 = OpCompositeExtract %3 %11 0 +%19 = OpCompositeExtract %3 %11 1 +%20 = OpCompositeExtract %3 %11 2 +%21 = OpCompositeConstruct %7 %18 %19 %20 %16 +OpStore %12 %21 +OpReturn +OpFunctionEnd \ No newline at end of file diff --git a/naga/tests/in/spv/per-vertex.toml b/naga/tests/in/spv/per-vertex.toml new file mode 100644 index 00000000000..0de3e5d18b5 --- /dev/null +++ b/naga/tests/in/spv/per-vertex.toml @@ -0,0 +1,2 @@ +capabilities = "PER_VERTEX" +targets = "SPIRV | WGSL" diff --git a/naga/tests/in/wgsl/per-vertex.toml b/naga/tests/in/wgsl/per-vertex.toml new file mode 100644 index 00000000000..ca0449904b6 --- /dev/null +++ b/naga/tests/in/wgsl/per-vertex.toml @@ -0,0 +1,2 @@ +capabilities = "PER_VERTEX" +targets = "WGSL | SPIRV" diff --git a/naga/tests/in/wgsl/per-vertex.wgsl b/naga/tests/in/wgsl/per-vertex.wgsl new file mode 100644 index 00000000000..6a7c09ba885 --- /dev/null +++ b/naga/tests/in/wgsl/per-vertex.wgsl @@ -0,0 +1,4 @@ +@fragment +fn fs_main(@location(0) @interpolate(per_vertex) v: array) -> @location(0) vec4 { + return vec4(v[0], v[1], v[2], 1.0); +} diff --git a/naga/tests/naga/validation.rs b/naga/tests/naga/validation.rs index 789c3a1ec20..238938ed954 100644 --- a/naga/tests/naga/validation.rs +++ b/naga/tests/naga/validation.rs @@ -409,6 +409,7 @@ fn incompatible_interpolation_and_sampling_types() { naga::Interpolation::Flat, naga::Interpolation::Linear, naga::Interpolation::Perspective, + naga::Interpolation::PerVertex, ] .into_iter() .cartesian_product( @@ -498,6 +499,7 @@ mod dummy_interpolation_shader { naga::Interpolation::Flat => "flat", naga::Interpolation::Linear => "linear", naga::Interpolation::Perspective => "perspective", + naga::Interpolation::PerVertex => "per_vertex", }; let sampling_str = match sampling { None => String::new(), @@ -515,6 +517,7 @@ mod dummy_interpolation_shader { let member_type = match interpolation { naga::Interpolation::Perspective | naga::Interpolation::Linear => "f32", naga::Interpolation::Flat => "u32", + naga::Interpolation::PerVertex => "array", }; let interpolate_attr = format!("@interpolate({interpolation_str}{sampling_str})"); diff --git a/naga/tests/naga/wgsl_errors.rs b/naga/tests/naga/wgsl_errors.rs index 743889aeb05..96c4da2cdbd 100644 --- a/naga/tests/naga/wgsl_errors.rs +++ b/naga/tests/naga/wgsl_errors.rs @@ -1209,6 +1209,31 @@ fn int64_capability() { } } +#[test] +fn per_vertex_capability() { + check_validation! { + r#" + @fragment + fn fs_main(@location(0) @interpolate(per_vertex) v: array) -> @location(0) vec4 { + return vec4(v[0], v[1], v[2], 1.0); + } + "#: + Err( + naga::valid::ValidationError::EntryPoint { + stage: naga::ShaderStage::Fragment, + source: valid::EntryPointError::Argument( + 0, + valid::VaryingError::UnsupportedCapability( + Capabilities::PER_VERTEX, + + ), + ), + .. + }, + ) + } +} + #[test] fn multiple_enables_valid() { check_success( diff --git a/naga/tests/out/spv/spv-per-vertex.spvasm b/naga/tests/out/spv/spv-per-vertex.spvasm new file mode 100644 index 00000000000..66bfc15e73c --- /dev/null +++ b/naga/tests/out/spv/spv-per-vertex.spvasm @@ -0,0 +1,56 @@ +; SPIR-V +; Version: 1.1 +; Generator: rspirv +; Bound: 34 +OpCapability Shader +OpCapability FragmentBarycentricKHR +OpExtension "SPV_KHR_fragment_shader_barycentric" +%1 = OpExtInstImport "GLSL.std.450" +OpMemoryModel Logical GLSL450 +OpEntryPoint Fragment %30 "fs_main" %25 %28 +OpExecutionMode %30 OriginUpperLeft +OpDecorate %4 ArrayStride 4 +OpDecorate %25 Location 0 +OpDecorate %25 PerVertexKHR +OpDecorate %28 Location 0 +%2 = OpTypeVoid +%3 = OpTypeFloat 32 +%6 = OpTypeInt 32 0 +%5 = OpConstant %6 3 +%4 = OpTypeArray %3 %5 +%7 = OpTypeVector %3 4 +%8 = OpConstant %3 1 +%10 = OpTypePointer Private %4 +%11 = OpConstantNull %4 +%9 = OpVariable %10 Private %11 +%13 = OpTypePointer Private %7 +%14 = OpConstantNull %7 +%12 = OpVariable %13 Private %14 +%17 = OpTypeFunction %2 +%26 = OpTypePointer Input %4 +%25 = OpVariable %26 Input +%29 = OpTypePointer Output %7 +%28 = OpVariable %29 Output +%16 = OpFunction %2 None %17 +%15 = OpLabel +OpBranch %18 +%18 = OpLabel +%19 = OpLoad %4 %9 +%20 = OpCompositeExtract %3 %19 0 +%21 = OpCompositeExtract %3 %19 1 +%22 = OpCompositeExtract %3 %19 2 +%23 = OpCompositeConstruct %7 %20 %21 %22 %8 +OpStore %12 %23 +OpReturn +OpFunctionEnd +%30 = OpFunction %2 None %17 +%24 = OpLabel +%27 = OpLoad %4 %25 +OpBranch %31 +%31 = OpLabel +OpStore %9 %27 +%32 = OpFunctionCall %2 %16 +%33 = OpLoad %7 %12 +OpStore %28 %33 +OpReturn +OpFunctionEnd \ No newline at end of file diff --git a/naga/tests/out/spv/wgsl-per-vertex.spvasm b/naga/tests/out/spv/wgsl-per-vertex.spvasm new file mode 100644 index 00000000000..81abac09cee --- /dev/null +++ b/naga/tests/out/spv/wgsl-per-vertex.spvasm @@ -0,0 +1,39 @@ +; SPIR-V +; Version: 1.1 +; Generator: rspirv +; Bound: 22 +OpCapability Shader +OpCapability FragmentBarycentricKHR +OpExtension "SPV_KHR_fragment_shader_barycentric" +%1 = OpExtInstImport "GLSL.std.450" +OpMemoryModel Logical GLSL450 +OpEntryPoint Fragment %14 "fs_main" %9 %12 +OpExecutionMode %14 OriginUpperLeft +OpDecorate %4 ArrayStride 4 +OpDecorate %9 Location 0 +OpDecorate %9 PerVertexKHR +OpDecorate %12 Location 0 +%2 = OpTypeVoid +%3 = OpTypeFloat 32 +%6 = OpTypeInt 32 0 +%5 = OpConstant %6 3 +%4 = OpTypeArray %3 %5 +%7 = OpTypeVector %3 4 +%10 = OpTypePointer Input %4 +%9 = OpVariable %10 Input +%13 = OpTypePointer Output %7 +%12 = OpVariable %13 Output +%15 = OpTypeFunction %2 +%16 = OpConstant %3 1 +%14 = OpFunction %2 None %15 +%8 = OpLabel +%11 = OpLoad %4 %9 +OpBranch %17 +%17 = OpLabel +%18 = OpCompositeExtract %3 %11 0 +%19 = OpCompositeExtract %3 %11 1 +%20 = OpCompositeExtract %3 %11 2 +%21 = OpCompositeConstruct %7 %18 %19 %20 %16 +OpStore %12 %21 +OpReturn +OpFunctionEnd \ No newline at end of file diff --git a/naga/tests/out/wgsl/spv-per-vertex.wgsl b/naga/tests/out/wgsl/spv-per-vertex.wgsl new file mode 100644 index 00000000000..3841e9537b3 --- /dev/null +++ b/naga/tests/out/wgsl/spv-per-vertex.wgsl @@ -0,0 +1,16 @@ +var global: array; +var global_1: vec4; + +fn function() { + let _e3 = global; + global_1 = vec4(_e3[0], _e3[1], _e3[2], 1f); + return; +} + +@fragment +fn fs_main(@location(0) @interpolate(per_vertex) param: array) -> @location(0) vec4 { + global = param; + function(); + let _e3 = global_1; + return _e3; +} diff --git a/naga/tests/out/wgsl/wgsl-per-vertex.wgsl b/naga/tests/out/wgsl/wgsl-per-vertex.wgsl new file mode 100644 index 00000000000..dd45184d7c6 --- /dev/null +++ b/naga/tests/out/wgsl/wgsl-per-vertex.wgsl @@ -0,0 +1,4 @@ +@fragment +fn fs_main(@location(0) @interpolate(per_vertex) v: array) -> @location(0) vec4 { + return vec4(v[0], v[1], v[2], 1f); +} diff --git a/tests/tests/wgpu-gpu/main.rs b/tests/tests/wgpu-gpu/main.rs index 77444f63f0c..235de5091e8 100644 --- a/tests/tests/wgpu-gpu/main.rs +++ b/tests/tests/wgpu-gpu/main.rs @@ -42,6 +42,7 @@ mod occlusion_query; mod oob_indexing; mod oom; mod pass_ops; +mod per_vertex; mod pipeline; mod pipeline_cache; mod planar_texture; @@ -106,6 +107,7 @@ fn all_tests() -> Vec { oob_indexing::all_tests(&mut tests); oom::all_tests(&mut tests); pass_ops::all_tests(&mut tests); + per_vertex::all_tests(&mut tests); pipeline_cache::all_tests(&mut tests); pipeline::all_tests(&mut tests); planar_texture::all_tests(&mut tests); diff --git a/tests/tests/wgpu-gpu/per_vertex/mod.rs b/tests/tests/wgpu-gpu/per_vertex/mod.rs new file mode 100644 index 00000000000..c63a050b61f --- /dev/null +++ b/tests/tests/wgpu-gpu/per_vertex/mod.rs @@ -0,0 +1,179 @@ +use wgpu::util::DeviceExt; +use wgpu_test::{gpu_test, GpuTestConfiguration, TestParameters, TestingContext}; + +pub fn all_tests(vec: &mut Vec) { + vec.push(PER_VERTEX); +} + +// +// These tests render a triangle strip to a 2x2 render target. The first triangle +// in the vertex buffer covers the top-left pixel, the second triangle +// covers the bottom two pixels, and the last triangle covers the top-right pixel. +// XY layout of the render target, with the three triangles, pixel centers marked with ' +// +// (-1,1) (0,1) (1,1) +// +---------+---------+ +// | o-------o-------o | +// | | /|\ | | +// | | ' / | \ ' | | +// | | / | \ | | +// (-1,0) +-|---/---+---\---|-+ (1,0) +// | | / | \ | | +// | | / | \ | | +// | |/ ' | ' \| | +// | o---------------o | +// +---------+---------+ +// (-1,-1) (0,-1) (1,-1) +// +// The fragment shader outputs color based on per-vertex position: +// +// return vec4(z[0], z[1], z[2], 1.0); +// + +#[gpu_test] +static PER_VERTEX: GpuTestConfiguration = GpuTestConfiguration::new() + .parameters( + TestParameters::default() + .test_features_limits() + .features(wgpu::Features::SHADER_PER_VERTEX), + ) + .run_async(per_vertex); + +async fn per_vertex(ctx: TestingContext) { + let shader = ctx + .device + .create_shader_module(wgpu::include_wgsl!("per_vertex.wgsl")); + + let trianglestrip_xyz: [f32; 15] = [ + -0.9, 0.9, 0.0, // top left + -0.9, -0.9, 0.25, // bottom left + 0.0, 0.9, 0.5, // top center + 0.9, -0.9, 0.75, // bottom right + 0.9, 0.9, 1.0, // top right + ]; + let vertex_buffer = ctx + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: None, + contents: bytemuck::cast_slice(&trianglestrip_xyz), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + + let indices = [0u32, 1, 2, 3, 4]; + let index_buffer = ctx + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: None, + contents: bytemuck::cast_slice(&indices), + usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, + }); + + let pipeline = ctx + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: None, + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[wgpu::VertexBufferLayout { + array_stride: 12, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x3, + offset: 0, + shader_location: 0, + }], + }], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + strip_index_format: Some(wgpu::IndexFormat::Uint32), + ..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_mask: None, + cache: None, + }); + + let width = 2; + let height = 2; + let texture_size = wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + let color_texture = ctx.device.create_texture(&wgpu::TextureDescriptor { + label: None, + size: texture_size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let readback_buffer = wgpu_test::image::ReadbackBuffers::new(&ctx.device, &color_texture); + + let mut encoder = ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), + store: wgpu::StoreOp::Store, + }, + resolve_target: None, + view: &color_view, + depth_slice: None, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + multiview_mask: None, + }); + + rpass.set_pipeline(&pipeline); + rpass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32); + rpass.set_vertex_buffer(0, vertex_buffer.slice(..)); + rpass.draw(0..5, 0..1); + } + readback_buffer.copy_from(&ctx.device, &mut encoder, &color_texture); + ctx.queue.submit(Some(encoder.finish())); + + // 0 127 255 + // o-----o-----o + // | /|\ | + // | '/ | \' | + // +--/--+--\--+ + // | / | \ | + // |/ ' | ' \| + // o-----+-----o + // 64 191 + let expected = [ + 0, 64, 127, 255, // top left + 127, 191, 255, 255, // top right + 64, 191, 127, 255, // bottom left + 64, 191, 127, 255, // bottom right + ]; + readback_buffer + .assert_buffer_contents(&ctx, &expected) + .await; +} diff --git a/tests/tests/wgpu-gpu/per_vertex/per_vertex.wgsl b/tests/tests/wgpu-gpu/per_vertex/per_vertex.wgsl new file mode 100644 index 00000000000..c0b4f244807 --- /dev/null +++ b/tests/tests/wgpu-gpu/per_vertex/per_vertex.wgsl @@ -0,0 +1,14 @@ +struct VertexOutput { + @builtin(position) clip: vec4, + @interpolate(flat) @location(0) z: f32, +} + +@vertex +fn vs_main(@location(0) xyz: vec3) -> VertexOutput { + return VertexOutput(vec4(xyz.xy, 0.0, 1.0), xyz.z); +} + +@fragment +fn fs_main(@interpolate(per_vertex) @location(0) z: array) -> @location(0) vec4 { + return vec4(z[0], z[1], z[2], 1.0); +} diff --git a/wgpu-core/src/device/mod.rs b/wgpu-core/src/device/mod.rs index ddec6eab90a..fbe0a75e1a5 100644 --- a/wgpu-core/src/device/mod.rs +++ b/wgpu-core/src/device/mod.rs @@ -543,6 +543,10 @@ pub fn create_validator( Caps::COOPERATIVE_MATRIX, features.intersects(wgt::Features::EXPERIMENTAL_COOPERATIVE_MATRIX), ); + caps.set( + Caps::PER_VERTEX, + features.intersects(wgt::Features::SHADER_PER_VERTEX), + ); naga::valid::Validator::new(flags, caps) } diff --git a/wgpu-hal/src/vulkan/adapter.rs b/wgpu-hal/src/vulkan/adapter.rs index 796f660e34d..027dbc4b263 100644 --- a/wgpu-hal/src/vulkan/adapter.rs +++ b/wgpu-hal/src/vulkan/adapter.rs @@ -563,7 +563,9 @@ impl PhysicalDeviceFeatures { shader_barycentrics: if enabled_extensions .contains(&khr::fragment_shader_barycentric::NAME) { - let needed = requested_features.intersects(wgt::Features::SHADER_BARYCENTRICS); + let needed = requested_features.intersects( + wgt::Features::SHADER_BARYCENTRICS | wgt::Features::SHADER_PER_VERTEX, + ); Some( vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR::default() .fragment_shader_barycentric(needed), @@ -745,7 +747,7 @@ impl PhysicalDeviceFeatures { if let Some(ref shader_barycentrics) = self.shader_barycentrics { features.set( - F::SHADER_BARYCENTRICS, + F::SHADER_BARYCENTRICS | F::SHADER_PER_VERTEX, shader_barycentrics.fragment_shader_barycentric != 0, ); } @@ -1292,8 +1294,11 @@ impl PhysicalDeviceProperties { extensions.push(ext::mesh_shader::NAME); } - // Require `VK_KHR_fragment_shader_barycentric` if the associated feature was requested - if requested_features.intersects(wgt::Features::SHADER_BARYCENTRICS) { + // Require `VK_KHR_fragment_shader_barycentric` if an associated feature was requested + // Vulkan bundles both barycentrics and per-vertex attributes under the same feature. + if requested_features + .intersects(wgt::Features::SHADER_BARYCENTRICS | wgt::Features::SHADER_PER_VERTEX) + { extensions.push(khr::fragment_shader_barycentric::NAME); } @@ -2341,7 +2346,10 @@ impl super::Adapter { capabilities.push(spv::Capability::ClipDistance); } - if features.intersects(wgt::Features::SHADER_BARYCENTRICS) { + // Vulkan bundles both barycentrics and per-vertex attributes under the same feature. + if features + .intersects(wgt::Features::SHADER_BARYCENTRICS | wgt::Features::SHADER_PER_VERTEX) + { capabilities.push(spv::Capability::FragmentBarycentricKHR); } diff --git a/wgpu-types/src/features.rs b/wgpu-types/src/features.rs index 99e173a5aeb..4be871f398f 100644 --- a/wgpu-types/src/features.rs +++ b/wgpu-types/src/features.rs @@ -1274,6 +1274,14 @@ bitflags_array! { /// /// This is a native only feature. const EXPERIMENTAL_COOPERATIVE_MATRIX = 1 << 57; + + /// Enables shader per-vertex attributes. + /// + /// Supported platforms: + /// - Vulkan (with VK_KHR_fragment_shader_barycentric) + /// + /// This is a native only feature. + const SHADER_PER_VERTEX = 1 << 58; } /// Features that are not guaranteed to be supported.