From 53f88ae359f9d05d93d79ea1638525f881271073 Mon Sep 17 00:00:00 2001 From: Gregg Tavares Date: Tue, 26 Nov 2024 17:44:20 -0800 Subject: [PATCH] Compat: Test texture+sampler limits are enforced See: https://github.com/gpuweb/gpuweb/pull/4989 --- .../api/validation/pipeline_creation.spec.ts | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/src/webgpu/compat/api/validation/pipeline_creation.spec.ts b/src/webgpu/compat/api/validation/pipeline_creation.spec.ts index 7e70a036f536..73d997d298a4 100644 --- a/src/webgpu/compat/api/validation/pipeline_creation.spec.ts +++ b/src/webgpu/compat/api/validation/pipeline_creation.spec.ts @@ -144,3 +144,158 @@ g.test('depth_textures') break; } }); + +function numCombosToNumber(maxCombos: number, numCombos: '1' | '2' | 'max' | 'max+1') { + switch (numCombos) { + case '1': + return 1; + case '2': + return 2; + case 'max': + return maxCombos; + case 'max+1': + return maxCombos + 1; + } +} + +function numNonSampledToNumber(maxTextures: number, numNonSampled: '1' | 'max') { + switch (numNonSampled) { + case '1': + return 1; + case 'max': + return maxTextures; + } +} + +g.test('texture_sampler_combos') + .desc( + ` +Test that you can not use more texture+sampler combos than +min(maxSamplersPerShaderStage, maxSampledTexturesPerShaderStage) +in compatibility mode. +` + ) + .params(u => + u + .combineWithParams([ + { pass: true, stages: 'vertex', numCombos: 'max', numNonSampled: '1' }, + { pass: true, stages: 'fragment', numCombos: 'max', numNonSampled: '1' }, + { pass: false, stages: 'vertex', numCombos: 'max+1', numNonSampled: '1' }, + { pass: false, stages: 'fragment', numCombos: 'max+1', numNonSampled: '1' }, + { pass: true, stages: 'vertex', numCombos: '1', numNonSampled: 'max' }, + { pass: true, stages: 'fragment', numCombos: '1', numNonSampled: 'max' }, + { pass: false, stages: 'vertex', numCombos: '2', numNonSampled: 'max' }, + { pass: false, stages: 'fragment', numCombos: '2', numNonSampled: 'max' }, + { pass: true, stages: 'vertex,fragment', numCombos: 'max', numNonSampled: '1' }, + ] as const) + .combine('async', [false, true] as const) + ) + .fn(t => { + const { device } = t; + const { pass, stages, numCombos, numNonSampled, async } = t.params; + const { maxSampledTexturesPerShaderStage, maxSamplersPerShaderStage } = device.limits; + + const maxCombos = Math.min(maxSampledTexturesPerShaderStage, maxSamplersPerShaderStage); + const numCombinations = numCombosToNumber(maxCombos, numCombos); + const numNonSampledTextures = numNonSampledToNumber( + maxSampledTexturesPerShaderStage, + numNonSampled + ); + + const textureDeclarations: string[][] = [[], []]; + const samplerDeclarations: string[][] = [[], []]; + const usages: string[][] = [[], []]; + const bindGroupLayoutEntries: GPUBindGroupLayoutEntry[][] = [[], [], [], []]; + const visibilityByStage = [GPUShaderStage.VERTEX, GPUShaderStage.FRAGMENT]; + + const addTextureDeclaration = (stage: number, binding: number) => { + textureDeclarations[stage].push( + `@group(${stage * 2}) @binding(${binding}) var t${stage}_${binding}: texture_2d;` + ); + bindGroupLayoutEntries[stage * 2].push({ + binding, + visibility: visibilityByStage[stage], + texture: {}, + }); + }; + + const addSamplerDeclaration = (stage: number, binding: number) => { + samplerDeclarations[stage].push( + `@group(${stage * 2 + 1}) @binding(${binding}) var s${stage}_${binding}: sampler;` + ); + bindGroupLayoutEntries[stage * 2 + 1].push({ + binding, + visibility: visibilityByStage[stage], + sampler: {}, + }); + }; + + for (let stage = 0; stage < 2; ++stage) { + let count = 0; + for (let t = 0; count < numCombinations && t < maxSampledTexturesPerShaderStage; ++t) { + addTextureDeclaration(stage, t); + for (let s = 0; count < numCombinations && s < maxSamplersPerShaderStage; ++s) { + if (t === 0) { + addSamplerDeclaration(stage, s); + } + usages[stage].push( + ` c += textureSampleLevel(t${stage}_${t}, s${stage}_${s}, vec2f(0), 0);` + ); + ++count; + } + } + + for (let t = 0; t < numNonSampledTextures; ++t) { + if (t >= textureDeclarations[stage].length) { + addTextureDeclaration(stage, t); + } + usages[stage].push(` c += textureLoad(t${stage}_${t}, vec2u(0), 0);`); + } + } + + const code = ` +${textureDeclarations[0].join('\n')} +${textureDeclarations[1].join('\n')} + +${samplerDeclarations[0].join('\n')} +${samplerDeclarations[1].join('\n')} + +fn usage0() -> vec4f { + var c: vec4f; + ${usages[0].join('\n')} + return c; +} + +fn usage1() -> vec4f { + var c: vec4f; + ${usages[1].join('\n')} + return c; +} + +@vertex fn vs() -> @builtin(position) vec4f { + _ = ${stages.includes('vertex') ? 'usage0()' : 'vec4f(0)'}; + return vec4f(0); +} + +@group(2) @binding(0) var tt: texture_2d; + +@fragment fn fs() -> @location(0) vec4f { + return ${stages.includes('fragment') ? 'usage1()' : 'vec4f(0)'}; +} +`; + + // MAINTENANCE_TODO: remove this. It's only needed because of a bug in dawn + // with auto layouts. + const layout = device.createPipelineLayout({ + bindGroupLayouts: bindGroupLayoutEntries.map(entries => + device.createBindGroupLayout({ entries }) + ), + }); + + const module = device.createShaderModule({ code }); + t.doCreateRenderPipelineTest(async, pass || !t.isCompatibility, { + layout, + vertex: { module }, + fragment: { module, targets: [{ format: 'rgba8unorm' }] }, + }); + });