Skip to content

Commit 561ab2a

Browse files
authored
User IO passthrough shader tests (#3454)
* User-defined IO passthrough execution tests * Tests i32, u32, f32, and f16 * scalar, vec2, and vec4 * Passes values from vertex input -> vertex output -> fragment input -> fragment output * always uses uints at vertex input and fragment output for simplicity
1 parent a93708e commit 561ab2a

File tree

2 files changed

+214
-0
lines changed

2 files changed

+214
-0
lines changed

src/webgpu/listing_meta.json

+1
Original file line numberDiff line numberDiff line change
@@ -1743,6 +1743,7 @@
17431743
"webgpu:shader,execution,shader_io,shared_structs:shared_between_stages:*": { "subcaseMS": 9.601 },
17441744
"webgpu:shader,execution,shader_io,shared_structs:shared_with_buffer:*": { "subcaseMS": 20.701 },
17451745
"webgpu:shader,execution,shader_io,shared_structs:shared_with_non_entry_point_function:*": { "subcaseMS": 6.801 },
1746+
"webgpu:shader,execution,shader_io,user_io:passthrough:*": { "subcaseMS": 373.385 },
17461747
"webgpu:shader,execution,shader_io,workgroup_size:workgroup_size:*": { "subcaseMS": 0.000 },
17471748
"webgpu:shader,execution,shadow:builtin:*": { "subcaseMS": 4.700 },
17481749
"webgpu:shader,execution,shadow:declaration:*": { "subcaseMS": 9.700 },
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
export const description = `
2+
Test for user-defined shader I/O.
3+
4+
passthrough:
5+
* Data passed into vertex shader as uints and converted to test type
6+
* Passed from vertex to fragment as test type
7+
* Output from fragment shader as uint
8+
`;
9+
10+
import { makeTestGroup } from '../../../../common/framework/test_group.js';
11+
import { range } from '../../../../common/util/util.js';
12+
import { GPUTest } from '../../../gpu_test.js';
13+
14+
export const g = makeTestGroup(GPUTest);
15+
16+
function generateInterstagePassthroughCode(type: string): string {
17+
return `
18+
${type === 'f16' ? 'enable f16;' : ''}
19+
struct IOData {
20+
@builtin(position) pos : vec4f,
21+
@location(0) @interpolate(flat) user0 : ${type},
22+
@location(1) @interpolate(flat) user1 : vec2<${type}>,
23+
@location(2) @interpolate(flat) user2 : vec4<${type}>,
24+
}
25+
26+
struct VertexInput {
27+
@builtin(vertex_index) idx : u32,
28+
@location(0) in0 : u32,
29+
@location(1) in1 : vec2u,
30+
@location(2) in2 : vec4u,
31+
}
32+
33+
@vertex
34+
fn vsMain(input : VertexInput) -> IOData {
35+
const vertices = array(
36+
vec4f(-1, -1, 0, 1),
37+
vec4f(-1, 1, 0, 1),
38+
vec4f( 1, -1, 0, 1),
39+
);
40+
var data : IOData;
41+
data.pos = vertices[input.idx];
42+
data.user0 = ${type}(input.in0);
43+
data.user1 = vec2<${type}>(input.in1);
44+
data.user2 = vec4<${type}>(input.in2);
45+
return data;
46+
}
47+
48+
struct FragOutput {
49+
@location(0) out0 : u32,
50+
@location(1) out1 : vec2u,
51+
@location(2) out2 : vec4u,
52+
}
53+
54+
@fragment
55+
fn fsMain(input : IOData) -> FragOutput {
56+
var out : FragOutput;
57+
out.out0 = u32(input.user0);
58+
out.out1 = vec2u(input.user1);
59+
out.out2 = vec4u(input.user2);
60+
return out;
61+
}
62+
`;
63+
}
64+
65+
function drawPassthrough(t: GPUTest, code: string) {
66+
// Default limit is 32 bytes of color attachments.
67+
// These attachments use 28 bytes (which is why vec3 is skipped).
68+
const formats: GPUTextureFormat[] = ['r32uint', 'rg32uint', 'rgba32uint'];
69+
const components = [1, 2, 4];
70+
const pipeline = t.device.createRenderPipeline({
71+
layout: 'auto',
72+
vertex: {
73+
module: t.device.createShaderModule({ code }),
74+
entryPoint: 'vsMain',
75+
buffers: [
76+
{
77+
arrayStride: 4,
78+
attributes: [
79+
{
80+
format: 'uint32',
81+
offset: 0,
82+
shaderLocation: 0,
83+
},
84+
],
85+
},
86+
{
87+
arrayStride: 8,
88+
attributes: [
89+
{
90+
format: 'uint32x2',
91+
offset: 0,
92+
shaderLocation: 1,
93+
},
94+
],
95+
},
96+
{
97+
arrayStride: 16,
98+
attributes: [
99+
{
100+
format: 'uint32x4',
101+
offset: 0,
102+
shaderLocation: 2,
103+
},
104+
],
105+
},
106+
],
107+
},
108+
fragment: {
109+
module: t.device.createShaderModule({ code }),
110+
entryPoint: 'fsMain',
111+
targets: formats.map(x => {
112+
return { format: x };
113+
}),
114+
},
115+
primitive: {
116+
topology: 'triangle-list',
117+
},
118+
});
119+
120+
const vertexBuffer = t.makeBufferWithContents(
121+
new Uint32Array([
122+
// scalar: offset 0
123+
1, 1, 1, 0,
124+
// vec2: offset 16
125+
2, 2, 2, 2, 2, 2, 0, 0,
126+
// vec4: offset 48
127+
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
128+
]),
129+
GPUBufferUsage.COPY_SRC | GPUBufferUsage.VERTEX
130+
);
131+
132+
const bytesPerComponent = 4;
133+
// 256 is the minimum bytes per row for texture to buffer copies.
134+
const width = 256 / bytesPerComponent;
135+
const height = 2;
136+
const copyWidth = 4;
137+
const outputTextures = range(3, i => {
138+
const texture = t.device.createTexture({
139+
size: [width, height],
140+
usage:
141+
GPUTextureUsage.COPY_SRC |
142+
GPUTextureUsage.RENDER_ATTACHMENT |
143+
GPUTextureUsage.TEXTURE_BINDING,
144+
format: formats[i],
145+
});
146+
t.trackForCleanup(texture);
147+
return texture;
148+
});
149+
150+
let bufferSize = 1;
151+
for (const comp of components) {
152+
bufferSize *= comp;
153+
}
154+
bufferSize *= outputTextures.length * bytesPerComponent * copyWidth;
155+
const outputBuffer = t.device.createBuffer({
156+
size: bufferSize,
157+
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
158+
});
159+
t.trackForCleanup(outputBuffer);
160+
161+
const encoder = t.device.createCommandEncoder();
162+
const pass = encoder.beginRenderPass({
163+
colorAttachments: outputTextures.map(t => ({
164+
view: t.createView(),
165+
loadOp: 'clear',
166+
storeOp: 'store',
167+
})),
168+
});
169+
pass.setPipeline(pipeline);
170+
pass.setVertexBuffer(0, vertexBuffer, 0, 12);
171+
pass.setVertexBuffer(1, vertexBuffer, 16, 24);
172+
pass.setVertexBuffer(2, vertexBuffer, 48, 48);
173+
pass.draw(3);
174+
pass.end();
175+
176+
// Copy 'copyWidth' samples from each attachment into a buffer to check the results.
177+
let offset = 0;
178+
let expectArray: number[] = [];
179+
for (let i = 0; i < outputTextures.length; i++) {
180+
encoder.copyTextureToBuffer(
181+
{ texture: outputTextures[i] },
182+
{
183+
buffer: outputBuffer,
184+
offset,
185+
bytesPerRow: bytesPerComponent * components[i] * width,
186+
rowsPerImage: height,
187+
},
188+
{ width: copyWidth, height: 1 }
189+
);
190+
offset += components[i] * bytesPerComponent * copyWidth;
191+
for (let j = 0; j < components[i]; j++) {
192+
const value = i + 1;
193+
expectArray = expectArray.concat([value, value, value, value]);
194+
}
195+
}
196+
t.queue.submit([encoder.finish()]);
197+
198+
const expect = new Uint32Array(expectArray);
199+
t.expectGPUBufferValuesEqual(outputBuffer, expect);
200+
}
201+
202+
g.test('passthrough')
203+
.desc('Tests passing user-defined data from vertex input through fragment output')
204+
.params(u => u.combine('type', ['f32', 'f16', 'i32', 'u32'] as const))
205+
.beforeAllSubcases(t => {
206+
if (t.params.type === 'f16') {
207+
t.selectDeviceOrSkipTestCase('shader-f16');
208+
}
209+
})
210+
.fn(t => {
211+
const code = generateInterstagePassthroughCode(t.params.type);
212+
drawPassthrough(t, code);
213+
});

0 commit comments

Comments
 (0)