Skip to content

Commit 0ea9bce

Browse files
Add immediate size validation cases for pipeline creation (#4573)
* Add immediate size validation cases for pipeline creation Add cases to validate immediate size checks during both compute and render pipeline creation. * Adding assert to ensure test parameter valid * Address comments * Update src/webgpu/api/validation/pipeline/immediates.spec.ts Kai's way to make the case yield block more readable. Co-authored-by: Kai Ninomiya <kainino1@gmail.com> --------- Co-authored-by: Kai Ninomiya <kainino1@gmail.com>
1 parent e9eab3d commit 0ea9bce

2 files changed

Lines changed: 153 additions & 2 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
export const description = `
2+
Pipeline creation validation tests for immediate data size mismatches.
3+
4+
Validates that creating a pipeline fails if the shader uses immediate data
5+
larger than the immediateSize specified in the pipeline layout, or larger than
6+
maxImmediateSize if layout is 'auto'.
7+
`;
8+
9+
import { makeTestGroup } from '../../../../common/framework/test_group.js';
10+
import { getGPU } from '../../../../common/util/navigator_gpu.js';
11+
import { assert, range, supportsImmediateData } from '../../../../common/util/util.js';
12+
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
13+
import * as vtu from '../validation_test_utils.js';
14+
15+
export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
16+
17+
/**
18+
* Generate shader code for a given stage with the specified immediate data size.
19+
* If size is 0, the shader has no immediate data.
20+
*/
21+
function makeShaderCode(size: number, stage: 'compute' | 'vertex' | 'fragment'): string {
22+
if (size === 0) {
23+
switch (stage) {
24+
case 'compute':
25+
return `@compute @workgroup_size(1) fn main_compute() {}`;
26+
case 'vertex':
27+
return `@vertex fn main_vertex() -> @builtin(position) vec4<f32> { return vec4<f32>(0.0, 0.0, 0.0, 1.0); }`;
28+
case 'fragment':
29+
return `@fragment fn main_fragment() -> @location(0) vec4<f32> { return vec4<f32>(0.0, 1.0, 0.0, 1.0); }`;
30+
}
31+
}
32+
const numFields = size / 4;
33+
const fields = range(numFields, i => `m${i}: u32`).join(', ');
34+
const structDecl = `struct Immediates { ${fields} }\nvar<immediate> data: Immediates;`;
35+
switch (stage) {
36+
case 'compute':
37+
return `${structDecl}\nfn use_data() { _ = data.m0; }\n@compute @workgroup_size(1) fn main_compute() { use_data(); }`;
38+
case 'vertex':
39+
return `${structDecl}\n@vertex fn main_vertex() -> @builtin(position) vec4<f32> { _ = data.m0; return vec4<f32>(0.0, 0.0, 0.0, 1.0); }`;
40+
case 'fragment':
41+
return `${structDecl}\n@fragment fn main_fragment() -> @location(0) vec4<f32> { _ = data.m0; return vec4<f32>(0.0, 1.0, 0.0, 1.0); }`;
42+
}
43+
}
44+
45+
g.test('pipeline_creation_immediate_size_mismatch')
46+
.desc(
47+
`
48+
Validate that creating a compute or render pipeline fails if the shader uses
49+
immediate data larger than the immediateSize specified in the pipeline layout,
50+
or larger than maxImmediateSize if layout is 'auto'.
51+
Also validates that using less or equal size is allowed.
52+
53+
For compute pipelines, stageASize is the compute stage size (stageBSize is unused).
54+
For render pipelines, stageASize is the vertex stage size and stageBSize is the
55+
fragment stage size.
56+
`
57+
)
58+
.params(u =>
59+
u
60+
.combine('pipelineType', ['compute', 'render'] as const)
61+
.combine('isAsync', [true, false])
62+
.beginSubcases()
63+
.expandWithParams(function* (p) {
64+
yield { stageASize: 16, stageBSize: 16, layoutSize: 16 }; // Equal
65+
yield { stageASize: 12, stageBSize: 12, layoutSize: 16 }; // Shader smaller
66+
yield { stageASize: 20, stageBSize: 20, layoutSize: 16 }; // Shader larger (small diff)
67+
yield { stageASize: 32, stageBSize: 32, layoutSize: 16 }; // Shader larger
68+
yield { stageASize: 'max', stageBSize: 0, layoutSize: 'auto' }; // StageA at limit
69+
yield { stageASize: 'exceedLimits', stageBSize: 0, layoutSize: 'auto' }; // StageA exceeds
70+
71+
if (p.pipelineType === 'render') {
72+
yield { stageASize: 0, stageBSize: 'max', layoutSize: 'auto' }; // StageB at limit
73+
yield { stageASize: 'max', stageBSize: 'max', layoutSize: 'auto' }; // Both at limit
74+
yield { stageASize: 0, stageBSize: 'exceedLimits', layoutSize: 'auto' }; // StageB exceeds
75+
}
76+
})
77+
)
78+
.fn(t => {
79+
t.skipIf(!supportsImmediateData(getGPU(t.rec)), 'Immediate data not supported');
80+
81+
const { pipelineType, isAsync, stageASize, stageBSize, layoutSize } = t.params;
82+
83+
const maxImmediateSize = t.device.limits.maxImmediateSize;
84+
assert(maxImmediateSize !== undefined);
85+
86+
const resolveSize = (sizeDescriptor: number | string): number => {
87+
if (typeof sizeDescriptor === 'number') return sizeDescriptor;
88+
if (sizeDescriptor === 'max') return maxImmediateSize;
89+
if (sizeDescriptor === 'exceedLimits') return maxImmediateSize + 4;
90+
return 0;
91+
};
92+
93+
const resolvedStageASize = resolveSize(stageASize);
94+
const resolvedStageBSize = resolveSize(stageBSize);
95+
96+
// Ensure the test's fixed sizes fit within the device limit.
97+
if (stageASize !== 'exceedLimits') {
98+
assert(
99+
resolvedStageASize <= maxImmediateSize,
100+
`stageASize (${resolvedStageASize}) must be <= maxImmediateSize (${maxImmediateSize})`
101+
);
102+
}
103+
if (stageBSize !== 'exceedLimits') {
104+
assert(
105+
resolvedStageBSize <= maxImmediateSize,
106+
`stageBSize (${resolvedStageBSize}) must be <= maxImmediateSize (${maxImmediateSize})`
107+
);
108+
}
109+
110+
// Build pipeline layout.
111+
let layout: GPUPipelineLayout | 'auto';
112+
let validSize: number;
113+
114+
if (layoutSize === 'auto') {
115+
layout = 'auto';
116+
validSize = maxImmediateSize;
117+
} else {
118+
layout = t.device.createPipelineLayout({
119+
bindGroupLayouts: [],
120+
immediateSize: layoutSize as number,
121+
});
122+
validSize = layoutSize as number;
123+
}
124+
125+
const stageAExceedsLimit = resolvedStageASize > validSize;
126+
const stageBExceedsLimit = resolvedStageBSize > validSize;
127+
const shouldError = stageAExceedsLimit || stageBExceedsLimit;
128+
129+
if (pipelineType === 'compute') {
130+
const code = makeShaderCode(resolvedStageASize, 'compute');
131+
132+
vtu.doCreateComputePipelineTest(t, isAsync, !shouldError, {
133+
layout,
134+
compute: { module: t.device.createShaderModule({ code }) },
135+
});
136+
} else {
137+
const vertexCode = makeShaderCode(resolvedStageASize, 'vertex');
138+
const fragmentCode = makeShaderCode(resolvedStageBSize, 'fragment');
139+
140+
vtu.doCreateRenderPipelineTest(t, isAsync, !shouldError, {
141+
layout,
142+
vertex: { module: t.device.createShaderModule({ code: vertexCode }) },
143+
fragment: {
144+
module: t.device.createShaderModule({ code: fragmentCode }),
145+
targets: [{ format: 'rgba8unorm' }],
146+
},
147+
});
148+
}
149+
});

src/webgpu/api/validation/render_pipeline/misc.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,10 @@ g.test('pipeline_layout,device_mismatch')
119119
});
120120

121121
g.test('external_texture')
122-
.desc('Tests createRenderPipeline() with an external_texture')
122+
.desc('Tests createRenderPipeline(Async) with an external_texture')
123+
.params(u => u.combine('isAsync', [false, true]))
123124
.fn(t => {
125+
const { isAsync } = t.params;
124126
const shader = t.device.createShaderModule({
125127
code: `
126128
@vertex
@@ -149,7 +151,7 @@ g.test('external_texture')
149151
},
150152
};
151153

152-
vtu.doCreateRenderPipelineTest(t, false, true, descriptor);
154+
vtu.doCreateRenderPipelineTest(t, isAsync, true, descriptor);
153155
});
154156

155157
g.test('storage_texture,format')

0 commit comments

Comments
 (0)