export const description = ` TODO: - interface matching between pipeline layout and shader - x= bind group index values, binding index values, multiple bindings - x= {superset, subset} `; import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js'; import { makeTestGroup } from '../../../common/framework/test_group.js'; import { kShaderStageCombinations, kShaderStages, ValidBindableResource, } from '../../capability_info.js'; import { GPUConst } from '../../constants.js'; import * as vtu from './validation_test_utils.js'; type BindableResourceType = ValidBindableResource | 'readonlyStorageBuf'; const kBindableResources = [ 'uniformBuf', 'storageBuf', 'readonlyStorageBuf', 'filtSamp', 'nonFiltSamp', 'compareSamp', 'sampledTex', 'sampledTexMS', 'readonlyStorageTex', 'writeonlyStorageTex', 'readwriteStorageTex', ] as const; const bindGroupLayoutEntryContents = { compareSamp: { sampler: { type: 'comparison', }, }, filtSamp: { sampler: { type: 'filtering', }, }, nonFiltSamp: { sampler: { type: 'non-filtering', }, }, sampledTex: { texture: { sampleType: 'unfilterable-float', }, }, sampledTexMS: { texture: { sampleType: 'unfilterable-float', multisampled: true, }, }, storageBuf: { buffer: { type: 'storage', }, }, readonlyStorageBuf: { buffer: { type: 'read-only-storage', }, }, uniformBuf: { buffer: { type: 'uniform', }, }, readonlyStorageTex: { storageTexture: { format: 'r32float', access: 'read-only', }, }, writeonlyStorageTex: { storageTexture: { format: 'r32float', access: 'write-only', }, }, readwriteStorageTex: { storageTexture: { format: 'r32float', access: 'read-write', }, }, } as const; class F extends AllFeaturesMaxLimitsGPUTest { createPipelineLayout( bindingInPipelineLayout: BindableResourceType, visibility: number ): GPUPipelineLayout { return this.device.createPipelineLayout({ bindGroupLayouts: [ this.device.createBindGroupLayout({ entries: [ { binding: 0, visibility, ...bindGroupLayoutEntryContents[bindingInPipelineLayout], }, ], }), ], }); } getBindableResourceShaderDeclaration(bindableResource: BindableResourceType): string { switch (bindableResource) { case 'compareSamp': return 'var tmp : sampler_comparison'; case 'filtSamp': case 'nonFiltSamp': return 'var tmp : sampler'; case 'sampledTex': return 'var tmp : texture_2d'; case 'sampledTexMS': return 'var tmp : texture_multisampled_2d'; case 'storageBuf': return 'var tmp : vec4u'; case 'readonlyStorageBuf': return 'var tmp : vec4u'; case 'uniformBuf': return 'var tmp : vec4u;'; case 'readonlyStorageTex': return 'var tmp : texture_storage_2d'; case 'writeonlyStorageTex': return 'var tmp : texture_storage_2d'; case 'readwriteStorageTex': return 'var tmp : texture_storage_2d'; } } } const bindingResourceCompatibleWithShaderStages = function ( bindingResource: BindableResourceType, shaderStages: number ): boolean { if ((shaderStages & GPUConst.ShaderStage.VERTEX) > 0) { switch (bindingResource) { case 'writeonlyStorageTex': case 'readwriteStorageTex': case 'storageBuf': return false; default: break; } } return true; }; export const g = makeTestGroup(F); g.test('pipeline_layout_shader_exact_match') .desc( ` Test that the binding type in the pipeline layout must match the related declaration in shader. Note that read-write storage textures in the pipeline layout can match write-only storage textures in the shader. ` ) .params(u => u .combine('bindingInPipelineLayout', kBindableResources) .combine('bindingInShader', kBindableResources) .beginSubcases() .combine('pipelineLayoutVisibility', kShaderStageCombinations) .combine('shaderStageWithBinding', kShaderStages) .combine('isBindingStaticallyUsed', [true, false] as const) .unless( p => // We don't test using non-filtering sampler in shader because it has the same declaration // as filtering sampler. p.bindingInShader === 'nonFiltSamp' || !bindingResourceCompatibleWithShaderStages( p.bindingInPipelineLayout, p.pipelineLayoutVisibility ) || !bindingResourceCompatibleWithShaderStages(p.bindingInShader, p.shaderStageWithBinding) ) ) .fn(t => { const { bindingInPipelineLayout, bindingInShader, pipelineLayoutVisibility, shaderStageWithBinding, isBindingStaticallyUsed, } = t.params; if (t.isCompatibility) { const bindingUsedWithVertexStage = (shaderStageWithBinding & GPUShaderStage.VERTEX) !== 0 || (pipelineLayoutVisibility & GPUShaderStage.VERTEX) !== 0; const bindingUsedWithFragmentStage = (shaderStageWithBinding & GPUShaderStage.FRAGMENT) !== 0 || (pipelineLayoutVisibility & GPUShaderStage.FRAGMENT) !== 0; const bindingIsStorageBuffer = bindingInPipelineLayout === 'readonlyStorageBuf' || bindingInPipelineLayout === 'storageBuf'; const bindingIsStorageTexture = bindingInPipelineLayout === 'readonlyStorageTex' || bindingInPipelineLayout === 'readwriteStorageTex' || bindingInPipelineLayout === 'writeonlyStorageTex'; t.skipIf( bindingUsedWithVertexStage && bindingIsStorageBuffer && t.device.limits.maxStorageBuffersInVertexStage === 0, 'Storage buffers can not be used in vertex shaders because maxStorageBuffersInVertexStage === 0' ); t.skipIf( bindingUsedWithVertexStage && bindingIsStorageTexture && t.device.limits.maxStorageTexturesInVertexStage === 0, 'Storage textures can not be used in vertex shaders because maxStorageTexturesInVertexStage === 0' ); t.skipIf( bindingUsedWithFragmentStage && bindingIsStorageBuffer && t.device.limits.maxStorageBuffersInFragmentStage === 0, 'Storage buffers can not be used in fragment shaders because maxStorageBuffersInFragmentStage === 0' ); t.skipIf( bindingUsedWithFragmentStage && bindingIsStorageTexture && t.device.limits.maxStorageTexturesInFragmentStage === 0, 'Storage textures can not be used in fragment shaders because maxStorageTexturesInFragmentStage === 0' ); } const layout = t.createPipelineLayout(bindingInPipelineLayout, pipelineLayoutVisibility); const bindResourceDeclaration = `@group(0) @binding(0) ${t.getBindableResourceShaderDeclaration( bindingInShader )}`; const staticallyUseBinding = isBindingStaticallyUsed ? '_ = tmp; ' : ''; const isAsync = false; let success = true; if (isBindingStaticallyUsed) { success = bindingInPipelineLayout === bindingInShader; // Filtering and non-filtering both have the same shader declaration. success ||= bindingInPipelineLayout === 'nonFiltSamp' && bindingInShader === 'filtSamp'; // Promoting storage textures that are read-write in the layout can be readonly in the shader. success ||= bindingInPipelineLayout === 'readwriteStorageTex' && bindingInShader === 'writeonlyStorageTex'; // The shader using the resource must be included in the visibility in the layout. success &&= (pipelineLayoutVisibility & shaderStageWithBinding) > 0; } switch (shaderStageWithBinding) { case GPUConst.ShaderStage.COMPUTE: { const computeShader = ` ${bindResourceDeclaration}; @compute @workgroup_size(1) fn main() { ${staticallyUseBinding} } `; vtu.doCreateComputePipelineTest(t, isAsync, success, { layout, compute: { module: t.device.createShaderModule({ code: computeShader, }), }, }); break; } case GPUConst.ShaderStage.VERTEX: { const vertexShader = ` ${bindResourceDeclaration}; @vertex fn main() -> @builtin(position) vec4f { ${staticallyUseBinding} return vec4f(); } `; vtu.doCreateRenderPipelineTest(t, isAsync, success, { layout, vertex: { module: t.device.createShaderModule({ code: vertexShader, }), }, depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'always' }, }); break; } case GPUConst.ShaderStage.FRAGMENT: { const fragmentShader = ` ${bindResourceDeclaration}; @fragment fn main() -> @location(0) vec4f { ${staticallyUseBinding} return vec4f(); } `; vtu.doCreateRenderPipelineTest(t, isAsync, success, { layout, vertex: { module: t.device.createShaderModule({ code: ` @vertex fn main() -> @builtin(position) vec4f { return vec4f(); }`, }), }, fragment: { module: t.device.createShaderModule({ code: fragmentShader, }), targets: [ { format: 'rgba8unorm', }, ], }, }); break; } } });