Skip to content

Commit 5f9d9bc

Browse files
fix: reject fragment shader output locations > max_color_attachments limit
1 parent 2e2d9b5 commit 5f9d9bc

File tree

4 files changed

+98
-2
lines changed

4 files changed

+98
-2
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ This is a breaking change
5454

5555
By @R-Cramer4 in [#8230](https://github.com/gfx-rs/wgpu/pull/8230)
5656

57+
### Bug Fixes
58+
59+
#### General
60+
61+
- Reject fragment shader output `location`s > `max_color_attachments` limit. By @ErichDonGubler in [#8316](https://github.com/gfx-rs/wgpu/pull/8316).
62+
5763
## v27.0.2 (2025-10-03)
5864

5965
### Bug Fixes

tests/tests/wgpu-validation/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ mod device;
77
mod experimental;
88
mod external_texture;
99
mod instance;
10+
mod render_pipeline;
1011
mod texture;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//! Tests of [`wgpu::RenderPipeline`] and related.
2+
3+
use wgpu_hal::MAX_COLOR_ATTACHMENTS;
4+
use wgpu_test::fail;
5+
6+
#[test]
7+
fn reject_fragment_shader_output_over_max_color_attachments() {
8+
let (device, _queue) = wgpu::Device::noop(&Default::default());
9+
10+
// NOTE: Vertex shader is a boring quad. The fragment shader is the interesting part.
11+
let source = format!(
12+
"\
13+
@vertex
14+
fn vert(@builtin(vertex_index) vertex_index : u32) -> @builtin(position) vec4f {{
15+
var pos = array<vec2f, 3>(
16+
vec2(0.0, 0.5),
17+
vec2(-0.5, -0.5),
18+
vec2(0.5, -0.5)
19+
);
20+
return vec4f(pos[vertex_index], 0.0, 1.0);
21+
}}
22+
23+
@fragment
24+
fn frag() -> @location({MAX_COLOR_ATTACHMENTS}) vec4f {{
25+
return vec4(1.0, 0.0, 0.0, 1.0);
26+
}}
27+
"
28+
);
29+
30+
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
31+
label: None,
32+
source: wgpu::ShaderSource::Wgsl(source.into()),
33+
});
34+
let module = &module;
35+
36+
fail(
37+
&device,
38+
|| {
39+
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
40+
layout: None,
41+
label: None,
42+
vertex: wgpu::VertexState {
43+
module,
44+
entry_point: None,
45+
compilation_options: Default::default(),
46+
buffers: &[],
47+
},
48+
fragment: Some(wgpu::FragmentState {
49+
module,
50+
entry_point: None,
51+
compilation_options: Default::default(),
52+
targets: &[Some(wgpu::ColorTargetState {
53+
format: wgpu::TextureFormat::Rgba8Unorm,
54+
blend: None,
55+
write_mask: Default::default(),
56+
})],
57+
}),
58+
primitive: Default::default(),
59+
depth_stencil: None,
60+
multisample: Default::default(),
61+
multiview: None,
62+
cache: None,
63+
})
64+
},
65+
Some(concat!(
66+
"Location[8] Float32x4 interpolated as Some(Perspective) ",
67+
"with sampling Some(Center)'s index exceeds `max_color_attachments` (8)"
68+
)),
69+
);
70+
}

wgpu-core/src/validation.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,11 @@ pub enum StageError {
313313
MultipleEntryPointsFound,
314314
#[error(transparent)]
315315
InvalidResource(#[from] InvalidResourceError),
316+
#[error(
317+
"Location[{location}] {var}'s index exceeds `max_color_attachments` ({})",
318+
hal::MAX_COLOR_ATTACHMENTS
319+
)]
320+
ColorAttachmentLocationExceedsLimit { location: u32, var: InterfaceVar },
316321
}
317322

318323
impl WebGpuError for StageError {
@@ -334,7 +339,8 @@ impl WebGpuError for StageError {
334339
| Self::TooManyVaryings { .. }
335340
| Self::MissingEntryPoint(..)
336341
| Self::NoEntryPointFound
337-
| Self::MultipleEntryPointsFound => return ErrorType::Validation,
342+
| Self::MultipleEntryPointsFound
343+
| Self::ColorAttachmentLocationExceedsLimit { .. } => return ErrorType::Validation,
338344
};
339345
e.webgpu_error_type()
340346
}
@@ -1317,7 +1323,6 @@ impl Interface {
13171323
}
13181324
}
13191325

1320-
#[expect(clippy::single_match)]
13211326
match shader_stage {
13221327
naga::ShaderStage::Vertex => {
13231328
for output in entry_point.outputs.iter() {
@@ -1352,6 +1357,19 @@ impl Interface {
13521357
}
13531358
}
13541359
}
1360+
naga::ShaderStage::Fragment => {
1361+
for output in &entry_point.outputs {
1362+
let &Varying::Local { location, ref iv } = output else {
1363+
continue;
1364+
};
1365+
if location >= self.limits.max_color_attachments {
1366+
return Err(StageError::ColorAttachmentLocationExceedsLimit {
1367+
location,
1368+
var: iv.clone(),
1369+
});
1370+
}
1371+
}
1372+
}
13551373
_ => (),
13561374
}
13571375

@@ -1370,6 +1388,7 @@ impl Interface {
13701388
Varying::BuiltIn(_) => None,
13711389
})
13721390
.collect();
1391+
13731392
Ok(outputs)
13741393
}
13751394

0 commit comments

Comments
 (0)