diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts | 1376 |
1 files changed, 1376 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts new file mode 100644 index 0000000000..26a2a66db6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_pass_encoder.spec.ts @@ -0,0 +1,1376 @@ +export const description = ` +Texture Usages Validation Tests in Render Pass and Compute Pass. +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { pp } from '../../../../../common/util/preprocessor.js'; +import { assert } from '../../../../../common/util/util.js'; +import { + kDepthStencilFormats, + kDepthStencilFormatResolvedAspect, + kTextureFormatInfo, +} from '../../../../capability_info.js'; +import { GPUConst } from '../../../../constants.js'; +import { ValidationTest } from '../../validation_test.js'; + +type TextureBindingType = 'sampled-texture' | 'multisampled-texture' | 'writeonly-storage-texture'; +const kTextureBindingTypes = [ + 'sampled-texture', + 'multisampled-texture', + 'writeonly-storage-texture', +] as const; + +const SIZE = 32; +class TextureUsageTracking extends ValidationTest { + createTexture( + options: { + width?: number; + height?: number; + arrayLayerCount?: number; + mipLevelCount?: number; + sampleCount?: number; + format?: GPUTextureFormat; + usage?: GPUTextureUsageFlags; + } = {} + ): GPUTexture { + const { + width = SIZE, + height = SIZE, + arrayLayerCount = 1, + mipLevelCount = 1, + sampleCount = 1, + format = 'rgba8unorm', + usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, + } = options; + + return this.device.createTexture({ + size: { width, height, depthOrArrayLayers: arrayLayerCount }, + mipLevelCount, + sampleCount, + dimension: '2d', + format, + usage, + }); + } + + createBindGroupLayout( + binding: number, + bindingType: TextureBindingType, + viewDimension: GPUTextureViewDimension, + options: { + format?: GPUTextureFormat; + sampleType?: GPUTextureSampleType; + } = {} + ): GPUBindGroupLayout { + const { sampleType, format } = options; + let entry: Omit<GPUBindGroupLayoutEntry, 'binding' | 'visibility'>; + switch (bindingType) { + case 'sampled-texture': + entry = { texture: { viewDimension, sampleType } }; + break; + case 'multisampled-texture': + entry = { texture: { viewDimension, multisampled: true, sampleType } }; + break; + case 'writeonly-storage-texture': + assert(format !== undefined); + entry = { storageTexture: { access: 'write-only', format, viewDimension } }; + break; + } + + return this.device.createBindGroupLayout({ + entries: [ + { binding, visibility: GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT, ...entry }, + ], + }); + } + + createBindGroup( + binding: number, + resource: GPUTextureView, + bindingType: TextureBindingType, + viewDimension: GPUTextureViewDimension, + options: { + format?: GPUTextureFormat; + sampleType?: GPUTextureSampleType; + } = {} + ): GPUBindGroup { + return this.device.createBindGroup({ + entries: [{ binding, resource }], + layout: this.createBindGroupLayout(binding, bindingType, viewDimension, options), + }); + } + + createAndExecuteBundle( + binding: number, + bindGroup: GPUBindGroup, + pass: GPURenderPassEncoder, + depthStencilFormat?: GPUTextureFormat + ) { + const bundleEncoder = this.device.createRenderBundleEncoder({ + colorFormats: ['rgba8unorm'], + depthStencilFormat, + }); + bundleEncoder.setBindGroup(binding, bindGroup); + const bundle = bundleEncoder.finish(); + pass.executeBundles([bundle]); + } + + beginSimpleRenderPass(encoder: GPUCommandEncoder, view: GPUTextureView): GPURenderPassEncoder { + return encoder.beginRenderPass({ + colorAttachments: [ + { + view, + clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + } + + /** + * Create two bind groups. Resource usages conflict between these two bind groups. But resource + * usage inside each bind group doesn't conflict. + */ + makeConflictingBindGroups() { + 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' }), + ]; + return { + bindGroupLayouts, + bindGroups: [ + this.device.createBindGroup({ + layout: bindGroupLayouts[0], + entries: [{ binding: 0, resource: view }], + }), + this.device.createBindGroup({ + layout: bindGroupLayouts[1], + entries: [{ binding: 0, resource: view }], + }), + ], + }; + } + + testValidationScope( + compute: boolean + ): { + bindGroup0: GPUBindGroup; + bindGroup1: GPUBindGroup; + encoder: GPUCommandEncoder; + pass: GPURenderPassEncoder | GPUComputePassEncoder; + pipeline: GPURenderPipeline | GPUComputePipeline; + } { + const { bindGroupLayouts, bindGroups } = this.makeConflictingBindGroups(); + + const encoder = this.device.createCommandEncoder(); + const pass = compute + ? encoder.beginComputePass() + : this.beginSimpleRenderPass(encoder, this.createTexture().createView()); + + // Create pipeline. Note that bindings unused in pipeline should be validated too. + const pipelineLayout = this.device.createPipelineLayout({ + bindGroupLayouts, + }); + const pipeline = compute + ? this.createNoOpComputePipeline(pipelineLayout) + : this.createNoOpRenderPipeline(pipelineLayout); + return { + bindGroup0: bindGroups[0], + bindGroup1: bindGroups[1], + encoder, + pass, + pipeline, + }; + } + + setPipeline( + pass: GPURenderPassEncoder | GPUComputePassEncoder, + pipeline: GPURenderPipeline | GPUComputePipeline + ) { + if (pass instanceof GPUComputePassEncoder) { + pass.setPipeline(pipeline as GPUComputePipeline); + } else { + pass.setPipeline(pipeline as GPURenderPipeline); + } + } + + issueDrawOrDispatch(pass: GPURenderPassEncoder | GPUComputePassEncoder) { + if (pass instanceof GPUComputePassEncoder) { + pass.dispatchWorkgroups(1); + } else { + pass.draw(3, 1, 0, 0); + } + } + + setComputePipelineAndCallDispatch(pass: GPUComputePassEncoder, layout?: GPUPipelineLayout) { + const pipeline = this.createNoOpComputePipeline(layout); + pass.setPipeline(pipeline); + pass.dispatchWorkgroups(1); + } +} + +export const g = makeTestGroup(TextureUsageTracking); + +const BASE_LEVEL = 1; +const TOTAL_LEVELS = 6; +const BASE_LAYER = 1; +const TOTAL_LAYERS = 6; +const SLICE_COUNT = 2; + +g.test('subresources_and_binding_types_combination_for_color') + .desc( + ` + Test the resource usage rules by using two views of the same GPUTexture in a usage scope. Tests + various combinations of {sampled, storage, render target} usages, mip-level ranges, and + array-layer ranges, in {compute pass, render pass, render pass via bundle}. + - Error if a subresource (level/layer) is used as read+write or write+write in the scope, + except when both usages are writeonly-storage-texture which is allowed. + ` + ) + .params(u => + u + .combine('compute', [false, true]) + .combineWithParams([ + { _usageOK: true, type0: 'sampled-texture', type1: 'sampled-texture' }, + { _usageOK: false, type0: 'sampled-texture', type1: 'writeonly-storage-texture' }, + { _usageOK: false, type0: 'sampled-texture', type1: 'render-target' }, + // Race condition upon multiple writable storage texture is valid. + { _usageOK: true, type0: 'writeonly-storage-texture', type1: 'writeonly-storage-texture' }, + { _usageOK: false, type0: 'writeonly-storage-texture', type1: 'render-target' }, + { _usageOK: false, type0: 'render-target', type1: 'render-target' }, + ] as const) + .beginSubcases() + .combine('binding0InBundle', [false, true]) + .combine('binding1InBundle', [false, true]) + .unless( + p => + // We can't set 'render-target' in bundle, so we need to exclude it from bundle. + (p.binding0InBundle && p.type0 === 'render-target') || + (p.binding1InBundle && p.type1 === 'render-target') || + // We can't set 'render-target' or bundle in compute. + (p.compute && + (p.binding0InBundle || + p.binding1InBundle || + p.type0 === 'render-target' || + p.type1 === 'render-target')) + ) + .combineWithParams([ + // Two texture usages are binding to the same texture subresource. + { + levelCount0: 1, + layerCount0: 1, + baseLevel1: BASE_LEVEL, + levelCount1: 1, + baseLayer1: BASE_LAYER, + layerCount1: 1, + _resourceSuccess: false, + }, + + // Two texture usages are binding to different mip levels of the same texture. + { + levelCount0: 1, + layerCount0: 1, + baseLevel1: BASE_LEVEL + 1, + levelCount1: 1, + baseLayer1: BASE_LAYER, + layerCount1: 1, + _resourceSuccess: true, + }, + + // Two texture usages are binding to different array layers of the same texture. + { + levelCount0: 1, + layerCount0: 1, + baseLevel1: BASE_LEVEL, + levelCount1: 1, + baseLayer1: BASE_LAYER + 1, + layerCount1: 1, + _resourceSuccess: true, + }, + + // The second texture usage contains the whole mip chain where the first texture usage is + // using. + { + levelCount0: 1, + layerCount0: 1, + baseLevel1: 0, + levelCount1: TOTAL_LEVELS, + baseLayer1: BASE_LAYER, + layerCount1: 1, + _resourceSuccess: false, + }, + + // The second texture usage contains all layers where the first texture usage is using. + { + levelCount0: 1, + layerCount0: 1, + baseLevel1: BASE_LEVEL, + levelCount1: 1, + baseLayer1: 0, + layerCount1: TOTAL_LAYERS, + _resourceSuccess: false, + }, + + // The second texture usage contains all subresources where the first texture usage is + // using. + { + levelCount0: 1, + layerCount0: 1, + baseLevel1: 0, + levelCount1: TOTAL_LEVELS, + baseLayer1: 0, + layerCount1: TOTAL_LAYERS, + _resourceSuccess: false, + }, + + // Both of the two usages access a few mip levels on the same layer but they don't overlap. + { + levelCount0: SLICE_COUNT, + layerCount0: 1, + baseLevel1: BASE_LEVEL + SLICE_COUNT, + levelCount1: 3, + baseLayer1: BASE_LAYER, + layerCount1: 1, + _resourceSuccess: true, + }, + + // Both of the two usages access a few mip levels on the same layer and they overlap. + { + levelCount0: SLICE_COUNT, + layerCount0: 1, + baseLevel1: BASE_LEVEL + SLICE_COUNT - 1, + levelCount1: 3, + baseLayer1: BASE_LAYER, + layerCount1: 1, + _resourceSuccess: false, + }, + + // Both of the two usages access a few array layers on the same level but they don't + // overlap. + { + levelCount0: 1, + layerCount0: SLICE_COUNT, + baseLevel1: BASE_LEVEL, + levelCount1: 1, + baseLayer1: BASE_LAYER + SLICE_COUNT, + layerCount1: 3, + _resourceSuccess: true, + }, + + // Both of the two usages access a few array layers on the same level and they overlap. + { + levelCount0: 1, + layerCount0: SLICE_COUNT, + baseLevel1: BASE_LEVEL, + levelCount1: 1, + baseLayer1: BASE_LAYER + SLICE_COUNT - 1, + layerCount1: 3, + _resourceSuccess: false, + }, + + // Both of the two usages access a few array layers and mip levels but they don't overlap. + { + levelCount0: SLICE_COUNT, + layerCount0: SLICE_COUNT, + baseLevel1: BASE_LEVEL + SLICE_COUNT, + levelCount1: 3, + baseLayer1: BASE_LAYER + SLICE_COUNT, + layerCount1: 3, + _resourceSuccess: true, + }, + + // Both of the two usages access a few array layers and mip levels and they overlap. + { + levelCount0: SLICE_COUNT, + layerCount0: SLICE_COUNT, + baseLevel1: BASE_LEVEL + SLICE_COUNT - 1, + levelCount1: 3, + baseLayer1: BASE_LAYER + SLICE_COUNT - 1, + layerCount1: 3, + _resourceSuccess: false, + }, + ]) + .unless( + p => + // Every color attachment or storage texture can use only one single subresource. + (p.type0 !== 'sampled-texture' && (p.levelCount0 !== 1 || p.layerCount0 !== 1)) || + (p.type1 !== 'sampled-texture' && (p.levelCount1 !== 1 || p.layerCount1 !== 1)) || + // All color attachments' size should be the same. + (p.type0 === 'render-target' && + p.type1 === 'render-target' && + p.baseLevel1 !== BASE_LEVEL) + ) + ) + .fn(t => { + const { + compute, + binding0InBundle, + binding1InBundle, + levelCount0, + layerCount0, + baseLevel1, + baseLayer1, + levelCount1, + layerCount1, + type0, + type1, + _usageOK, + _resourceSuccess, + } = t.params; + + const texture = t.createTexture({ + arrayLayerCount: TOTAL_LAYERS, + mipLevelCount: TOTAL_LEVELS, + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.STORAGE_BINDING | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const dimension0 = layerCount0 !== 1 ? '2d-array' : '2d'; + const view0 = texture.createView({ + dimension: dimension0, + baseMipLevel: BASE_LEVEL, + mipLevelCount: levelCount0, + baseArrayLayer: BASE_LAYER, + arrayLayerCount: layerCount0, + }); + + const dimension1 = layerCount1 !== 1 ? '2d-array' : '2d'; + const view1 = texture.createView({ + dimension: dimension1, + baseMipLevel: baseLevel1, + mipLevelCount: levelCount1, + baseArrayLayer: baseLayer1, + arrayLayerCount: layerCount1, + }); + + const encoder = t.device.createCommandEncoder(); + if (type0 === 'render-target') { + // Note that type1 is 'render-target' too. So we don't need to create bindings. + assert(type1 === 'render-target'); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: view0, + clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + { + view: view1, + clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + pass.end(); + } else { + const pass = compute + ? encoder.beginComputePass() + : t.beginSimpleRenderPass( + encoder, + type1 === 'render-target' ? view1 : t.createTexture().createView() + ); + + 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 bgl0 = t.createBindGroupLayout(0, type0, dimension0, { format: storageTextureFormat0 }); + const bindGroup0 = t.device.createBindGroup({ + layout: bgl0, + entries: [{ binding: 0, resource: view0 }], + }); + bgls.push(bgl0); + + if (binding0InBundle) { + assert(pass instanceof GPURenderPassEncoder); + t.createAndExecuteBundle(0, bindGroup0, pass); + } else { + pass.setBindGroup(0, bindGroup0); + } + if (type1 !== 'render-target') { + const storageTextureFormat1 = type1 === 'sampled-texture' ? undefined : 'rgba8unorm'; + + const bgl1 = t.createBindGroupLayout(1, type1, dimension1, { + format: storageTextureFormat1, + }); + const bindGroup1 = t.device.createBindGroup({ + layout: bgl1, + entries: [{ binding: 1, resource: view1 }], + }); + bgls.push(bgl1); + + if (binding1InBundle) { + assert(pass instanceof GPURenderPassEncoder); + t.createAndExecuteBundle(1, bindGroup1, pass); + } else { + pass.setBindGroup(1, bindGroup1); + } + } + if (compute) { + t.setComputePipelineAndCallDispatch( + pass as GPUComputePassEncoder, + t.device.createPipelineLayout({ bindGroupLayouts: bgls }) + ); + } + pass.end(); + } + + const success = _resourceSuccess || _usageOK; + t.expectValidationError(() => { + encoder.finish(); + }, !success); + }); + +g.test('subresources_and_binding_types_combination_for_aspect') + .desc( + ` + Test the resource usage rules by using two views of the same GPUTexture in a usage scope. Tests + various combinations of {sampled, render target} usages, {all, depth-only, stencil-only} aspects + that overlap a given subresources in {compute pass, render pass, render pass via bundle}. + - Error if a subresource (level/layer/aspect) is used as read+write or write+write in the + scope. + ` + ) + .params(u => + u + .combine('compute', [false, true]) + .combine('binding0InBundle', [false, true]) + .combine('binding1InBundle', [false, true]) + .combine('format', kDepthStencilFormats) + .beginSubcases() + .combineWithParams([ + { + baseLevel: BASE_LEVEL, + baseLayer: BASE_LAYER, + _resourceSuccess: false, + }, + { + baseLevel: BASE_LEVEL + 1, + baseLayer: BASE_LAYER, + _resourceSuccess: true, + }, + { + baseLevel: BASE_LEVEL, + baseLayer: BASE_LAYER + 1, + _resourceSuccess: true, + }, + ]) + .combine('aspect0', ['all', 'depth-only', 'stencil-only'] as const) + .combine('aspect1', ['all', 'depth-only', 'stencil-only'] as const) + .unless( + p => + (p.aspect0 === 'stencil-only' && !kTextureFormatInfo[p.format].stencil) || + (p.aspect1 === 'stencil-only' && !kTextureFormatInfo[p.format].stencil) + ) + .unless( + p => + (p.aspect0 === 'depth-only' && !kTextureFormatInfo[p.format].depth) || + (p.aspect1 === 'depth-only' && !kTextureFormatInfo[p.format].depth) + ) + .combineWithParams([ + { + type0: 'sampled-texture', + type1: 'sampled-texture', + _usageSuccess: true, + }, + { + type0: 'sampled-texture', + type1: 'render-target', + _usageSuccess: false, + }, + ] as const) + .unless( + // Can't sample a multiplanar texture without selecting an aspect. + p => + kTextureFormatInfo[p.format].depth && + kTextureFormatInfo[p.format].stencil && + ((p.aspect0 === 'all' && p.type0 === 'sampled-texture') || + (p.aspect1 === 'all' && p.type1 === 'sampled-texture')) + ) + .unless( + p => + // We can't set 'render-target' in bundle, so we need to exclude it from bundle. + p.binding1InBundle && p.type1 === 'render-target' + ) + .unless( + p => + // We can't set 'render-target' or bundle in compute. Note that type0 is definitely not + // 'render-target' + p.compute && (p.binding0InBundle || p.binding1InBundle || p.type1 === 'render-target') + ) + .unless( + p => + // Depth-stencil attachment views must encompass all aspects of the texture. Invalid + // cases are for depth-stencil textures when the aspect is not 'all'. + p.type1 === 'render-target' && + kTextureFormatInfo[p.format].depth && + kTextureFormatInfo[p.format].stencil && + p.aspect1 !== 'all' + ) + ) + .beforeAllSubcases(t => { + const { format } = t.params; + t.selectDeviceOrSkipTestCase(kTextureFormatInfo[format].feature); + }) + .fn(t => { + const { + compute, + binding0InBundle, + binding1InBundle, + format, + baseLevel, + baseLayer, + aspect0, + aspect1, + type0, + type1, + _resourceSuccess, + _usageSuccess, + } = t.params; + + const texture = t.createTexture({ + arrayLayerCount: TOTAL_LAYERS, + mipLevelCount: TOTAL_LEVELS, + format, + }); + + const view0 = texture.createView({ + dimension: '2d', + baseMipLevel: BASE_LEVEL, + mipLevelCount: 1, + baseArrayLayer: BASE_LAYER, + arrayLayerCount: 1, + aspect: aspect0, + }); + + const view1 = texture.createView({ + dimension: '2d', + baseMipLevel: baseLevel, + mipLevelCount: 1, + baseArrayLayer: baseLayer, + arrayLayerCount: 1, + aspect: aspect1, + }); + const view1ResolvedFormat = kDepthStencilFormatResolvedAspect[format][aspect1]!; + const view1HasDepth = kTextureFormatInfo[view1ResolvedFormat].depth; + const view1HasStencil = kTextureFormatInfo[view1ResolvedFormat].stencil; + + const encoder = t.device.createCommandEncoder(); + // Color attachment's size should match depth/stencil attachment's size. Note that if + // type1 !== 'render-target' then there's no depthStencilAttachment to match anyway. + const depthStencilFormat = type1 === 'render-target' ? view1ResolvedFormat : undefined; + + const size = SIZE >> baseLevel; + const pass = compute + ? encoder.beginComputePass() + : encoder.beginRenderPass({ + colorAttachments: [ + { + view: t.createTexture({ width: size, height: size }).createView(), + clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: depthStencilFormat + ? { + view: view1, + depthStoreOp: view1HasDepth ? 'discard' : undefined, + depthLoadOp: view1HasDepth ? 'load' : undefined, + stencilStoreOp: view1HasStencil ? 'discard' : undefined, + stencilLoadOp: view1HasStencil ? 'load' : undefined, + } + : undefined, + }); + + const aspectSampleType = (format: GPUTextureFormat, aspect: typeof aspect0) => { + switch (aspect) { + case 'depth-only': + return 'depth'; + case 'stencil-only': + return 'uint'; + case 'all': + assert(kTextureFormatInfo[format].depth !== kTextureFormatInfo[format].stencil); + if (kTextureFormatInfo[format].stencil) { + return 'uint'; + } + return 'depth'; + } + }; + + // Create bind groups. Set bind groups in pass directly or set bind groups in bundle. + const bindGroup0 = t.createBindGroup(0, view0, type0, '2d', { + sampleType: type0 === 'sampled-texture' ? aspectSampleType(format, aspect0) : undefined, + }); + if (binding0InBundle) { + assert(pass instanceof GPURenderPassEncoder); + t.createAndExecuteBundle(0, bindGroup0, pass, depthStencilFormat); + } else { + pass.setBindGroup(0, bindGroup0); + } + if (type1 !== 'render-target') { + const bindGroup1 = t.createBindGroup(1, view1, type1, '2d', { + sampleType: type1 === 'sampled-texture' ? aspectSampleType(format, aspect1) : undefined, + }); + if (binding1InBundle) { + assert(pass instanceof GPURenderPassEncoder); + t.createAndExecuteBundle(1, bindGroup1, pass, depthStencilFormat); + } else { + pass.setBindGroup(1, bindGroup1); + } + } + if (compute) t.setComputePipelineAndCallDispatch(pass as GPUComputePassEncoder); + pass.end(); + + const disjointAspects = + (aspect0 === 'depth-only' && aspect1 === 'stencil-only') || + (aspect0 === 'stencil-only' && aspect1 === 'depth-only'); + + // If subresources' mip/array slices has no overlap, or their binding types don't conflict, + // it will definitely success no matter what aspects they are binding to. + const success = disjointAspects || _resourceSuccess || _usageSuccess; + + t.expectValidationError(() => { + encoder.finish(); + }, !success); + }); + +g.test('shader_stages_and_visibility,storage_write') + .desc( + ` + Test that stage visibility doesn't affect resource usage validation. + - Use a texture as sampled, with 'readVisibility' {0,VERTEX,FRAGMENT,COMPUTE} + - Use a {same,different} texture as storage, with 'writeVisibility' {0,FRAGMENT,COMPUTE} + + There should be a validation error IFF the same texture was used. + ` + ) + .params(u => + u + .combine('compute', [false, true]) + .beginSubcases() + .combine('secondUseConflicts', [false, true]) + .combine('readVisibility', [ + 0, + GPUConst.ShaderStage.VERTEX, + GPUConst.ShaderStage.FRAGMENT, + GPUConst.ShaderStage.COMPUTE, + ]) + .combine('writeVisibility', [0, GPUConst.ShaderStage.FRAGMENT, GPUConst.ShaderStage.COMPUTE]) + ) + .fn(t => { + const { compute, readVisibility, writeVisibility, secondUseConflicts } = t.params; + + const usage = GPUTextureUsage.TEXTURE_BINDING | 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: {} }, + { + binding: 1, + visibility: writeVisibility, + storageTexture: { access: 'write-only', format: 'rgba8unorm' }, + }, + ], + }); + const bindGroup = t.device.createBindGroup({ + layout: bgl, + entries: [ + { binding: 0, resource: view }, + { binding: 1, resource: view2 }, + ], + }); + + const encoder = t.device.createCommandEncoder(); + if (compute) { + const pass = encoder.beginComputePass(); + pass.setBindGroup(0, bindGroup); + + t.setComputePipelineAndCallDispatch( + pass, + t.device.createPipelineLayout({ + bindGroupLayouts: [bgl], + }) + ); + pass.end(); + } else { + const pass = t.beginSimpleRenderPass(encoder, t.createTexture().createView()); + pass.setBindGroup(0, bindGroup); + pass.end(); + } + + t.expectValidationError(() => { + encoder.finish(); + }, secondUseConflicts); + }); + +g.test('shader_stages_and_visibility,attachment_write') + .desc( + ` + Test that stage visibility doesn't affect resource usage validation. + - Use a texture as sampled, with 'readVisibility' {0,VERTEX,FRAGMENT,COMPUTE} + - Use a {same,different} texture as a render pass attachment + + There should be a validation error IFF the same texture was used. + ` + ) + .params(u => + u + .beginSubcases() + .combine('secondUseConflicts', [false, true]) + .combine('readVisibility', [ + 0, + GPUConst.ShaderStage.VERTEX, + GPUConst.ShaderStage.FRAGMENT, + GPUConst.ShaderStage.COMPUTE, + ]) + ) + .fn(t => { + const { readVisibility, 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 view = t.createTexture({ usage }).createView(); + const view2 = secondUseConflicts ? view : t.createTexture({ usage }).createView(); + const bgl = t.device.createBindGroupLayout({ + entries: [{ binding: 0, visibility: readVisibility, texture: {} }], + }); + const bindGroup = t.device.createBindGroup({ + layout: bgl, + entries: [{ binding: 0, resource: view }], + }); + + const encoder = t.device.createCommandEncoder(); + const pass = t.beginSimpleRenderPass(encoder, view2); + pass.setBindGroup(0, bindGroup); + pass.end(); + + // Texture usages in bindings with invisible shader stages should be validated. Invisible shader + // stages include shader stage with visibility none, compute shader stage in render pass, and + // vertex/fragment shader stage in compute pass. + t.expectValidationError(() => { + encoder.finish(); + }, secondUseConflicts); + }); + +g.test('replaced_binding') + .desc( + ` + Test whether a binding that's been replaced by another setBindGroup call can still + cause validation to fail (with a write/write conflict). + - In render pass, all setBindGroup calls contribute to the validation even if they're + shadowed. + - In compute pass, only the bindings visible at dispatchWorkgroups() contribute to validation. + ` + ) + .params(u => + u + .combine('compute', [false, true]) + .combine('callDrawOrDispatch', [false, true]) + .combine('entry', [ + { texture: {} }, + { storageTexture: { access: 'write-only', format: 'rgba8unorm' } }, + ] as const) + ) + .fn(t => { + const { compute, callDrawOrDispatch, entry } = t.params; + + const sampledView = t.createTexture().createView(); + const sampledStorageView = t + .createTexture({ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING }) + .createView(); + + // Create bindGroup0. It has two bindings. These two bindings use different views/subresources. + const bglEntries0: GPUBindGroupLayoutEntry[] = [ + { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} }, + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + ...entry, + }, + ]; + const bgEntries0: GPUBindGroupEntry[] = [ + { binding: 0, resource: sampledView }, + { binding: 1, resource: sampledStorageView }, + ]; + const bindGroup0 = t.device.createBindGroup({ + entries: bgEntries0, + layout: t.device.createBindGroupLayout({ entries: bglEntries0 }), + }); + + // 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 encoder = t.device.createCommandEncoder(); + const pass = compute + ? encoder.beginComputePass() + : t.beginSimpleRenderPass(encoder, t.createTexture().createView()); + + // Set bindGroup0 and bindGroup1. bindGroup0 is replaced by bindGroup1 in the current pass. + // But bindings in bindGroup0 should be validated too. + pass.setBindGroup(0, bindGroup0); + if (callDrawOrDispatch) { + const pipeline = compute ? t.createNoOpComputePipeline() : t.createNoOpRenderPipeline(); + t.setPipeline(pass, pipeline); + t.issueDrawOrDispatch(pass); + } + pass.setBindGroup(0, bindGroup1); + pass.end(); + + // 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'; + // 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; + + t.expectValidationError(() => { + encoder.finish(); + }, !success); + }); + +g.test('bindings_in_bundle') + .desc( + ` + Test the texture usages in bundles by using two bindings of the same texture with various + combination of {sampled, storage, render target} usages. + ` + ) + .params(u => + u + .combine('type0', ['render-target', ...kTextureBindingTypes] as const) + .combine('type1', ['render-target', ...kTextureBindingTypes] as const) + .beginSubcases() + .combine('binding0InBundle', [false, true]) + .combine('binding1InBundle', [false, true]) + .expandWithParams(function* ({ type0, type1 }) { + const usageForType = (type: typeof type0 | typeof type1) => { + switch (type) { + case 'multisampled-texture': + case 'sampled-texture': + return 'TEXTURE_BINDING' as const; + case 'writeonly-storage-texture': + return 'STORAGE_BINDING' as const; + case 'render-target': + return 'RENDER_ATTACHMENT' as const; + } + }; + + yield { + _usage0: usageForType(type0), + _usage1: usageForType(type1), + _sampleCount: + type0 === 'multisampled-texture' || type1 === 'multisampled-texture' + ? (4 as const) + : undefined, + }; + }) + .unless( + p => + // We can't set 'render-target' in bundle, so we need to exclude it from bundle. + // In addition, if both bindings are non-bundle, there is no need to test it because + // we have far more comprehensive test cases for that situation in this file. + (p.binding0InBundle && p.type0 === 'render-target') || + (p.binding1InBundle && p.type1 === 'render-target') || + (!p.binding0InBundle && !p.binding1InBundle) || + // Storage textures can't be multisampled. + (p._sampleCount !== undefined && + p._sampleCount > 1 && + (p._usage0 === 'STORAGE_BINDING' || p._usage1 === 'STORAGE_BINDING')) || + // If both are sampled, we create two views of the same texture, so both must be + // multisampled. + (p.type0 === 'multisampled-texture' && p.type1 === 'sampled-texture') || + (p.type0 === 'sampled-texture' && p.type1 === 'multisampled-texture') + ) + ) + .fn(t => { + const { + binding0InBundle, + binding1InBundle, + type0, + type1, + _usage0, + _usage1, + _sampleCount, + } = t.params; + + // Two bindings are attached to the same texture view. + const usage = + _sampleCount === 4 + ? GPUTextureUsage[_usage0] | GPUTextureUsage[_usage1] | GPUTextureUsage.RENDER_ATTACHMENT + : GPUTextureUsage[_usage0] | GPUTextureUsage[_usage1]; + const view = t + .createTexture({ + usage, + sampleCount: _sampleCount, + }) + .createView(); + + const bindGroups: GPUBindGroup[] = []; + if (type0 !== 'render-target') { + const binding0TexFormat = type0 === 'sampled-texture' ? undefined : 'rgba8unorm'; + bindGroups[0] = t.createBindGroup(0, view, type0, '2d', { format: binding0TexFormat }); + } + if (type1 !== 'render-target') { + const binding1TexFormat = type1 === 'sampled-texture' ? undefined : 'rgba8unorm'; + bindGroups[1] = t.createBindGroup(1, view, type1, '2d', { format: binding1TexFormat }); + } + + const encoder = t.device.createCommandEncoder(); + // At least one binding is in bundle, which means that its type is not 'render-target'. + // As a result, only one binding's type is 'render-target' at most. + const pass = t.beginSimpleRenderPass( + encoder, + type0 === 'render-target' || type1 === 'render-target' ? view : t.createTexture().createView() + ); + + const bindingsInBundle: boolean[] = [binding0InBundle, binding1InBundle]; + for (let i = 0; i < 2; i++) { + // Create a bundle for each bind group if its bindings is required to be in bundle on purpose. + // Otherwise, call setBindGroup directly in pass if needed (when its binding is not + // 'render-target'). + if (bindingsInBundle[i]) { + const bundleEncoder = t.device.createRenderBundleEncoder({ + colorFormats: ['rgba8unorm'], + }); + bundleEncoder.setBindGroup(i, bindGroups[i]); + const bundleInPass = bundleEncoder.finish(); + pass.executeBundles([bundleInPass]); + } else if (bindGroups[i] !== undefined) { + pass.setBindGroup(i, bindGroups[i]); + } + } + + pass.end(); + + const isReadOnly = (t: typeof type0 | typeof type1) => { + switch (t) { + case 'sampled-texture': + case 'multisampled-texture': + return true; + default: + return false; + } + }; + + let success = false; + if (isReadOnly(type0) && isReadOnly(type1)) { + success = true; + } + + if (type0 === 'writeonly-storage-texture' && type1 === 'writeonly-storage-texture') { + success = true; + } + + // Resource usages in bundle should be validated. + t.expectValidationError(() => { + encoder.finish(); + }, !success); + }); + +g.test('unused_bindings_in_pipeline') + .desc( + ` + Test that for compute pipelines with 'auto' layout, only bindings used by the pipeline count + toward the usage scope. For render passes, test the pipeline doesn't matter because only the + calls to setBindGroup count toward the usage scope. + ` + ) + .params(u => + u + .combine('compute', [false, true]) + .combine('useBindGroup0', [false, true]) + .combine('useBindGroup1', [false, true]) + .combine('setBindGroupsOrder', ['common', 'reversed'] as const) + .combine('setPipeline', ['before', 'middle', 'after', 'none'] as const) + .combine('callDrawOrDispatch', [false, true]) + ) + .fn(t => { + const { + compute, + useBindGroup0, + useBindGroup1, + setBindGroupsOrder, + setPipeline, + callDrawOrDispatch, + } = t.params; + const view = t + .createTexture({ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING }) + .createView(); + const bindGroup0 = t.createBindGroup(0, view, 'sampled-texture', '2d', { + format: 'rgba8unorm', + }); + const bindGroup1 = t.createBindGroup(0, view, 'writeonly-storage-texture', '2d', { + format: 'rgba8unorm', + }); + + const wgslVertex = `@vertex fn main() -> @builtin(position) vec4<f32> { + return vec4<f32>(); +}`; + const wgslFragment = pp` + ${pp._if(useBindGroup0)} + @group(0) @binding(0) var image0 : texture_storage_2d<rgba8unorm, write>; + ${pp._endif} + ${pp._if(useBindGroup1)} + @group(1) @binding(0) var image1 : texture_storage_2d<rgba8unorm, write>; + ${pp._endif} + @fragment fn main() {} + `; + + const wgslCompute = pp` + ${pp._if(useBindGroup0)} + @group(0) @binding(0) var image0 : texture_storage_2d<rgba8unorm, write>; + ${pp._endif} + ${pp._if(useBindGroup1)} + @group(1) @binding(0) var image1 : texture_storage_2d<rgba8unorm, write>; + ${pp._endif} + @compute @workgroup_size(1) fn main() {} + `; + + const pipeline = compute + ? t.device.createComputePipeline({ + layout: 'auto', + compute: { + module: t.device.createShaderModule({ + code: wgslCompute, + }), + entryPoint: 'main', + }, + }) + : t.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: t.device.createShaderModule({ + code: wgslVertex, + }), + entryPoint: 'main', + }, + fragment: { + module: t.device.createShaderModule({ + code: wgslFragment, + }), + entryPoint: 'main', + targets: [{ format: 'rgba8unorm', writeMask: 0 }], + }, + primitive: { topology: 'triangle-list' }, + }); + + const encoder = t.device.createCommandEncoder(); + const pass = compute + ? encoder.beginComputePass() + : encoder.beginRenderPass({ + colorAttachments: [ + { + view: t.createTexture().createView(), + clearValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + const index0 = setBindGroupsOrder === 'common' ? 0 : 1; + const index1 = setBindGroupsOrder === 'common' ? 1 : 0; + if (setPipeline === 'before') t.setPipeline(pass, pipeline); + pass.setBindGroup(index0, bindGroup0); + if (setPipeline === 'middle') t.setPipeline(pass, pipeline); + pass.setBindGroup(index1, bindGroup1); + if (setPipeline === 'after') t.setPipeline(pass, pipeline); + if (callDrawOrDispatch) t.issueDrawOrDispatch(pass); + pass.end(); + + // Resource usage validation scope is defined by the whole render pass or by dispatch calls. + // Regardless of whether or not dispatch is called, in a compute pass, we always succeed + // because in this test, none of the bindings are used by the pipeline. + // In a render pass, we always fail because usage is based on any bindings used in the + // render pass, regardless of whether the pipeline uses them. + let success = compute; + + // Also fails if we try to draw/dispatch without a pipeline. + if (callDrawOrDispatch && setPipeline === 'none') { + success = false; + } + + t.expectValidationError(() => { + encoder.finish(); + }, !success); + }); + +g.test('scope,dispatch') + .desc( + ` + Tests that in a compute pass, no usage validation occurs without a dispatch call. + {Sets,skips} each of two conflicting bind groups in a pass {with,without} a dispatch call. + If both are set, AND there is a dispatch call, validation should fail. + ` + ) + .params(u => + u + .combine('dispatch', ['none', 'direct', 'indirect']) + .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 { bindGroup0, bindGroup1, encoder, pass, pipeline } = t.testValidationScope(true); + assert(pass instanceof GPUComputePassEncoder); + t.setPipeline(pass, pipeline); + + if (setBindGroup0) pass.setBindGroup(0, bindGroup0); + if (setBindGroup1) pass.setBindGroup(1, bindGroup1); + + switch (dispatch) { + case 'direct': + pass.dispatchWorkgroups(1); + break; + case 'indirect': + { + const indirectBuffer = t.device.createBuffer({ size: 4, usage: GPUBufferUsage.INDIRECT }); + pass.dispatchWorkgroupsIndirect(indirectBuffer, 0); + } + break; + } + + pass.end(); + + t.expectValidationError(() => { + encoder.finish(); + }, dispatch !== 'none' && setBindGroup0 && setBindGroup1); + }); + +g.test('scope,basic,render') + .desc( + ` + Tests that in a render pass, validation occurs even without a pipeline or draw call. + {Set,skip} each of two conflicting bind groups. If both are set, validation should fail. + ` + ) + .paramsSubcasesOnly(u => + u // + .combine('setBindGroup0', [false, true]) + .combine('setBindGroup1', [false, true]) + ) + .fn(t => { + const { setBindGroup0, setBindGroup1 } = t.params; + + const { bindGroup0, bindGroup1, encoder, pass } = t.testValidationScope(false); + assert(pass instanceof GPURenderPassEncoder); + + if (setBindGroup0) pass.setBindGroup(0, bindGroup0); + if (setBindGroup1) pass.setBindGroup(1, bindGroup1); + + pass.end(); + + t.expectValidationError(() => { + encoder.finish(); + }, setBindGroup0 && setBindGroup1); + }); + +g.test('scope,pass_boundary,compute') + .desc( + ` + Test using two conflicting bind groups in separate dispatch calls, {with,without} a pass + boundary in between. This should always be valid. + ` + ) + .paramsSubcasesOnly(u => u.combine('splitPass', [false, true])) + .fn(t => { + const { splitPass } = t.params; + + const { bindGroupLayouts, bindGroups } = t.makeConflictingBindGroups(); + + const encoder = t.device.createCommandEncoder(); + + const pipelineUsingBG0 = t.createNoOpComputePipeline( + t.device.createPipelineLayout({ + bindGroupLayouts: [bindGroupLayouts[0]], + }) + ); + const pipelineUsingBG1 = t.createNoOpComputePipeline( + t.device.createPipelineLayout({ + bindGroupLayouts: [bindGroupLayouts[1]], + }) + ); + + let pass = encoder.beginComputePass(); + pass.setPipeline(pipelineUsingBG0); + pass.setBindGroup(0, bindGroups[0]); + pass.dispatchWorkgroups(1); + if (splitPass) { + pass.end(); + pass = encoder.beginComputePass(); + } + pass.setPipeline(pipelineUsingBG1); + pass.setBindGroup(0, bindGroups[1]); + pass.dispatchWorkgroups(1); + pass.end(); + + // Always valid + encoder.finish(); + }); + +g.test('scope,pass_boundary,render') + .desc( + ` + Test using two conflicting bind groups in separate draw calls, {with,without} a pass + boundary in between. This should be valid only if there is a pass boundary. + ` + ) + .paramsSubcasesOnly(u => + u // + .combine('splitPass', [false, true]) + .combine('draw', [false, true]) + ) + .fn(t => { + const { splitPass, draw } = t.params; + + const { bindGroupLayouts, bindGroups } = t.makeConflictingBindGroups(); + + const encoder = t.device.createCommandEncoder(); + + const pipelineUsingBG0 = t.createNoOpRenderPipeline( + t.device.createPipelineLayout({ + bindGroupLayouts: [bindGroupLayouts[0]], + }) + ); + const pipelineUsingBG1 = t.createNoOpRenderPipeline( + t.device.createPipelineLayout({ + bindGroupLayouts: [bindGroupLayouts[1]], + }) + ); + + const attachment = t.createTexture().createView(); + + let pass = t.beginSimpleRenderPass(encoder, attachment); + pass.setPipeline(pipelineUsingBG0); + pass.setBindGroup(0, bindGroups[0]); + if (draw) pass.draw(3); + if (splitPass) { + pass.end(); + pass = t.beginSimpleRenderPass(encoder, attachment); + } + pass.setPipeline(pipelineUsingBG1); + pass.setBindGroup(0, bindGroups[1]); + if (draw) pass.draw(3); + pass.end(); + + t.expectValidationError(() => { + encoder.finish(); + }, !splitPass); + }); |