export const description = `Validation tests for entry point built-in variables`; import { makeTestGroup } from '../../../../common/framework/test_group.js'; import { keysOf } from '../../../../common/util/data_tables.js'; import { WGSLLanguageFeature } from '../../../capability_info.js'; import { ShaderValidationTest } from '../shader_validation_test.js'; import { generateShader } from './util.js'; export const g = makeTestGroup(ShaderValidationTest); interface Builtin { name: string; stage: 'vertex' | 'fragment' | 'compute'; io: 'in' | 'out'; type: string; enable?: string; requires?: WGSLLanguageFeature; } // List of all built-in variables and their stage, in|out usage, and type. // Taken from table in Section 15: // https://www.w3.org/TR/WGSL/#builtin-inputs-outputs export const kBuiltins: readonly Builtin[] = [ { name: 'vertex_index', stage: 'vertex', io: 'in', type: 'u32' }, { name: 'instance_index', stage: 'vertex', io: 'in', type: 'u32' }, { name: 'position', stage: 'vertex', io: 'out', type: 'vec4' }, { name: 'position', stage: 'fragment', io: 'in', type: 'vec4' }, { name: 'front_facing', stage: 'fragment', io: 'in', type: 'bool' }, { name: 'frag_depth', stage: 'fragment', io: 'out', type: 'f32' }, { name: 'local_invocation_id', stage: 'compute', io: 'in', type: 'vec3' }, { name: 'local_invocation_index', stage: 'compute', io: 'in', type: 'u32' }, { name: 'global_invocation_id', stage: 'compute', io: 'in', type: 'vec3' }, { name: 'workgroup_id', stage: 'compute', io: 'in', type: 'vec3' }, { name: 'num_workgroups', stage: 'compute', io: 'in', type: 'vec3' }, { name: 'sample_index', stage: 'fragment', io: 'in', type: 'u32' }, { name: 'sample_mask', stage: 'fragment', io: 'in', type: 'u32' }, { name: 'sample_mask', stage: 'fragment', io: 'out', type: 'u32' }, { name: 'subgroup_invocation_id', stage: 'compute', io: 'in', type: 'u32', enable: 'subgroups' }, { name: 'subgroup_size', stage: 'compute', io: 'in', type: 'u32', enable: 'subgroups' }, { name: 'subgroup_invocation_id', stage: 'fragment', io: 'in', type: 'u32', enable: 'subgroups' }, { name: 'subgroup_size', stage: 'fragment', io: 'in', type: 'u32', enable: 'subgroups' }, { name: 'clip_distances', stage: 'vertex', io: 'out', type: 'array', enable: 'clip_distances', }, { name: 'clip_distances', stage: 'vertex', io: 'out', type: 'array', enable: 'clip_distances', }, { name: 'clip_distances', stage: 'vertex', io: 'out', type: 'array', enable: 'clip_distances', }, { name: 'clip_distances', stage: 'vertex', io: 'out', type: 'array', enable: 'clip_distances', }, { name: 'clip_distances', stage: 'vertex', io: 'out', type: 'array', enable: 'clip_distances', }, { name: 'clip_distances', stage: 'vertex', io: 'out', type: 'array', enable: 'clip_distances', }, { name: 'clip_distances', stage: 'vertex', io: 'out', type: 'array', enable: 'clip_distances', }, { name: 'clip_distances', stage: 'vertex', io: 'out', type: 'array', enable: 'clip_distances', }, { name: 'primitive_id', stage: 'fragment', io: 'in', type: 'u32', enable: 'chromium_experimental_primitive_id', }, { name: 'subgroup_id', stage: 'compute', io: 'in', type: 'u32', enable: 'subgroups', requires: 'subgroup_id', }, { name: 'num_subgroups', stage: 'compute', io: 'in', type: 'u32', enable: 'subgroups', requires: 'subgroup_id', }, { name: 'workgroup_index', stage: 'compute', io: 'in', type: 'u32', requires: 'linear_indexing', }, { name: 'global_invocation_index', stage: 'compute', io: 'in', type: 'u32', requires: 'linear_indexing', }, ] as const; // List of types to test against. const kTestTypes = [ 'bool', 'u32', 'i32', 'f32', 'vec2', 'vec2', 'vec2', 'vec2', 'vec3', 'vec3', 'vec3', 'vec3', 'vec4', 'vec4', 'vec4', 'vec4', 'mat2x2', 'mat2x3', 'mat2x4', 'mat3x2', 'mat3x3', 'mat3x4', 'mat4x2', 'mat4x3', 'mat4x4', 'atomic', 'atomic', 'array', 'array', 'array', 'array', 'array', 'array', 'array', 'array', 'array', 'array', 'array', 'array', 'MyStruct', ] as const; g.test('stage_inout') .desc( `Test that each @builtin attribute is validated against the required stage and in/out usage for that built-in variable.` ) .params(u => u .combineWithParams(kBuiltins) .combine('use_struct', [true, false] as const) .combine('target_stage', ['', 'vertex', 'fragment', 'compute'] as const) .combine('target_io', ['in', 'out'] as const) .beginSubcases() ) .beforeAllSubcases(t => { t.skipIf( t.isCompatibility && ['sample_index', 'sample_mask'].includes(t.params.name), 'compatibility mode does not support sample_index or sample_mask' ); t.skipIf( t.params.name !== 'position' && t.params.target_stage === 'vertex' && t.params.target_io === 'out' && !t.params.use_struct, 'missing @builtin(position) in the vertex output when the vertex output is not a struct' ); }) .fn(t => { const code = generateShader({ attribute: `@builtin(${t.params.name})`, type: t.params.type, stage: t.params.target_stage, io: t.params.target_io, use_struct: t.params.use_struct, enable: t.params.enable, }); // Expect to pass iff the built-in table contains an entry that matches. const expectation = kBuiltins.some( x => x.name === t.params.name && (x.stage === t.params.target_stage || (t.params.use_struct && t.params.target_stage === '')) && (x.io === t.params.target_io || t.params.target_stage === '') && x.type === t.params.type && (x.requires === undefined || t.hasLanguageFeature(x.requires)) ); t.expectCompileResult(expectation, code); }); g.test('type') .desc( `Test that each @builtin attribute is validated against the required type of that built-in variable.` ) .params(u => u .combineWithParams(kBuiltins) .combine('use_struct', [true, false] as const) .beginSubcases() .combine('target_type', kTestTypes) ) .beforeAllSubcases(t => { t.skipIf( t.isCompatibility && ['sample_index', 'sample_mask'].includes(t.params.name), 'compatibility mode does not support sample_index or sample_mask' ); t.skipIf( t.params.name !== 'position' && t.params.stage === 'vertex' && t.params.io === 'out' && !t.params.use_struct, 'missing @builtin(position) in the vertex output' ); }) .fn(t => { let code = ''; if (t.params.target_type === 'MyStruct') { // Generate a struct that contains the correct built-in type. code += 'struct MyStruct {\n'; code += ` value : ${t.params.type}\n`; code += '};\n\n'; } code += generateShader({ attribute: `@builtin(${t.params.name})`, type: t.params.target_type, stage: t.params.stage, io: t.params.io, use_struct: t.params.use_struct, enable: t.params.enable, }); // Expect to pass iff the built-in table contains an entry that matches. const expectation = kBuiltins.some( x => x.name === t.params.name && x.stage === t.params.stage && x.io === t.params.io && x.type === t.params.target_type && (x.requires === undefined || t.hasLanguageFeature(x.requires)) ); t.expectCompileResult(expectation, code); }); g.test('nesting') .desc(`Test validation of nested built-in variables`) .params(u => u .combine('target_stage', ['fragment', ''] as const) .combine('target_io', ['in', 'out'] as const) .beginSubcases() ) .fn(t => { // Generate a struct that contains a frag_depth builtin, nested inside another struct. let code = ` struct Inner { @builtin(frag_depth) value : f32 }; struct Outer { inner : Inner };`; code += generateShader({ attribute: '', type: 'Outer', stage: t.params.target_stage, io: t.params.target_io, use_struct: false, }); // Expect to pass only if the struct is not used for entry point IO. t.expectCompileResult(t.params.target_stage === '', code); }); g.test('duplicates') .desc(`Test that duplicated built-in variables are validated.`) .params(u => u // Place two @builtin(sample_mask) attributes onto the entry point function. // We use `sample_mask` as it is valid as both an input and output for the same entry point. // The function: // - has two non-struct parameters (`p1` and `p2`) // - has two struct parameters each with two members (`s1{a,b}` and `s2{a,b}`) // - returns a struct with two members (`ra` and `rb`) // By default, all of these variables will have unique @location() attributes. .combine('first', ['p1', 's1a', 's2a', 'ra'] as const) .combine('second', ['p2', 's1b', 's2b', 'rb'] as const) .beginSubcases() ) .beforeAllSubcases(t => { t.skipIf(t.isCompatibility, 'compatibility mode does not support sample_mask'); }) .fn(t => { const p1 = t.params.first === 'p1' ? '@builtin(sample_mask)' : '@location(1) @interpolate(flat, either)'; const p2 = t.params.second === 'p2' ? '@builtin(sample_mask)' : '@location(2) @interpolate(flat, either)'; const s1a = t.params.first === 's1a' ? '@builtin(sample_mask)' : '@location(3) @interpolate(flat, either)'; const s1b = t.params.second === 's1b' ? '@builtin(sample_mask)' : '@location(4) @interpolate(flat, either)'; const s2a = t.params.first === 's2a' ? '@builtin(sample_mask)' : '@location(5) @interpolate(flat, either)'; const s2b = t.params.second === 's2b' ? '@builtin(sample_mask)' : '@location(6) @interpolate(flat, either)'; const ra = t.params.first === 'ra' ? '@builtin(sample_mask)' : '@location(1) @interpolate(flat, either)'; const rb = t.params.second === 'rb' ? '@builtin(sample_mask)' : '@location(2) @interpolate(flat, either)'; const code = ` struct S1 { ${s1a} a : u32, ${s1b} b : u32, }; struct S2 { ${s2a} a : u32, ${s2b} b : u32, }; struct R { ${ra} a : u32, ${rb} b : u32, }; @fragment fn main(${p1} p1 : u32, ${p2} p2 : u32, s1 : S1, s2 : S2, ) -> R { return R(); } `; // The test should fail if both @builtin(sample_mask) attributes are on the input parameters // or structures, or it they are both on the output struct. Otherwise it should pass. const firstIsRet = t.params.first === 'ra'; const secondIsRet = t.params.second === 'rb'; const expectation = firstIsRet !== secondIsRet; t.expectCompileResult(expectation, code); }); g.test('missing_vertex_position') .desc(`Test that vertex shaders are required to output @builtin(position).`) .params(u => u .combine('use_struct', [true, false] as const) .combine('attribute', ['@builtin(position)', '@location(0)'] as const) .beginSubcases() ) .fn(t => { const code = ` struct S { ${t.params.attribute} value : vec4 }; @vertex fn main() -> ${t.params.use_struct ? 'S' : `${t.params.attribute} vec4`} { return ${t.params.use_struct ? 'S' : 'vec4'}(); } `; // Expect to pass only when using @builtin(position). t.expectCompileResult(t.params.attribute === '@builtin(position)', code); }); g.test('reuse_builtin_name') .desc(`Test that a builtin name can be used in different contexts`) .params(u => u .combineWithParams(kBuiltins) .combine('use', ['alias', 'struct', 'function', 'module-var', 'function-var']) ) .fn(t => { let code = ''; if (t.params.use === 'alias') { code += `alias ${t.params.name} = i32;`; } else if (t.params.use === `struct`) { code += `struct ${t.params.name} { i: f32, }`; } else if (t.params.use === `function`) { code += `fn ${t.params.name}() {}`; } else if (t.params.use === `module-var`) { code += `const ${t.params.name} = 1;`; } else if (t.params.use === `function-var`) { code += `fn test() { let ${t.params.name} = 1; }`; } t.expectCompileResult(true, code); }); const kBuiltinTests = { pos: { src: `@builtin(position)`, pass: true, }, trailing_comma: { src: `@builtin(position,)`, pass: true, }, newline_in_attr: { src: `@ \n builtin(position)`, pass: true, }, whitespace_in_attr: { src: `@/* comment */builtin/* comment */\n\n(\t/*comment*/position/*comment*/)`, pass: true, }, invalid_name: { src: `@abuiltin(position)`, pass: false, }, no_params: { src: `@builtin`, pass: false, }, missing_param: { src: `@builtin()`, pass: false, }, missing_parens: { src: `@builtin position`, pass: false, }, missing_lparen: { src: `@builtin position)`, pass: false, }, missing_rparen: { src: `@builtin(position`, pass: false, }, multiple_params: { src: `@builtin(position, frag_depth)`, pass: false, }, ident_param: { src: `@builtin(identifier)`, pass: false, }, number_param: { src: `@builtin(2)`, pass: false, }, duplicate: { src: `@builtin(position) @builtin(position)`, pass: false, }, }; g.test('parse_builtin') .desc(`Test that @builtin is parsed correctly.`) .params(u => u.combine('builtin', keysOf(kBuiltinTests))) .fn(t => { const src = kBuiltinTests[t.params.builtin].src; const code = ` @vertex fn main() -> ${src} vec4 { return vec4(.4, .2, .3, .1); }`; t.expectCompileResult(kBuiltinTests[t.params.builtin].pass, code); }); g.test('placement') .desc('Tests the locations @builtin is allowed to appear') .params(u => u .combine('scope', [ // The fn-param and fn-ret are part of the shader_io/builtins tests 'private-var', 'storage-var', 'struct-member', 'non-ep-param', 'non-ep-ret', 'fn-decl', 'fn-var', 'while-stmt', undefined, ] as const) .combine('attribute', [ { 'private-var': false, 'storage-var': false, 'struct-member': true, 'non-ep-param': false, 'non-ep-ret': false, 'fn-decl': false, 'fn-var': false, 'fn-return': false, 'while-stmt': false, }, ]) .beginSubcases() ) .fn(t => { const scope = t.params.scope; const attr = '@builtin(vertex_index)'; const code = ` ${scope === 'private-var' ? attr : ''} var priv_var : u32; ${scope === 'storage-var' ? attr : ''} @group(0) @binding(0) var stor_var : u32; struct A { ${scope === 'struct-member' ? attr : ''} a : u32, } fn v(${scope === 'non-ep-param' ? attr : ''} i : u32) -> ${scope === 'non-ep-ret' ? attr : ''} u32 { return 1; } @vertex ${scope === 'fn-decl' ? attr : ''} fn f( @location(0) b : u32, ) -> @builtin(position) vec4f { ${scope === 'fn-var' ? attr : ''} var func_v : u32; ${scope === 'while-stmt' ? attr : ''} while false {} return vec4(1, 1, 1, 1); } `; t.expectCompileResult(scope === undefined || t.params.attribute[scope], code); }); const kFragDepthTests = { unset: { src: `@builtin(frag_depth)`, pass: true, requires_feature: false, }, less: { src: `@builtin(frag_depth, less)`, pass: true, requires_feature: true, }, greater: { src: `@builtin(frag_depth, greater)`, pass: true, requires_feature: true, }, trailing_comma: { src: `@builtin(frag_depth, less,)`, pass: true, requires_feature: true, }, missing_enum: { src: `@builtin(frag_depth,)`, pass: true, requires_feature: false, }, invalid_enum: { src: `@builtin(frag_depth, any)`, pass: false, requires_feature: false, }, missing_comma: { src: `@builtin(frag_depth greater)`, pass: false, requires_feature: false, }, }; g.test('parse_frag_depth') .desc(`Test that @builtin is parsed correctly.`) .params(u => u.combine('builtin', keysOf(kFragDepthTests))) .fn(t => { const data = kFragDepthTests[t.params.builtin]; if (data.requires_feature) { t.skipIfLanguageFeatureNotSupported('fragment_depth'); } const code = ` @fragment fn main() -> ${data.src} f32 { return .5; }`; t.expectCompileResult(data.pass, code); });