diff --git a/src/resources/cache/webgpu/shader/execution/bitcast.bin b/src/resources/cache/webgpu/shader/execution/bitcast.bin index 7ef0b7653d0a..9a0dd012e550 100644 Binary files a/src/resources/cache/webgpu/shader/execution/bitcast.bin and b/src/resources/cache/webgpu/shader/execution/bitcast.bin differ diff --git a/src/webgpu/api/validation/pipeline/immediates.spec.ts b/src/webgpu/api/validation/pipeline/immediates.spec.ts new file mode 100644 index 000000000000..f2106629a8ea --- /dev/null +++ b/src/webgpu/api/validation/pipeline/immediates.spec.ts @@ -0,0 +1,157 @@ +export const description = ` +Pipeline creation validation tests for immediate data size mismatches. + +Validates that creating a pipeline fails if the shader uses immediate data +larger than the immediateSize specified in the pipeline layout, or larger than +maxImmediateSize if layout is 'auto'. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { getGPU } from '../../../../common/util/navigator_gpu.js'; +import { assert, range, supportsImmediateData } from '../../../../common/util/util.js'; +import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js'; +import * as vtu from '../validation_test_utils.js'; + +export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest); + +/** + * Generate shader code for a given stage with the specified immediate data size. + * If size is 0, the shader has no immediate data. + */ +function makeShaderCode(size: number, stage: 'compute' | 'vertex' | 'fragment'): string { + if (size === 0) { + switch (stage) { + case 'compute': + return `@compute @workgroup_size(1) fn main_compute() {}`; + case 'vertex': + return `@vertex fn main_vertex() -> @builtin(position) vec4 { return vec4(0.0, 0.0, 0.0, 1.0); }`; + case 'fragment': + return `@fragment fn main_fragment() -> @location(0) vec4 { return vec4(0.0, 1.0, 0.0, 1.0); }`; + } + } + const numFields = size / 4; + const fields = range(numFields, i => `m${i}: u32`).join(', '); + const structDecl = `struct Immediates { ${fields} }\nvar data: Immediates;`; + switch (stage) { + case 'compute': + return `${structDecl}\nfn use_data() { _ = data.m0; }\n@compute @workgroup_size(1) fn main_compute() { use_data(); }`; + case 'vertex': + return `${structDecl}\n@vertex fn main_vertex() -> @builtin(position) vec4 { _ = data.m0; return vec4(0.0, 0.0, 0.0, 1.0); }`; + case 'fragment': + return `${structDecl}\n@fragment fn main_fragment() -> @location(0) vec4 { _ = data.m0; return vec4(0.0, 1.0, 0.0, 1.0); }`; + } +} + +g.test('pipeline_creation_immediate_size_mismatch') + .desc( + ` + Validate that creating a compute or render pipeline fails if the shader uses + immediate data larger than the immediateSize specified in the pipeline layout, + or larger than maxImmediateSize if layout is 'auto'. + Also validates that using less or equal size is allowed. + + For compute pipelines, stageASize is the compute stage size (stageBSize is unused). + For render pipelines, stageASize is the vertex stage size and stageBSize is the + fragment stage size. + ` + ) + .params(u => + u + .combine('pipelineType', ['compute', 'render'] as const) + .combine('isAsync', [true, false]) + .combineWithParams([ + { stageASize: 16, stageBSize: 16, layoutSize: 16 }, // Equal + { stageASize: 12, stageBSize: 12, layoutSize: 16 }, // Shader smaller + { stageASize: 20, stageBSize: 20, layoutSize: 16 }, // Shader larger (small diff) + { stageASize: 32, stageBSize: 32, layoutSize: 16 }, // Shader larger + { stageASize: 'max', stageBSize: 0, layoutSize: 'auto' }, // StageA at limit + { stageASize: 0, stageBSize: 'max', layoutSize: 'auto' }, // StageB at limit + { stageASize: 'max', stageBSize: 'max', layoutSize: 'auto' }, // Both at limit + { stageASize: 'exceedLimits', stageBSize: 0, layoutSize: 'auto' }, // StageA exceeds + { stageASize: 0, stageBSize: 'exceedLimits', layoutSize: 'auto' }, // StageB exceeds + ] as const) + .filter(p => { + // Compute has a single stage. + // For numeric cases (stageBSize === stageASize), keep them — stageBSize is ignored. + // For max-limit cases, keep only those where stageBSize is 0. + if (p.pipelineType === 'compute') { + if (typeof p.stageASize === 'string' || typeof p.stageBSize === 'string') { + return p.stageBSize === 0; + } + return p.stageBSize === p.stageASize; + } + return true; + }) + ) + .fn(t => { + t.skipIf(!supportsImmediateData(getGPU(t.rec)), 'Immediate data not supported'); + + const { pipelineType, isAsync, stageASize, stageBSize, layoutSize } = t.params; + + const maxImmediateSize = t.device.limits.maxImmediateSize; + assert(maxImmediateSize !== undefined); + + const resolveSize = (sizeDescriptor: number | string): number => { + if (typeof sizeDescriptor === 'number') return sizeDescriptor; + if (sizeDescriptor === 'max') return maxImmediateSize; + if (sizeDescriptor === 'exceedLimits') return maxImmediateSize + 4; + return 0; + }; + + const resolvedStageASize = resolveSize(stageASize); + const resolvedStageBSize = resolveSize(stageBSize); + + // Ensure the test's fixed sizes fit within the device limit. + if (stageASize !== 'exceedLimits') { + assert( + resolvedStageASize <= maxImmediateSize, + `stageASize (${resolvedStageASize}) must be <= maxImmediateSize (${maxImmediateSize})` + ); + } + if (stageBSize !== 'exceedLimits') { + assert( + resolvedStageBSize <= maxImmediateSize, + `stageBSize (${resolvedStageBSize}) must be <= maxImmediateSize (${maxImmediateSize})` + ); + } + + // Build pipeline layout. + let layout: GPUPipelineLayout | 'auto'; + let validSize: number; + + if (layoutSize === 'auto') { + layout = 'auto'; + validSize = maxImmediateSize; + } else { + layout = t.device.createPipelineLayout({ + bindGroupLayouts: [], + immediateSize: layoutSize as number, + }); + validSize = layoutSize as number; + } + + const stageAExceedsLimit = resolvedStageASize > validSize; + const stageBExceedsLimit = resolvedStageBSize > validSize; + const shouldError = stageAExceedsLimit || stageBExceedsLimit; + + if (pipelineType === 'compute') { + const code = makeShaderCode(resolvedStageASize, 'compute'); + + vtu.doCreateComputePipelineTest(t, isAsync, !shouldError, { + layout, + compute: { module: t.device.createShaderModule({ code }) }, + }); + } else { + const vertexCode = makeShaderCode(resolvedStageASize, 'vertex'); + const fragmentCode = makeShaderCode(resolvedStageBSize, 'fragment'); + + vtu.doCreateRenderPipelineTest(t, isAsync, !shouldError, { + layout, + vertex: { module: t.device.createShaderModule({ code: vertexCode }) }, + fragment: { + module: t.device.createShaderModule({ code: fragmentCode }), + targets: [{ format: 'rgba8unorm' }], + }, + }); + } + }); diff --git a/src/webgpu/api/validation/render_pipeline/misc.spec.ts b/src/webgpu/api/validation/render_pipeline/misc.spec.ts index e688ea14b456..bd8fb7f6a2b7 100644 --- a/src/webgpu/api/validation/render_pipeline/misc.spec.ts +++ b/src/webgpu/api/validation/render_pipeline/misc.spec.ts @@ -119,8 +119,10 @@ g.test('pipeline_layout,device_mismatch') }); g.test('external_texture') - .desc('Tests createRenderPipeline() with an external_texture') + .desc('Tests createRenderPipeline(Async) with an external_texture') + .params(u => u.combine('isAsync', [false, true])) .fn(t => { + const { isAsync } = t.params; const shader = t.device.createShaderModule({ code: ` @vertex @@ -149,7 +151,7 @@ g.test('external_texture') }, }; - vtu.doCreateRenderPipelineTest(t, false, true, descriptor); + vtu.doCreateRenderPipelineTest(t, isAsync, true, descriptor); }); g.test('storage_texture,format')