Skip to content

Commit 48757bb

Browse files
fix: reject fragment shader output locations > max_color_attachments limit
1 parent d4411f0 commit 48757bb

File tree

4 files changed

+102
-2
lines changed

4 files changed

+102
-2
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ SamplerDescriptor {
7373
}
7474
```
7575

76+
### Bug Fixes
77+
78+
#### General
79+
80+
- Reject fragment shader output `location`s > `max_color_attachments` limit. By @ErichDonGubler in [#8316](https://github.com/gfx-rs/wgpu/pull/8316).
81+
7682
## v27.0.2 (2025-10-03)
7783

7884
### 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_test::fail;
4+
5+
#[test]
6+
fn reject_fragment_shader_output_over_max_color_attachments() {
7+
let (device, _queue) = wgpu::Device::noop(&Default::default());
8+
9+
// NOTE: Vertex shader is a boring quad. The fragment shader is the interesting part.
10+
let source = format!(
11+
"\
12+
@vertex
13+
fn vert(@builtin(vertex_index) vertex_index : u32) -> @builtin(position) vec4f {{
14+
var pos = array<vec2f, 3>(
15+
vec2(0.0, 0.5),
16+
vec2(-0.5, -0.5),
17+
vec2(0.5, -0.5)
18+
);
19+
return vec4f(pos[vertex_index], 0.0, 1.0);
20+
}}
21+
22+
@fragment
23+
fn frag() -> @location({}) vec4f {{
24+
return vec4(1.0, 0.0, 0.0, 1.0);
25+
}}
26+
",
27+
device.limits().max_color_attachments
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: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,14 @@ pub enum StageError {
313313
MultipleEntryPointsFound,
314314
#[error(transparent)]
315315
InvalidResource(#[from] InvalidResourceError),
316+
#[error(
317+
"Location[{location}] {var}'s index exceeds the `max_color_attachments` limit ({limit})"
318+
)]
319+
ColorAttachmentLocationTooLarge {
320+
location: u32,
321+
var: InterfaceVar,
322+
limit: u32,
323+
},
316324
}
317325

318326
impl WebGpuError for StageError {
@@ -334,7 +342,8 @@ impl WebGpuError for StageError {
334342
| Self::TooManyVaryings { .. }
335343
| Self::MissingEntryPoint(..)
336344
| Self::NoEntryPointFound
337-
| Self::MultipleEntryPointsFound => return ErrorType::Validation,
345+
| Self::MultipleEntryPointsFound
346+
| Self::ColorAttachmentLocationTooLarge { .. } => return ErrorType::Validation,
338347
};
339348
e.webgpu_error_type()
340349
}
@@ -1317,7 +1326,6 @@ impl Interface {
13171326
}
13181327
}
13191328

1320-
#[expect(clippy::single_match)]
13211329
match shader_stage {
13221330
naga::ShaderStage::Vertex => {
13231331
for output in entry_point.outputs.iter() {
@@ -1352,6 +1360,20 @@ impl Interface {
13521360
}
13531361
}
13541362
}
1363+
naga::ShaderStage::Fragment => {
1364+
for output in &entry_point.outputs {
1365+
let &Varying::Local { location, ref iv } = output else {
1366+
continue;
1367+
};
1368+
if location >= self.limits.max_color_attachments {
1369+
return Err(StageError::ColorAttachmentLocationTooLarge {
1370+
location,
1371+
var: iv.clone(),
1372+
limit: self.limits.max_color_attachments,
1373+
});
1374+
}
1375+
}
1376+
}
13551377
_ => (),
13561378
}
13571379

@@ -1370,6 +1392,7 @@ impl Interface {
13701392
Varying::BuiltIn(_) => None,
13711393
})
13721394
.collect();
1395+
13731396
Ok(outputs)
13741397
}
13751398

0 commit comments

Comments
 (0)