Skip to content

Commit 438463e

Browse files
committed
Add immediate size validation cases for pipeline creation
Add cases to validate immediate size checks during both compute and render pipeline creation.
1 parent eb9091d commit 438463e

3 files changed

Lines changed: 210 additions & 2 deletions

File tree

0 Bytes
Binary file not shown.

src/webgpu/api/validation/compute_pipeline.spec.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Note: entry point matching tests are in shader_module/entry_point.spec.ts
77
import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js';
88
import { makeTestGroup } from '../../../common/framework/test_group.js';
99
import { keysOf } from '../../../common/util/data_tables.js';
10+
import { getGPU } from '../../../common/util/navigator_gpu.js';
11+
import { assert, supportsImmediateData } from '../../../common/util/util.js';
1012
import {
1113
isTextureFormatUsableWithStorageAccessMode,
1214
kPossibleStorageTextureFormats,
@@ -811,3 +813,86 @@ generates a validation error at createComputePipeline(Async)
811813
};
812814
vtu.doCreateComputePipelineTest(t, isAsync, success, descriptor);
813815
});
816+
817+
g.test('pipeline_creation_immediate_size_mismatch')
818+
.desc(
819+
`
820+
Validate that creating a pipeline fails if the shader uses immediate data
821+
larger than the immediateSize specified in the pipeline layout, or larger than
822+
maxImmediateSize if layout is 'auto'.
823+
Also validates that using less or equal size is allowed.
824+
`
825+
)
826+
.params(u => {
827+
const kNumericCases = [
828+
{ shaderSize: 16, layoutSize: 16 }, // Equal
829+
{ shaderSize: 12, layoutSize: 16 }, // Shader smaller
830+
{ shaderSize: 20, layoutSize: 16 }, // Shader larger (small diff)
831+
{ shaderSize: 32, layoutSize: 16 }, // Shader larger
832+
] as const;
833+
const kMaxLimitsCases = [
834+
{ shaderSize: 'max', layoutSize: 'auto' }, // Shader equal to limit (auto layout)
835+
{ shaderSize: 'exceedLimits', layoutSize: 'auto' }, // Shader larger than limit (auto layout)
836+
] as const;
837+
return u
838+
.combine('isAsync', [true, false])
839+
.combineWithParams([...kNumericCases, ...kMaxLimitsCases] as const);
840+
})
841+
.fn(t => {
842+
t.skipIf(!supportsImmediateData(getGPU(t.rec)), 'Immediate data not supported');
843+
844+
const { isAsync, shaderSize, layoutSize } = t.params;
845+
846+
assert(t.device.limits.maxImmediateSize !== undefined);
847+
const maxImmediateSize = t.device.limits.maxImmediateSize;
848+
849+
let actualLayout: GPUPipelineLayout | 'auto';
850+
let validSize: number;
851+
852+
if (layoutSize === 'auto') {
853+
actualLayout = 'auto';
854+
validSize = maxImmediateSize;
855+
} else {
856+
actualLayout = t.device.createPipelineLayout({
857+
bindGroupLayouts: [],
858+
immediateSize: layoutSize as number,
859+
});
860+
validSize = layoutSize as number;
861+
}
862+
863+
let actualShaderSize: number;
864+
if (shaderSize === 'max') {
865+
actualShaderSize = validSize;
866+
} else if (shaderSize === 'exceedLimits') {
867+
actualShaderSize = validSize + 4;
868+
} else {
869+
actualShaderSize = shaderSize as number;
870+
}
871+
872+
const numFields = actualShaderSize / 4;
873+
const fields = Array.from({ length: numFields }, (_, i) => `m${i}: u32`).join(', ');
874+
const code = `
875+
struct Immediates { ${fields} }
876+
var<immediate> data: Immediates;
877+
fn use_data() { _ = data.m0; }
878+
@compute @workgroup_size(1) fn main_compute() { use_data(); }
879+
`;
880+
881+
const shouldError = actualShaderSize > validSize;
882+
883+
// When the shader exceeds the device's maxImmediateSize, the error occurs
884+
// at shader module creation time, not pipeline creation time.
885+
if (layoutSize === 'auto' && shouldError) {
886+
t.expectValidationError(() => {
887+
t.device.createShaderModule({ code });
888+
});
889+
return;
890+
}
891+
892+
vtu.doCreateComputePipelineTest(t, isAsync, !shouldError, {
893+
layout: actualLayout,
894+
compute: {
895+
module: t.device.createShaderModule({ code }),
896+
},
897+
});
898+
});

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

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ misc createRenderPipeline and createRenderPipelineAsync validation tests.
33
`;
44

55
import { makeTestGroup } from '../../../../common/framework/test_group.js';
6+
import { getGPU } from '../../../../common/util/navigator_gpu.js';
7+
import { assert, supportsImmediateData } from '../../../../common/util/util.js';
68
import {
79
isTextureFormatUsableWithStorageAccessMode,
810
kPossibleStorageTextureFormats,
@@ -119,8 +121,10 @@ g.test('pipeline_layout,device_mismatch')
119121
});
120122

121123
g.test('external_texture')
122-
.desc('Tests createRenderPipeline() with an external_texture')
124+
.desc('Tests createRenderPipeline(Async) with an external_texture')
125+
.params(u => u.combine('isAsync', [false, true]))
123126
.fn(t => {
127+
const { isAsync } = t.params;
124128
const shader = t.device.createShaderModule({
125129
code: `
126130
@vertex
@@ -149,7 +153,7 @@ g.test('external_texture')
149153
},
150154
};
151155

152-
vtu.doCreateRenderPipelineTest(t, false, true, descriptor);
156+
vtu.doCreateRenderPipelineTest(t, isAsync, true, descriptor);
153157
});
154158

155159
g.test('storage_texture,format')
@@ -192,3 +196,122 @@ generates a validation error at createComputePipeline(Async)
192196
};
193197
vtu.doCreateRenderPipelineTest(t, isAsync, success, descriptor);
194198
});
199+
200+
g.test('pipeline_creation_immediate_size_mismatch')
201+
.desc(
202+
`
203+
Validate that creating a pipeline fails if the shader uses immediate data
204+
larger than the immediateSize specified in the pipeline layout, or larger than
205+
maxImmediateSize if layout is 'auto'.
206+
Also validates that using less or equal size is allowed.
207+
`
208+
)
209+
.params(u => {
210+
const kNumericCases = [
211+
{ vertexSize: 16, fragmentSize: 16, layoutSize: 16 }, // Equal
212+
{ vertexSize: 12, fragmentSize: 12, layoutSize: 16 }, // Shader smaller
213+
{ vertexSize: 20, fragmentSize: 20, layoutSize: 16 }, // Shader larger (small diff)
214+
{ vertexSize: 32, fragmentSize: 32, layoutSize: 16 }, // Shader larger
215+
] as const;
216+
const kMaxLimitsCases = [
217+
{ vertexSize: 'max', fragmentSize: 0, layoutSize: 'auto' }, // Vertex = Limit (Control)
218+
{ vertexSize: 0, fragmentSize: 'max', layoutSize: 'auto' }, // Fragment = Limit (Control)
219+
{ vertexSize: 'max', fragmentSize: 'max', layoutSize: 'auto' }, // Both at Limit (Control)
220+
{ vertexSize: 'exceedLimits', fragmentSize: 0, layoutSize: 'auto' }, // Vertex > Limit
221+
{ vertexSize: 0, fragmentSize: 'exceedLimits', layoutSize: 'auto' }, // Fragment > Limit
222+
] as const;
223+
return u
224+
.combine('isAsync', [true, false])
225+
.combineWithParams([...kNumericCases, ...kMaxLimitsCases] as const);
226+
})
227+
.fn(t => {
228+
t.skipIf(!supportsImmediateData(getGPU(t.rec)), 'Immediate data not supported');
229+
230+
const { isAsync, vertexSize, fragmentSize, layoutSize } = t.params;
231+
232+
assert(t.device.limits.maxImmediateSize !== undefined);
233+
const maxImmediateSize = t.device.limits.maxImmediateSize;
234+
235+
const resolveSize = (sizeDescriptor: number | string) => {
236+
if (typeof sizeDescriptor === 'number') return sizeDescriptor;
237+
if (sizeDescriptor === 'max') return maxImmediateSize;
238+
if (sizeDescriptor === 'exceedLimits') return maxImmediateSize + 4;
239+
return 0;
240+
};
241+
242+
const resolvedVertexImmediateSize = resolveSize(vertexSize);
243+
const resolvedFragmentImmediateSize = resolveSize(fragmentSize);
244+
245+
// Helper to generate a stage-specific shader module with the given immediate data size.
246+
const makeShaderCode = (size: number, stage: 'vertex' | 'fragment') => {
247+
if (size === 0) {
248+
if (stage === 'vertex') {
249+
return `@vertex fn main_vertex() -> @builtin(position) vec4<f32> { return vec4<f32>(0.0, 0.0, 0.0, 1.0); }`;
250+
}
251+
return `@fragment fn main_fragment() -> @location(0) vec4<f32> { return vec4<f32>(0.0, 1.0, 0.0, 1.0); }`;
252+
}
253+
const numFields = size / 4;
254+
const fields = Array.from({ length: numFields }, (_, i) => `m${i}: u32`).join(', ');
255+
if (stage === 'vertex') {
256+
return `
257+
struct Immediates { ${fields} }
258+
var<immediate> data: Immediates;
259+
@vertex fn main_vertex() -> @builtin(position) vec4<f32> { _ = data.m0; return vec4<f32>(0.0, 0.0, 0.0, 1.0); }
260+
`;
261+
}
262+
return `
263+
struct Immediates { ${fields} }
264+
var<immediate> data: Immediates;
265+
@fragment fn main_fragment() -> @location(0) vec4<f32> { _ = data.m0; return vec4<f32>(0.0, 1.0, 0.0, 1.0); }
266+
`;
267+
};
268+
269+
let layout: GPUPipelineLayout | 'auto';
270+
let validSize: number;
271+
272+
if (layoutSize === 'auto') {
273+
layout = 'auto';
274+
validSize = maxImmediateSize;
275+
} else {
276+
layout = t.device.createPipelineLayout({
277+
bindGroupLayouts: [],
278+
immediateSize: layoutSize as number,
279+
});
280+
validSize = layoutSize as number;
281+
}
282+
283+
const vertexExceedsLimit = resolvedVertexImmediateSize > validSize;
284+
const fragmentExceedsLimit = resolvedFragmentImmediateSize > validSize;
285+
const shouldError = vertexExceedsLimit || fragmentExceedsLimit;
286+
287+
// When the shader exceeds the device's maxImmediateSize, the error occurs
288+
// at shader module creation time, not pipeline creation time.
289+
// Create each shader module separately so the correct one gets the error.
290+
const vertexCode = makeShaderCode(resolvedVertexImmediateSize, 'vertex');
291+
const fragmentCode = makeShaderCode(resolvedFragmentImmediateSize, 'fragment');
292+
293+
if (layoutSize === 'auto' && vertexExceedsLimit) {
294+
t.expectValidationError(() => {
295+
t.device.createShaderModule({ code: vertexCode });
296+
});
297+
}
298+
if (layoutSize === 'auto' && fragmentExceedsLimit) {
299+
t.expectValidationError(() => {
300+
t.device.createShaderModule({ code: fragmentCode });
301+
});
302+
}
303+
if (layoutSize === 'auto' && shouldError) {
304+
return;
305+
}
306+
307+
vtu.doCreateRenderPipelineTest(t, isAsync, !shouldError, {
308+
layout,
309+
vertex: {
310+
module: t.device.createShaderModule({ code: vertexCode }),
311+
},
312+
fragment: {
313+
module: t.device.createShaderModule({ code: fragmentCode }),
314+
targets: [{ format: 'rgba8unorm' }],
315+
},
316+
});
317+
});

0 commit comments

Comments
 (0)