export const description = ` Tests that createComputePipeline(async), and createRenderPipeline(async) reject pipelines that are invalid in compat mode - test that depth textures can not be used with non-comparison samplers - test that dpdxFine, dpdyFine, fwidthFine are disallowed TODO: - test that a shader that has more than min(maxSamplersPerShaderStage, maxSampledTexturesPerShaderStage) texture+sampler combinations generates a validation error. `; import { makeTestGroup } from '../../../../common/framework/test_group.js'; import { keysOf } from '../../../../common/util/data_tables.js'; import * as vtu from '../../../api/validation/validation_test_utils.js'; import { kShortShaderStages, kShortShaderStageToShaderStage, } from '../../../shader/execution/expression/call/builtin/texture_utils.js'; import { CompatibilityTest } from '../../compatibility_test.js'; export const g = makeTestGroup(CompatibilityTest); const kDepthCases = { textureSampleWith2DF32: { sampleWGSL: 'textureSample(t, s, vec2f(0))', // should pass textureType: 'texture_2d', viewDimension: '2d', }, textureSampleWithDepth2D: { sampleWGSL: 'textureSample(t, s, vec2f(0))', textureType: 'texture_depth_2d', viewDimension: '2d', }, textureSampleWithDepthCube: { sampleWGSL: 'textureSample(t, s, vec3f(0))', textureType: 'texture_depth_cube', viewDimension: 'cube', }, textureSampleWithDepth2DArray: { sampleWGSL: 'textureSample(t, s, vec2f(0), 0)', textureType: 'texture_depth_2d_array', viewDimension: '2d-array', }, textureSampleWithOffsetWithDepth2D: { sampleWGSL: 'textureSample(t, s, vec2f(0), vec2i(0, 0))', textureType: 'texture_depth_2d', viewDimension: '2d', }, textureSampleWithOffsetWithDepth2DArray: { sampleWGSL: 'textureSample(t, s, vec2f(0), 0, vec2i(0, 0))', textureType: 'texture_depth_2d_array', viewDimension: '2d-array', }, textureSampleLevelWithDepth2D: { sampleWGSL: 'textureSampleLevel(t, s, vec2f(0), 0)', textureType: 'texture_depth_2d', viewDimension: '2d', }, textureSampleLevelWithDepthCube: { sampleWGSL: 'textureSampleLevel(t, s, vec3f(0), 0)', textureType: 'texture_depth_cube', viewDimension: 'cube', }, textureSampleLevelWithDepth2DArray: { sampleWGSL: 'textureSampleLevel(t, s, vec2f(0), 0, 0)', textureType: 'texture_depth_2d_array', viewDimension: '2d-array', }, textureSampleLevelWithOffsetWithDepth2D: { sampleWGSL: 'textureSampleLevel(t, s, vec2f(0), 0, vec2i(0, 0))', textureType: 'texture_depth_2d', viewDimension: '2d', }, textureSampleLevelWithOffsetWithDepth2DArray: { sampleWGSL: 'textureSampleLevel(t, s, vec2f(0), 0, 0, vec2i(0, 0))', textureType: 'texture_depth_2d_array', viewDimension: '2d-array', }, textureGatherWithDepth2D: { sampleWGSL: 'textureGather(t, s, vec2f(0))', textureType: 'texture_depth_2d', viewDimension: '2d', }, textureGatherWithDepthCube: { sampleWGSL: 'textureGather(t, s, vec3f(0))', textureType: 'texture_depth_cube', viewDimension: 'cube', }, textureGatherWithDepth2DArray: { sampleWGSL: 'textureGather(t, s, vec2f(0), 0)', textureType: 'texture_depth_2d_array', viewDimension: '2d-array', }, textureGatherWithOffsetWithDepth2D: { sampleWGSL: 'textureGather(t, s, vec2f(0), vec2i(0, 0))', textureType: 'texture_depth_2d', viewDimension: '2d', }, textureGatherWithOffsetDepth2DArray: { sampleWGSL: 'textureGather(t, s, vec2f(0), 0, vec2i(0, 0))', textureType: 'texture_depth_2d_array', viewDimension: '2d-array', }, } as const; g.test('depth_textures') .desc('Tests that depth textures can not be used with non-comparison samplers in compat mode.') .params(u => u // .combine('depthCase', keysOf(kDepthCases)) .combine('stage', kShortShaderStages) .filter( t => kDepthCases[t.depthCase].sampleWGSL.startsWith('textureGather') || t.stage === 'f' ) .combine('async', [false, true] as const) ) .fn(t => { const { depthCase, stage: shortStage, async } = t.params; const { sampleWGSL, textureType, viewDimension } = kDepthCases[depthCase]; const stage = kShortShaderStageToShaderStage[shortStage]; const usageWGSL = `_ = ${sampleWGSL};`; const module = t.device.createShaderModule({ code: ` @group(0) @binding(0) var t: ${textureType}; @group(1) @binding(0) var s: sampler; // make sure it's fine such a combination exists but it's not used. fn unused() { ${usageWGSL}; } @vertex fn vs() -> @builtin(position) vec4f { ${stage === 'vertex' ? usageWGSL : ''} return vec4f(0); } @fragment fn fs() -> @location(0) vec4f { ${stage === 'fragment' ? usageWGSL : ''} return vec4f(0); } @compute @workgroup_size(1) fn cs() { ${stage === 'compute' ? usageWGSL : ''}; } `, }); const bindGroupLayout0 = t.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE, texture: { sampleType: textureType.includes('depth') ? 'depth' : 'float', viewDimension, multisampled: false, }, }, ], }); const bindGroupLayout1 = t.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE, sampler: { type: 'non-filtering', }, }, ], }); const layout = t.device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout0, bindGroupLayout1], }); const success = !t.isCompatibility || textureType === 'texture_2d'; switch (stage) { case 'compute': vtu.doCreateComputePipelineTest(t, async, success, { layout, compute: { module, }, }); break; case 'fragment': case 'vertex': vtu.doCreateRenderPipelineTest(t, async, success, { layout, vertex: { module, }, fragment: { module, targets: [{ format: 'rgba8unorm' }], }, }); break; } }); function numCombosToNumber(maxCombos: number, numCombos: string): number { const m = /^max([+-]?)(\d+)$/.exec(numCombos); if (m) { if (m[1] === '+') { return maxCombos + parseInt(m[2]); } else if (m[1] === '-') { return maxCombos - parseInt(m[2]); } else { return maxCombos; } } return parseInt(numCombos); } function numNonSampledToNumber(maxTextures: number, numNonSampled: '0' | '1' | 'max') { switch (numNonSampled) { case '0': return 0; 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. The spec, copy and pasted here: maxCombinationsPerStage = min(maxSampledTexturesPerShaderStage, maxSamplersPerShaderStage) for each stage of the pipeline: sum = 0 for each texture binding in the pipeline layout which is visible to that stage: sum += max(1, number of texture sampler combos for that texture binding) for each external texture binding in the pipeline layout which is visible to that stage: sum += 1 // for LUT texture + LUT sampler sum += 3 * max(1, number of external_texture sampler combos) // for Y+U+V if sum > maxCombinationsPerStage generate a validation error. ` ) .params(u => // prettier-ignore u .combineWithParams([ ...([ { pass: true, numCombos: 'max', numNonSampled: '1', numExternal: 0, useSame: false }, { pass: false, numCombos: 'max+1', numNonSampled: '1', numExternal: 0, useSame: false }, { pass: true, numCombos: '1', numNonSampled: 'max', numExternal: 0, useSame: false }, { pass: false, numCombos: '2', numNonSampled: 'max', numExternal: 0, useSame: false }, { pass: true, numCombos: 'max-4', numNonSampled: '0', numExternal: 1, useSame: false }, { pass: false, numCombos: 'max-3', numNonSampled: '0', numExternal: 1, useSame: false }, { pass: true, numCombos: 'max-8', numNonSampled: '0', numExternal: 2, useSame: false }, { pass: false, numCombos: 'max-7', numNonSampled: '0', numExternal: 2, useSame: false }, { pass: true, numCombos: 'max-7', numNonSampled: '0', numExternal: 2, useSame: true }, { pass: false, numCombos: 'max-6', numNonSampled: '0', numExternal: 2, useSame: true }, ] as const).map(p => ([{ ...p, stages: 'vertex' }, { ...p, stages: 'fragment' }, { ...p, stages: 'compute' }] as const)).flat(), { pass: true, numCombos: 'max', numNonSampled: '1', numExternal: 0, useSame: false, stages: 'vertex,fragment' }, ] as const) .combine('async', [false, true] as const) ) .fn(t => { const { device } = t; const { pass, stages, numCombos, numNonSampled, numExternal, useSame, async } = t.params; const { maxSampledTexturesPerShaderStage, maxSamplersPerShaderStage } = device.limits; const maxTexturesPerStage = maxSampledTexturesPerShaderStage - numExternal * 3; 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 numStages = stages === 'compute' ? 1 : 2; const visibilityByStage = stages === 'compute' ? [GPUShaderStage.COMPUTE] : [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: {}, }); }; const addExternalTextureDeclaration = (stage: number, binding: number, id: number) => { textureDeclarations[stage].push( `@group(${stage * 2}) @binding(${binding}) var e${stage}_${id}: texture_external;` ); bindGroupLayoutEntries[stage * 2].push({ binding, visibility: visibilityByStage[stage], externalTexture: {}, }); }; for (let stage = 0; stage < numStages; ++stage) { let count = 0; for (let t = 0; count < numCombinations && t < maxTexturesPerStage; ++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);`); } for (let t = 0; t < numExternal; ++t) { if (t === 0 || !useSame) { const et = textureDeclarations[stage].length + t; addExternalTextureDeclaration(stage, et, t); } const eBinding = useSame ? 0 : t; const sBinding = useSame ? t : 0; usages[stage].push( ` c += textureSampleBaseClampToEdge(e${stage}_${eBinding}, s${stage}_${sBinding}, vec2f(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); } @fragment fn fs() -> @location(0) vec4f { return ${stages.includes('fragment') ? 'usage1()' : 'vec4f(0)'}; } @compute @workgroup_size(1) fn cs() { _ = usage0(); } `; // 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 }); if (stages === 'compute') { vtu.doCreateComputePipelineTest(t, async, pass || !t.isCompatibility, { layout, compute: { module }, }); } else { vtu.doCreateRenderPipelineTest(t, async, pass || !t.isCompatibility, { layout, vertex: { module }, fragment: { module, targets: [{ format: 'rgba8unorm' }] }, }); } }); g.test('fine_derivatives') .desc( ` Test that dpdxFine, dpdyFine, fwidthFine are disallowed in compatibility mode. ` ) .params(u => u .combine('builtin', [ 'dpdxCoarse', // to check the test itself, should pass always 'dpdxFine', 'dpdyFine', 'fwidthFine', ] as const) .combine('async', [false, true] as const) .beginSubcases() .combine('type', ['f32', 'vec2f', 'vec3f', 'vec4f']) ) .fn(t => { const { builtin, async, type } = t.params; const code = ` struct VOut { @builtin(position) pos: vec4f, @location(0) v: ${type}, }; @vertex fn vs(@builtin(vertex_index) vNdx: u32) -> VOut { let pos = array(vec2f(-1, 3), vec2f(3, -1), vec2f(-1, -1)); return VOut(vec4f(pos[vNdx], 0, 1), ${type}(pos[vNdx].x)); } @fragment fn fs(v: VOut) -> @location(0) vec4f { _ = ${builtin}(v.v); return vec4f(0); } `; const module = t.device.createShaderModule({ code }); const success = !t.isCompatibility || builtin === 'dpdxCoarse'; vtu.doCreateRenderPipelineTest(t, async, success, { layout: 'auto', vertex: { module }, fragment: { module, targets: [{ format: 'rgba8unorm' }] }, }); });