diff --git a/src/webgpu/listing_meta.json b/src/webgpu/listing_meta.json index ac2c73f31a2a..42717009e122 100644 --- a/src/webgpu/listing_meta.json +++ b/src/webgpu/listing_meta.json @@ -1928,6 +1928,10 @@ "webgpu:shader,validation,expression,call,builtin,sqrt:values:*": { "subcaseMS": 0.302 }, "webgpu:shader,validation,expression,call,builtin,tan:integer_argument:*": { "subcaseMS": 1.734 }, "webgpu:shader,validation,expression,call,builtin,tan:values:*": { "subcaseMS": 0.350 }, + "webgpu:shader,validation,expression,call,builtin,textureSample:array_index_argument:*": { "subcaseMS": 1.888 }, + "webgpu:shader,validation,expression,call,builtin,textureSample:coords_argument:*": { "subcaseMS": 1.342 }, + "webgpu:shader,validation,expression,call,builtin,textureSample:offset_argument,non_const:*": { "subcaseMS": 1.604 }, + "webgpu:shader,validation,expression,call,builtin,textureSample:offset_argument:*": { "subcaseMS": 1.401 }, "webgpu:shader,validation,expression,call,builtin,unpack4xI8:bad_args:*": { "subcaseMS": 121.263 }, "webgpu:shader,validation,expression,call,builtin,unpack4xI8:must_use:*": { "subcaseMS": 35.200 }, "webgpu:shader,validation,expression,call,builtin,unpack4xI8:supported:*": { "subcaseMS": 24.150 }, diff --git a/src/webgpu/shader/validation/expression/call/builtin/textureSample.spec.ts b/src/webgpu/shader/validation/expression/call/builtin/textureSample.spec.ts new file mode 100644 index 000000000000..51917e83b096 --- /dev/null +++ b/src/webgpu/shader/validation/expression/call/builtin/textureSample.spec.ts @@ -0,0 +1,254 @@ +const builtin = 'textureSample'; +export const description = ` +Validation tests for the ${builtin}() builtin. + +* test textureSample coords parameter must be correct type +* test textureSample array_index parameter must be correct type +* test textureSample coords parameter must be correct type +* test textureSample offset parameter must be correct type +* test textureSample offset parameter must be a const-expression +* test textureSample offset parameter must be between -8 and +7 inclusive +`; + +import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; +import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js'; +import { + kConcreteIntegerScalarsAndVectors, + kConvertableToFloatScalarsAndVectors, +} from '../../../../../util/conversion.js'; +import { ShaderValidationTest } from '../../../shader_validation_test.js'; + +type TextureSampleParams = { + coords: string; + array_index?: boolean; + offset?: string; +}; + +const kValidTextureSampleParameterTypes: { [n: string]: TextureSampleParams } = { + 'texture_1d': { coords: 'f32' }, + 'texture_2d': { coords: 'vec2', offset: 'vec2' }, + 'texture_2d_array': { coords: 'vec2', array_index: true, offset: 'vec2' }, + 'texture_3d': { coords: 'vec3', offset: 'vec3' }, + 'texture_cube': { coords: 'vec3' }, + 'texture_cube_array': { coords: 'vec3', array_index: true }, + texture_depth_2d: { coords: 'vec2', offset: 'vec2' }, + texture_depth_2d_array: { coords: 'vec2', array_index: true }, + texture_depth_cube: { coords: 'vec3' }, + texture_depth_cube_array: { coords: 'vec3', array_index: true }, +} as const; + +const kTextureTypes = keysOf(kValidTextureSampleParameterTypes); +const kValuesTypes = objectsToRecord([ + ...kConvertableToFloatScalarsAndVectors, + ...kConcreteIntegerScalarsAndVectors, +] as const); + +function innerType(type: string) { + const angleNdx = type.indexOf('<'); + return type.substring(angleNdx + 1, type.length - (angleNdx >= 0 ? 1 : 0)); +} + +// Replace with non-hacky version +function isConvertible(src: string, dst: string) { + if (src === dst) { + return true; + } + const angleNdx = src.indexOf('<'); + if (src.substring(0, angleNdx) !== dst.substring(0, angleNdx)) { + return false; + } + + src = src.substring(angleNdx + 1, src.length - (angleNdx >= 0 ? 1 : 0)); + dst = dst.substring(angleNdx + 1, dst.length - (angleNdx >= 0 ? 1 : 0)); + + switch (src) { + case 'abstract-float': + switch (dst) { + case 'abstract-float': + case 'f16': + case 'f32': + case 'f64': + return true; + default: + return false; + } + case 'abstract-int': + switch (dst) { + case 'abstract-int': + case 'abstract-float': + case 'f16': + case 'f32': + case 'f64': + case 'u16': + case 'u32': + case 'u8': + case 'i16': + case 'i32': + case 'i8': + return true; + default: + return false; + } + default: + return false; + } +} + +export const g = makeTestGroup(ShaderValidationTest); + +g.test('coords_argument') + .desc( + ` +Validates that only incorrect coords arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('coordType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-1, 0, 1] as const) + .expand('offset', ({ textureType }) => { + const offset = kValidTextureSampleParameterTypes[textureType].offset; + return offset ? ['', offset] : ['']; + }) + ) + .fn(t => { + const { textureType, coordType, value, offset } = t.params; + const args = [kValuesTypes[coordType].create(value)]; + const { array_index, coords } = kValidTextureSampleParameterTypes[textureType]; + + const coordWGSL = args.map(arg => arg.wgsl()).join(', '); + const arrayWGSL = array_index ? ', 0' : ''; + const offsetWGSL = offset ? `, ${offset}(0)` : ''; + + const code = ` + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: ${textureType}; + @fragment fn fs() -> @location(0) vec4f { + let v = textureSample(t, s, ${coordWGSL}${arrayWGSL}${offsetWGSL}); + return vec4f(0); + } + `; + + const expectSuccess = isConvertible(coordType, coords); + + t.expectCompileResult(expectSuccess, code); + }); + +g.test('array_index_argument') + .desc( + ` +Validates that only incorrect array_index arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no array_index + .filter(({ textureType }) => !!kValidTextureSampleParameterTypes[textureType].array_index) + .combine('arrayIndexType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + ) + .fn(t => { + const { textureType, arrayIndexType, value } = t.params; + const args = [kValuesTypes[arrayIndexType].create(value)]; + const { coords, offset = '' } = kValidTextureSampleParameterTypes[textureType]; + + const coordWGSL = `${coords}(0)`; + const arrayWGSL = args.map(arg => arg.wgsl()).join(', '); + const offsetWGSL = offset ? `, ${offset}(0)` : ''; + + const code = ` + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: ${textureType}; + @fragment fn fs() -> @location(0) vec4f { + let v = textureSample(t, s, ${coordWGSL}, ${arrayWGSL}${offsetWGSL}); + return vec4f(0); + } + `; + + const expectSuccess = + isConvertible(arrayIndexType, 'i32') || isConvertible(arrayIndexType, 'u32'); + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument') + .desc( + ` +Validates that only incorrect offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + // filter out types with no offset + .filter( + ({ textureType }) => kValidTextureSampleParameterTypes[textureType].offset !== undefined + ) + .combine('offsetType', keysOf(kValuesTypes)) + .beginSubcases() + .combine('value', [-9, -8, 0, 7, 8]) + ) + .fn(t => { + const { textureType, offsetType, value } = t.params; + const args = [kValuesTypes[offsetType].create(value)]; + const { coords, array_index, offset = '' } = kValidTextureSampleParameterTypes[textureType]; + + const coordWGSL = `${coords}(0)`; + const arrayWGSL = array_index ? ', 0' : ''; + const offsetWGSL = args.map(arg => arg.wgsl()).join(', '); + + const code = ` + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: ${textureType}; + @fragment fn fs() -> @location(0) vec4f { + let v = textureSample(t, s, ${coordWGSL}${arrayWGSL}, ${offsetWGSL}); + return vec4f(0); + } + `; + + const expectSuccess = isConvertible(offsetType, offset) && value >= -8 && value <= 7; + t.expectCompileResult(expectSuccess, code); + }); + +g.test('offset_argument,non_const') + .desc( + ` +Validates that only non-const offset arguments are rejected by ${builtin} +` + ) + .params(u => + u + .combine('textureType', kTextureTypes) + .combine('varType', ['c', 'u', 'l']) + // filter out types with no offset + .filter( + ({ textureType }) => kValidTextureSampleParameterTypes[textureType].offset !== undefined + ) + ) + .fn(t => { + const { textureType, varType } = t.params; + const { coords, array_index, offset = '' } = kValidTextureSampleParameterTypes[textureType]; + + const coordWGSL = `${coords}(0)`; + const arrayWGSL = array_index ? ', 0' : ''; + const offsetWGSL = `${offset}(${varType})`; + const castWGSL = innerType(offset); + + const code = ` + @group(0) @binding(0) var s: sampler; + @group(0) @binding(1) var t: ${textureType}; + @group(0) @binding(2) var u: ${offset}; + @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f { + const c = 1; + let l = ${offset}(${castWGSL}(p.x)); + let v = textureSample(t, s, ${coordWGSL}${arrayWGSL}, ${offsetWGSL}); + return vec4f(0); + } + `; + + const expectSuccess = varType === 'c'; + t.expectCompileResult(expectSuccess, code); + });