diff --git a/src/webgpu/api/validation/capability_checks/limits/maxInterStageShaderVariables.spec.ts b/src/webgpu/api/validation/capability_checks/limits/maxInterStageShaderVariables.spec.ts index 7df8fa3cdafb..29a7d5fe8c4b 100644 --- a/src/webgpu/api/validation/capability_checks/limits/maxInterStageShaderVariables.spec.ts +++ b/src/webgpu/api/validation/capability_checks/limits/maxInterStageShaderVariables.spec.ts @@ -1,24 +1,77 @@ -import { range } from '../../../../../common/util/util.js'; +import { Fixture } from '../../../../../common/framework/fixture.js'; +import { keysOf } from '../../../../../common/util/data_tables.js'; +import { hasFeature, range } from '../../../../../common/util/util.js'; import { kMaximumLimitBaseParams, makeLimitTestGroup } from './limit_utils.js'; +const kFragmentInputTypes = { + front_facing: 'bool', + sample_index: 'u32', + sample_mask: 'u32', + primitive_index: 'u32', + subgroup_invocation_id: 'u32', + subgroup_size: 'u32', +} as const; + +const kFragmentInputs = keysOf(kFragmentInputTypes); + +const kItemsThatCountAgainstLimit = ['point-list', ...kFragmentInputs] as const; + +const kExtraItems = [ + 'sample_mask_out', // special - see below +] as const; + +/** + * Generates every combination of size elements from array + * combinations([a, b, c], 2) generates [a, b], [a, c], [b, c] + */ +function* combinations( + arr: readonly T[], + size: number, + start = 0, + path: T[] = [] +): Generator { + if (path.length === size) { + yield [...path]; + return; + } + + for (let i = start; i < arr.length; i++) { + path.push(arr[i]); + yield* combinations(arr, size, i + 1, path); + path.pop(); + } +} + +const kTestItems = [...kItemsThatCountAgainstLimit, ...kExtraItems] as const; +const kTestItemCombinations = [ + [], // no builtins case + ...combinations(kTestItems, 1), // one builtin + ...combinations(kTestItems, 2), // 2 builtins + ...combinations(kTestItems, 3), // 3 builtins + kTestItems, // all builtins case +] as const; + +const requiresSubgroupsFeature = (items: Set<(typeof kTestItems)[number]>) => + items.has('subgroup_invocation_id') || items.has('subgroup_size'); + function getPipelineDescriptor( + t: Fixture, device: GPUDevice, testValue: number, - pointList: boolean, - frontFacing: boolean, - sampleIndex: boolean, - sampleMaskIn: boolean, - sampleMaskOut: boolean + items: Set<(typeof kTestItems)[number]> ): GPURenderPipelineDescriptor { - const vertexOutputDeductions = pointList ? 1 : 0; - const fragmentInputDeductions = [frontFacing, sampleIndex, sampleMaskIn] + const vertexOutputDeductions = items.has('point-list') ? 1 : 0; + const usedFragInputs = [...items.values()].filter(p => p in kFragmentInputTypes); + const fragmentInputDeductions = usedFragInputs .map(p => (p ? 1 : 0) as number) - .reduce((acc, p) => acc + p); + .reduce((acc, p) => acc + p, 0); - const vertexOutputVariables = testValue - vertexOutputDeductions; - const fragmentInputVariables = testValue - fragmentInputDeductions; - const numInterStageVariables = Math.min(vertexOutputVariables, fragmentInputVariables); + t.debug(() => `device features: ${[...device.features].join(', ')}`); + + const numVertexOutputVariables = testValue - vertexOutputDeductions; + const numFragmentInputVariables = testValue - fragmentInputDeductions; + const numInterStageVariables = Math.min(numVertexOutputVariables, numFragmentInputVariables); const maxVertexOutputVariables = device.limits.maxInterStageShaderVariables - vertexOutputDeductions; @@ -26,50 +79,68 @@ function getPipelineDescriptor( device.limits.maxInterStageShaderVariables - fragmentInputDeductions; const maxInterStageVariables = Math.min(maxVertexOutputVariables, maxFragmentInputVariables); - const varyings = ` - ${range(numInterStageVariables, i => `@location(${i}) v4_${i}: vec4f,`).join('\n')} - `; + const fragInputs = usedFragInputs + .map( + (input, i) => + ` @builtin(${input}) i_${i}: ${ + kFragmentInputTypes[input as keyof typeof kFragmentInputTypes] + },` + ) + .join('\n'); + + const varyings = `${range( + numInterStageVariables, + i => ` @location(${i}) v4_${i}: vec4f,` + ).join('\n')}`; const code = ` // test value : ${testValue} - // maxInterStageShaderVariables : ${device.limits.maxInterStageShaderVariables} - // num variables in vertex shader : ${vertexOutputVariables}${pointList ? ' + point-list' : ''} - // num variables in fragment shader : ${fragmentInputVariables}${ - frontFacing ? ' + front-facing' : '' - }${sampleIndex ? ' + sample_index' : ''}${sampleMaskIn ? ' + sample_mask' : ''} + // maxInterStageShaderVariables : ${device.limits.maxInterStageShaderVariables} + // num variables in vertex shader : ${numVertexOutputVariables}${ + items.has('point-list') ? ' + point-list' : '' + } + // num variables in fragment shader : ${numFragmentInputVariables} + ${usedFragInputs.join( + ' + ' + )} // maxInterStageVariables: : ${maxInterStageVariables} // num used inter stage variables : ${numInterStageVariables} + ${items.has('primitive_index') ? 'enable primitive_index;' : ''} + ${requiresSubgroupsFeature(items) ? 'enable subgroups;' : ''} + struct VSOut { @builtin(position) p: vec4f, - ${varyings} +${varyings} } struct FSIn { - ${frontFacing ? '@builtin(front_facing) frontFacing: bool,' : ''} - ${sampleIndex ? '@builtin(sample_index) sampleIndex: u32,' : ''} - ${sampleMaskIn ? '@builtin(sample_mask) sampleMask: u32,' : ''} - ${varyings} +${fragInputs} +${varyings} } + struct FSOut { @location(0) color: vec4f, - ${sampleMaskOut ? '@builtin(sample_mask) sampleMask: u32,' : ''} + ${items.has('sample_mask_out') ? '@builtin(sample_mask) sampleMask: u32,' : ''} } + @vertex fn vs() -> VSOut { var o: VSOut; o.p = vec4f(0); return o; } + @fragment fn fs(i: FSIn) -> FSOut { var o: FSOut; + o.color = vec4f(0); return o; } `; + t.debug(code); const module = device.createShaderModule({ code }); const pipelineDescriptor: GPURenderPipelineDescriptor = { layout: 'auto', primitive: { - topology: pointList ? 'point-list' : 'triangle-list', + topology: items.has('point-list') ? 'point-list' : 'triangle-list', }, vertex: { module, @@ -92,51 +163,59 @@ const limit = 'maxInterStageShaderVariables'; export const { g, description } = makeLimitTestGroup(limit); g.test('createRenderPipeline,at_over') - .desc(`Test using at and over ${limit} limit in createRenderPipeline(Async)`) + .desc( + ` +Test using at and over ${limit} limit in createRenderPipeline(Async) + +Note: We test combinations to make sure each entry is counted separately. +and that implementations don't accidentally add only 1 to the count when +2 or more builtins are used. We also include sample_mask as an output +to make sure it does not count against the limit since it has the same +name as sample_mask as an input. + ` + ) .params( - kMaximumLimitBaseParams - .combine('async', [false, true]) - .combine('pointList', [false, true]) - .combine('frontFacing', [false, true]) - .combine('sampleIndex', [false, true]) - .combine('sampleMaskIn', [false, true]) - .combine('sampleMaskOut', [false, true]) + kMaximumLimitBaseParams.combine('async', [false, true]).combine('items', kTestItemCombinations) ) - .beforeAllSubcases(t => { + .fn(async t => { + const { limitTest, testValueName, async, items: itemsAsArray } = t.params; + const items = new Set(itemsAsArray); + if (t.isCompatibility) { t.skipIf( - t.params.sampleMaskIn || t.params.sampleMaskOut, + items.has('sample_mask') || items.has('sample_mask_out'), 'sample_mask not supported in compatibility mode' ); - t.skipIf(t.params.sampleIndex, 'sample_index not supported in compatibility mode'); + t.skipIf(items.has('sample_index'), 'sample_index not supported in compatibility mode'); } - }) - .fn(async t => { - const { - limitTest, - testValueName, - async, - pointList, - frontFacing, - sampleIndex, - sampleMaskIn, - sampleMaskOut, - } = t.params; + + const features: GPUFeatureName[] = []; + + if (items.has('primitive_index')) { + if (hasFeature(t.adapter.features, 'primitive-index')) { + features.push('primitive-index'); + } else { + t.skip('primitive_index requires primitive-index feature'); + } + } + + if (requiresSubgroupsFeature(items)) { + if (hasFeature(t.adapter.features, 'subgroups')) { + features.push('subgroups'); + } else { + t.skip('subgroup_invocation_id or subgroup_size requires subgroups feature'); + } + } + await t.testDeviceWithRequestedMaximumLimits( limitTest, testValueName, async ({ device, testValue, shouldError }) => { - const pipelineDescriptor = getPipelineDescriptor( - device, - testValue, - pointList, - frontFacing, - sampleIndex, - sampleMaskIn, - sampleMaskOut - ); + const pipelineDescriptor = getPipelineDescriptor(t, device, testValue, items); await t.testCreateRenderPipeline(pipelineDescriptor, async, shouldError); - } + }, + undefined, + features ); });