diff --git a/src/webgpu/api/validation/non_filterable_texture.spec.ts b/src/webgpu/api/validation/non_filterable_texture.spec.ts new file mode 100644 index 000000000000..11057c397704 --- /dev/null +++ b/src/webgpu/api/validation/non_filterable_texture.spec.ts @@ -0,0 +1,126 @@ +export const description = ` +Tests that non-filterable textures used with filtering samplers generate a validation error. +`; + +import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { keysOf } from '../../../common/util/data_tables.js'; + +import { ValidationTest } from './validation_test.js'; + +const kNonFilterableCaseInfo: Record = { + sint: { type: 'i32', component: '0,' }, + uint: { type: 'u32', component: '0,' }, + float: { type: 'f32', component: '0,' }, // no error for f32 + 'unfilterable-float': { type: 'f32', component: '0,' }, // no error for f32 + depth: { type: 'depth', component: '' }, +}; +const kNonFilterableCases = keysOf(kNonFilterableCaseInfo); + +export const g = makeTestGroup(ValidationTest); + +g.test('non_filterable_texture_with_filtering_sampler') + .desc( + 'test that createXXXPipeline generates a validation error if a depth/u32/i32 texture binding is used with a filtering sampler binding' + ) + .params(u => + u + .combine('pipeline', ['compute', 'render']) + .combine('async', [true, false] as const) + .combine('sampleType', kNonFilterableCases) + .combine('viewDimension', ['2d', '2d-array', 'cube', 'cube-array'] as const) + .combine('sameGroup', [true, false] as const) + ) + .beforeAllSubcases(t => t.skipIfTextureViewDimensionNotSupported(t.params.viewDimension)) + .fn(t => { + const { device } = t; + const { pipeline, async, sampleType, viewDimension, sameGroup } = t.params; + const { type, component } = kNonFilterableCaseInfo[sampleType]; + + const coord = viewDimension.startsWith('2d') ? 'vec2f(0)' : 'vec3f(0)'; + const dimensionSuffix = viewDimension.replace('-', '_'); + const textureType = + type === 'depth' ? `texture_depth_${dimensionSuffix}` : `texture_${dimensionSuffix}<${type}>`; + const layer = viewDimension.endsWith('-array') ? ', 0' : ''; + + const groupNdx = sameGroup ? 0 : 1; + + const module = device.createShaderModule({ + code: ` + @group(0) @binding(0) var t: ${textureType}; + @group(${groupNdx}) @binding(1) var s: sampler; + + fn test() { + _ = textureGather(${component} t, s, ${coord}${layer}); + } + + @compute @workgroup_size(1) fn cs() { + test(); + } + + @vertex fn vs() -> @builtin(position) vec4f { + return vec4f(0); + } + + @fragment fn fs() -> @location(0) vec4f { + test(); + return vec4f(0); + } + `, + }); + + const bindGroup0LayoutEntries: GPUBindGroupLayoutEntry[] = [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT, + texture: { + sampleType, + viewDimension, + multisampled: false, + }, + }, + ]; + + const samplerBGLEntry: GPUBindGroupLayoutEntry = { + binding: 1, + visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT, + sampler: { + type: 'filtering', + }, + }; + + if (sameGroup) { + bindGroup0LayoutEntries.push(samplerBGLEntry); + } + + const bindGroupLayout0 = device.createBindGroupLayout({ + entries: bindGroup0LayoutEntries, + }); + + const pipelineLayoutDesc = { + bindGroupLayouts: [bindGroupLayout0], + }; + + if (!sameGroup) { + const bindGroupLayout1 = device.createBindGroupLayout({ + entries: [samplerBGLEntry], + }); + pipelineLayoutDesc.bindGroupLayouts.push(bindGroupLayout1); + } + + const layout = device.createPipelineLayout(pipelineLayoutDesc); + + const success = sampleType === 'float'; + + if (pipeline === 'compute') { + t.doCreateComputePipelineTest(async, success, { + layout, + compute: { module }, + }); + } else { + t.doCreateRenderPipelineTest(async, success, { + layout, + vertex: { module }, + fragment: { module, targets: [{ format: 'rgba8unorm' }] }, + }); + } + });