blob: 1b632bd70ab0e63572394047b8a2c4872fa07830 [file] [log] [blame]
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<f32>',
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<f32>';
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<f32>;`
);
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' }] },
});
});