diff --git a/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts b/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts index 6c8dafac594f..2a103da86331 100644 --- a/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts +++ b/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts @@ -13,11 +13,18 @@ import { } from '../../../../format_info.js'; import { ValidationTest } from '../../validation_test.js'; -type TextureBindingType = 'sampled-texture' | 'multisampled-texture' | 'writeonly-storage-texture'; +type TextureBindingType = + | 'sampled-texture' + | 'multisampled-texture' + | 'writeonly-storage-texture' + | 'readonly-storage-texture' + | 'readwrite-storage-texture'; const kTextureBindingTypes = [ 'sampled-texture', 'multisampled-texture', 'writeonly-storage-texture', + 'readonly-storage-texture', + 'readwrite-storage-texture', ] as const; const SIZE = 32; @@ -39,7 +46,7 @@ class TextureUsageTracking extends ValidationTest { arrayLayerCount = 1, mipLevelCount = 1, sampleCount = 1, - format = 'rgba8unorm', + format = 'r32float', usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, } = options; @@ -75,6 +82,14 @@ class TextureUsageTracking extends ValidationTest { assert(format !== undefined); entry = { storageTexture: { access: 'write-only', format, viewDimension } }; break; + case 'readonly-storage-texture': + assert(format !== undefined); + entry = { storageTexture: { access: 'read-only', format, viewDimension } }; + break; + case 'readwrite-storage-texture': + assert(format !== undefined); + entry = { storageTexture: { access: 'read-write', format, viewDimension } }; + break; } return this.device.createBindGroupLayout({ @@ -107,7 +122,7 @@ class TextureUsageTracking extends ValidationTest { depthStencilFormat?: GPUTextureFormat ) { const bundleEncoder = this.device.createRenderBundleEncoder({ - colorFormats: ['rgba8unorm'], + colorFormats: ['r32float'], depthStencilFormat, }); bundleEncoder.setBindGroup(binding, bindGroup); @@ -129,16 +144,21 @@ class TextureUsageTracking extends ValidationTest { } /** - * Create two bind groups. Resource usages conflict between these two bind groups. But resource - * usage inside each bind group doesn't conflict. + * Create two bind groups with one texture view. */ - makeConflictingBindGroups() { + makeTwoBindGroupsWithOneTextureView(usage1: TextureBindingType, usage2: TextureBindingType) { const view = this.createTexture({ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING, }).createView(); const bindGroupLayouts = [ - this.createBindGroupLayout(0, 'sampled-texture', '2d'), - this.createBindGroupLayout(0, 'writeonly-storage-texture', '2d', { format: 'rgba8unorm' }), + this.createBindGroupLayout(0, usage1, '2d', { + sampleType: 'unfilterable-float', + format: 'r32float', + }), + this.createBindGroupLayout(0, usage2, '2d', { + sampleType: 'unfilterable-float', + format: 'r32float', + }), ]; return { bindGroupLayouts, @@ -155,14 +175,21 @@ class TextureUsageTracking extends ValidationTest { }; } - testValidationScope(compute: boolean): { + testValidationScope( + compute: boolean, + usage1: TextureBindingType, + usage2: TextureBindingType + ): { bindGroup0: GPUBindGroup; bindGroup1: GPUBindGroup; encoder: GPUCommandEncoder; pass: GPURenderPassEncoder | GPUComputePassEncoder; pipeline: GPURenderPipeline | GPUComputePipeline; } { - const { bindGroupLayouts, bindGroups } = this.makeConflictingBindGroups(); + const { bindGroupLayouts, bindGroups } = this.makeTwoBindGroupsWithOneTextureView( + usage1, + usage2 + ); const encoder = this.device.createCommandEncoder(); const pass = compute @@ -175,7 +202,7 @@ class TextureUsageTracking extends ValidationTest { }); const pipeline = compute ? this.createNoOpComputePipeline(pipelineLayout) - : this.createNoOpRenderPipeline(pipelineLayout); + : this.createNoOpRenderPipeline(pipelineLayout, 'r32float'); return { bindGroup0: bindGroups[0], bindGroup1: bindGroups[1], @@ -237,6 +264,8 @@ g.test('subresources_and_binding_types_combination_for_color') [ { _usageOK: true, type0: 'sampled-texture', type1: 'sampled-texture' }, { _usageOK: false, type0: 'sampled-texture', type1: 'writeonly-storage-texture' }, + { _usageOK: true, type0: 'sampled-texture', type1: 'readonly-storage-texture' }, + { _usageOK: false, type0: 'sampled-texture', type1: 'readwrite-storage-texture' }, { _usageOK: false, type0: 'sampled-texture', type1: 'render-target' }, // Race condition upon multiple writable storage texture is valid. // For p.compute === true, fails at pass.dispatch because aliasing exists. @@ -245,7 +274,34 @@ g.test('subresources_and_binding_types_combination_for_color') type0: 'writeonly-storage-texture', type1: 'writeonly-storage-texture', }, + { + _usageOK: true, + type0: 'readonly-storage-texture', + type1: 'readonly-storage-texture', + }, + { + _usageOK: !p.compute, + type0: 'readwrite-storage-texture', + type1: 'readwrite-storage-texture', + }, + { + _usageOK: false, + type0: 'readonly-storage-texture', + type1: 'writeonly-storage-texture', + }, + { + _usageOK: false, + type0: 'readonly-storage-texture', + type1: 'readwrite-storage-texture', + }, + { + _usageOK: false, + type0: 'writeonly-storage-texture', + type1: 'readwrite-storage-texture', + }, + { _usageOK: false, type0: 'readonly-storage-texture', type1: 'render-target' }, { _usageOK: false, type0: 'writeonly-storage-texture', type1: 'render-target' }, + { _usageOK: false, type0: 'readwrite-storage-texture', type1: 'render-target' }, { _usageOK: false, type0: 'render-target', type1: 'render-target' }, ] as const ) @@ -502,9 +558,13 @@ g.test('subresources_and_binding_types_combination_for_color') const bgls: GPUBindGroupLayout[] = []; // Create bind groups. Set bind groups in pass directly or set bind groups in bundle. - const storageTextureFormat0 = type0 === 'sampled-texture' ? undefined : 'rgba8unorm'; + const storageTextureFormat0 = type0 === 'sampled-texture' ? undefined : 'r32float'; + const sampleType0 = type0 === 'sampled-texture' ? 'unfilterable-float' : undefined; - const bgl0 = t.createBindGroupLayout(0, type0, dimension0, { format: storageTextureFormat0 }); + const bgl0 = t.createBindGroupLayout(0, type0, dimension0, { + format: storageTextureFormat0, + sampleType: sampleType0, + }); const bindGroup0 = t.device.createBindGroup({ layout: bgl0, entries: [{ binding: 0, resource: view0 }], @@ -518,10 +578,11 @@ g.test('subresources_and_binding_types_combination_for_color') pass.setBindGroup(0, bindGroup0); } if (type1 !== 'render-target') { - const storageTextureFormat1 = type1 === 'sampled-texture' ? undefined : 'rgba8unorm'; - + const storageTextureFormat1 = type1 === 'sampled-texture' ? undefined : 'r32float'; + const sampleType1 = type1 === 'sampled-texture' ? 'unfilterable-float' : undefined; const bgl1 = t.createBindGroupLayout(1, type1, dimension1, { format: storageTextureFormat1, + sampleType: sampleType1, }); const bindGroup1 = t.device.createBindGroup({ layout: bgl1, @@ -789,9 +850,21 @@ g.test('shader_stages_and_visibility,storage_write') GPUConst.ShaderStage.COMPUTE, ]) .combine('writeVisibility', [0, GPUConst.ShaderStage.FRAGMENT, GPUConst.ShaderStage.COMPUTE]) + .combine('readEntry', [ + { texture: { sampleType: 'unfilterable-float' } }, + { storageTexture: { access: 'read-only', format: 'r32float' } }, + ] as const) + .combine('storageWriteAccess', ['write-only', 'read-write'] as const) ) .fn(t => { - const { compute, readVisibility, writeVisibility, secondUseConflicts } = t.params; + const { + compute, + readEntry, + storageWriteAccess, + readVisibility, + writeVisibility, + secondUseConflicts, + } = t.params; const usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING; const view = t.createTexture({ usage }).createView(); @@ -799,11 +872,11 @@ g.test('shader_stages_and_visibility,storage_write') const bgl = t.device.createBindGroupLayout({ entries: [ - { binding: 0, visibility: readVisibility, texture: {} }, + { binding: 0, visibility: readVisibility, ...readEntry }, { binding: 1, visibility: writeVisibility, - storageTexture: { access: 'write-only', format: 'rgba8unorm' }, + storageTexture: { access: storageWriteAccess, format: 'r32float' }, }, ], }); @@ -858,19 +931,23 @@ g.test('shader_stages_and_visibility,attachment_write') GPUConst.ShaderStage.FRAGMENT, GPUConst.ShaderStage.COMPUTE, ]) + .combine('readEntry', [ + { texture: { sampleType: 'unfilterable-float' } }, + { storageTexture: { access: 'read-only', format: 'r32float' } }, + ] as const) ) .fn(t => { - const { readVisibility, secondUseConflicts } = t.params; + const { readVisibility, readEntry, secondUseConflicts } = t.params; - // writeonly-storage-texture binding type is not supported in vertex stage. So, this test - // uses writeonly-storage-texture binding as writable binding upon the same subresource if - // vertex stage is not included. Otherwise, it uses output attachment instead. - const usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT; + const usage = + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.RENDER_ATTACHMENT | + GPUTextureUsage.STORAGE_BINDING; const view = t.createTexture({ usage }).createView(); const view2 = secondUseConflicts ? view : t.createTexture({ usage }).createView(); const bgl = t.device.createBindGroupLayout({ - entries: [{ binding: 0, visibility: readVisibility, texture: {} }], + entries: [{ binding: 0, visibility: readVisibility, ...readEntry }], }); const bindGroup = t.device.createBindGroup({ layout: bgl, @@ -905,8 +982,10 @@ g.test('replaced_binding') .combine('compute', [false, true]) .combine('callDrawOrDispatch', [false, true]) .combine('entry', [ - { texture: {} }, - { storageTexture: { access: 'write-only', format: 'rgba8unorm' } }, + { texture: { sampleType: 'unfilterable-float' } }, + { storageTexture: { access: 'read-only', format: 'r32float' } }, + { storageTexture: { access: 'write-only', format: 'r32float' } }, + { storageTexture: { access: 'read-write', format: 'r32float' } }, ] as const) ) .fn(t => { @@ -919,7 +998,11 @@ g.test('replaced_binding') // Create bindGroup0. It has two bindings. These two bindings use different views/subresources. const bglEntries0: GPUBindGroupLayoutEntry[] = [ - { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} }, + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + texture: { sampleType: 'unfilterable-float' }, + }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, @@ -937,7 +1020,9 @@ g.test('replaced_binding') // Create bindGroup1. It has one binding, which use the same view/subresource of a binding in // bindGroup0. So it may or may not conflicts with that binding in bindGroup0. - const bindGroup1 = t.createBindGroup(0, sampledStorageView, 'sampled-texture', '2d', undefined); + const bindGroup1 = t.createBindGroup(0, sampledStorageView, 'sampled-texture', '2d', { + sampleType: 'unfilterable-float', + }); const encoder = t.device.createCommandEncoder(); const pass = compute @@ -948,7 +1033,9 @@ g.test('replaced_binding') // But bindings in bindGroup0 should be validated too. pass.setBindGroup(0, bindGroup0); if (callDrawOrDispatch) { - const pipeline = compute ? t.createNoOpComputePipeline() : t.createNoOpRenderPipeline(); + const pipeline = compute + ? t.createNoOpComputePipeline() + : t.createNoOpRenderPipeline('auto', 'r32float'); t.setPipeline(pass, pipeline); t.issueDrawOrDispatch(pass); } @@ -958,7 +1045,9 @@ g.test('replaced_binding') // MAINTENANCE_TODO: If the Compatible Usage List // (https://gpuweb.github.io/gpuweb/#compatible-usage-list) gets programmatically defined in // capability_info, use it here, instead of this logic, for clarity. - let success = entry.storageTexture?.access !== 'write-only'; + let success = + entry.storageTexture?.access !== 'write-only' && + entry.storageTexture?.access !== 'read-write'; // Replaced bindings should not be validated in compute pass, because validation only occurs // inside dispatchWorkgroups() which only looks at the current resource usages. success ||= compute; @@ -988,7 +1077,9 @@ g.test('bindings_in_bundle') case 'multisampled-texture': case 'sampled-texture': return 'TEXTURE_BINDING' as const; + case 'readonly-storage-texture': case 'writeonly-storage-texture': + case 'readwrite-storage-texture': return 'STORAGE_BINDING' as const; case 'render-target': return 'RENDER_ATTACHMENT' as const; @@ -1040,17 +1131,17 @@ g.test('bindings_in_bundle') const bindGroups: GPUBindGroup[] = []; if (type0 !== 'render-target') { - const binding0TexFormat = type0 === 'sampled-texture' ? undefined : 'rgba8unorm'; + const binding0TexFormat = type0 === 'sampled-texture' ? undefined : 'r32float'; bindGroups[0] = t.createBindGroup(0, view, type0, '2d', { format: binding0TexFormat, - sampleType: _sampleCount && 'unfilterable-float', + sampleType: 'unfilterable-float', }); } if (type1 !== 'render-target') { - const binding1TexFormat = type1 === 'sampled-texture' ? undefined : 'rgba8unorm'; + const binding1TexFormat = type1 === 'sampled-texture' ? undefined : 'r32float'; bindGroups[1] = t.createBindGroup(1, view, type1, '2d', { format: binding1TexFormat, - sampleType: _sampleCount && 'unfilterable-float', + sampleType: 'unfilterable-float', }); } @@ -1069,7 +1160,7 @@ g.test('bindings_in_bundle') // 'render-target'). if (bindingsInBundle[i]) { const bundleEncoder = t.device.createRenderBundleEncoder({ - colorFormats: ['rgba8unorm'], + colorFormats: ['r32float'], }); bundleEncoder.setBindGroup(i, bindGroups[i]); const bundleInPass = bundleEncoder.finish(); @@ -1085,6 +1176,7 @@ g.test('bindings_in_bundle') switch (t) { case 'sampled-texture': case 'multisampled-texture': + case 'readonly-storage-texture': return true; default: return false; @@ -1096,7 +1188,8 @@ g.test('bindings_in_bundle') success = true; } - if (type0 === 'writeonly-storage-texture' && type1 === 'writeonly-storage-texture') { + // Writable storage textures (write-only and read-write storage textures) cannot be aliased. + if (type0 === type1) { success = true; } @@ -1117,6 +1210,8 @@ g.test('unused_bindings_in_pipeline') .params(u => u .combine('compute', [false, true]) + .combine('readOnlyUsage', ['sampled-texture', 'readonly-storage-texture'] as const) + .combine('writableUsage', ['writeonly-storage-texture', 'readwrite-storage-texture'] as const) .combine('useBindGroup0', [false, true]) .combine('useBindGroup1', [false, true]) .combine('setBindGroupsOrder', ['common', 'reversed'] as const) @@ -1126,41 +1221,49 @@ g.test('unused_bindings_in_pipeline') .fn(t => { const { compute, + readOnlyUsage, + writableUsage, useBindGroup0, useBindGroup1, setBindGroupsOrder, setPipeline, callDrawOrDispatch, } = t.params; + if (writableUsage === 'readwrite-storage-texture') { + t.requireLanguageFeatureOrSkipTestCase('readonly_and_readwrite_storage_textures'); + } + const view = t .createTexture({ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING }) .createView(); - const bindGroup0 = t.createBindGroup(0, view, 'sampled-texture', '2d', { - format: 'rgba8unorm', + const bindGroup0 = t.createBindGroup(0, view, readOnlyUsage, '2d', { + sampleType: 'unfilterable-float', + format: 'r32float', }); - const bindGroup1 = t.createBindGroup(0, view, 'writeonly-storage-texture', '2d', { - format: 'rgba8unorm', + const bindGroup1 = t.createBindGroup(0, view, writableUsage, '2d', { + format: 'r32float', }); + const writeAccess = writableUsage === 'writeonly-storage-texture' ? 'write' : 'read_write'; const wgslVertex = `@vertex fn main() -> @builtin(position) vec4 { return vec4(); }`; const wgslFragment = pp` ${pp._if(useBindGroup0)} - @group(0) @binding(0) var image0 : texture_storage_2d; + @group(0) @binding(0) var image0 : texture_storage_2d; ${pp._endif} ${pp._if(useBindGroup1)} - @group(1) @binding(0) var image1 : texture_storage_2d; + @group(1) @binding(0) var image1 : texture_storage_2d; ${pp._endif} @fragment fn main() {} `; const wgslCompute = pp` ${pp._if(useBindGroup0)} - @group(0) @binding(0) var image0 : texture_storage_2d; + @group(0) @binding(0) var image0 : texture_storage_2d; ${pp._endif} ${pp._if(useBindGroup1)} - @group(1) @binding(0) var image1 : texture_storage_2d; + @group(1) @binding(0) var image1 : texture_storage_2d; ${pp._endif} @compute @workgroup_size(1) fn main() {} `; @@ -1188,7 +1291,7 @@ g.test('unused_bindings_in_pipeline') code: wgslFragment, }), entryPoint: 'main', - targets: [{ format: 'rgba8unorm', writeMask: 0 }], + targets: [{ format: 'r32float', writeMask: 0 }], }, primitive: { topology: 'triangle-list' }, }); @@ -1244,14 +1347,28 @@ g.test('scope,dispatch') .params(u => u .combine('dispatch', ['none', 'direct', 'indirect']) + .expandWithParams( + p => + [ + { usage1: 'sampled-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'sampled-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'writeonly-storage-texture', usage2: 'readwrite-storage-texture' }, + ] as const + ) .beginSubcases() .expand('setBindGroup0', p => (p.dispatch ? [true] : [false, true])) .expand('setBindGroup1', p => (p.dispatch ? [true] : [false, true])) ) .fn(t => { - const { dispatch, setBindGroup0, setBindGroup1 } = t.params; + const { dispatch, usage1, usage2, setBindGroup0, setBindGroup1 } = t.params; - const { bindGroup0, bindGroup1, encoder, pass, pipeline } = t.testValidationScope(true); + const { bindGroup0, bindGroup1, encoder, pass, pipeline } = t.testValidationScope( + true, + usage1, + usage2 + ); assert(pass instanceof GPUComputePassEncoder); t.setPipeline(pass, pipeline); @@ -1291,11 +1408,21 @@ g.test('scope,basic,render') u // .combine('setBindGroup0', [false, true]) .combine('setBindGroup1', [false, true]) + .expandWithParams( + p => + [ + { usage1: 'sampled-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'sampled-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'writeonly-storage-texture', usage2: 'readwrite-storage-texture' }, + ] as const + ) ) .fn(t => { - const { setBindGroup0, setBindGroup1 } = t.params; + const { setBindGroup0, setBindGroup1, usage1, usage2 } = t.params; - const { bindGroup0, bindGroup1, encoder, pass } = t.testValidationScope(false); + const { bindGroup0, bindGroup1, encoder, pass } = t.testValidationScope(false, usage1, usage2); assert(pass instanceof GPURenderPassEncoder); if (setBindGroup0) pass.setBindGroup(0, bindGroup0); @@ -1315,11 +1442,22 @@ g.test('scope,pass_boundary,compute') boundary in between. This should always be valid. ` ) - .paramsSubcasesOnly(u => u.combine('splitPass', [false, true])) + .paramsSubcasesOnly(u => + u.combine('splitPass', [false, true]).expandWithParams( + p => + [ + { usage1: 'sampled-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'sampled-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'writeonly-storage-texture', usage2: 'readwrite-storage-texture' }, + ] as const + ) + ) .fn(t => { - const { splitPass } = t.params; + const { splitPass, usage1, usage2 } = t.params; - const { bindGroupLayouts, bindGroups } = t.makeConflictingBindGroups(); + const { bindGroupLayouts, bindGroups } = t.makeTwoBindGroupsWithOneTextureView(usage1, usage2); const encoder = t.device.createCommandEncoder(); @@ -1362,23 +1500,35 @@ g.test('scope,pass_boundary,render') u // .combine('splitPass', [false, true]) .combine('draw', [false, true]) + .expandWithParams( + p => + [ + { usage1: 'sampled-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'sampled-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'writeonly-storage-texture' }, + { usage1: 'readonly-storage-texture', usage2: 'readwrite-storage-texture' }, + { usage1: 'writeonly-storage-texture', usage2: 'readwrite-storage-texture' }, + ] as const + ) ) .fn(t => { - const { splitPass, draw } = t.params; + const { splitPass, draw, usage1, usage2 } = t.params; - const { bindGroupLayouts, bindGroups } = t.makeConflictingBindGroups(); + const { bindGroupLayouts, bindGroups } = t.makeTwoBindGroupsWithOneTextureView(usage1, usage2); const encoder = t.device.createCommandEncoder(); const pipelineUsingBG0 = t.createNoOpRenderPipeline( t.device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayouts[0]], - }) + }), + 'r32float' ); const pipelineUsingBG1 = t.createNoOpRenderPipeline( t.device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayouts[1]], - }) + }), + 'r32float' ); const attachment = t.createTexture().createView(); diff --git a/src/webgpu/api/validation/validation_test.ts b/src/webgpu/api/validation/validation_test.ts index 7ee5b9f7c17e..1be0866e1d60 100644 --- a/src/webgpu/api/validation/validation_test.ts +++ b/src/webgpu/api/validation/validation_test.ts @@ -317,7 +317,8 @@ export class ValidationTest extends GPUTest { /** Return a GPURenderPipeline with default options and no-op vertex and fragment shaders. */ createNoOpRenderPipeline( - layout: GPUPipelineLayout | GPUAutoLayoutMode = 'auto' + layout: GPUPipelineLayout | GPUAutoLayoutMode = 'auto', + colorFormat: GPUTextureFormat = 'rgba8unorm' ): GPURenderPipeline { return this.device.createRenderPipeline({ layout, @@ -332,7 +333,7 @@ export class ValidationTest extends GPUTest { code: this.getNoOpShaderCode('FRAGMENT'), }), entryPoint: 'main', - targets: [{ format: 'rgba8unorm', writeMask: 0 }], + targets: [{ format: colorFormat, writeMask: 0 }], }, primitive: { topology: 'triangle-list' }, });