diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts | 420 |
1 files changed, 420 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts new file mode 100644 index 0000000000..afde0863bb --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/resource_usages/texture/in_render_misc.spec.ts @@ -0,0 +1,420 @@ +export const description = ` +Texture Usages Validation Tests on All Kinds of WebGPU Subresource Usage Scopes. +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { unreachable } from '../../../../../common/util/util.js'; +import { ValidationTest } from '../../validation_test.js'; + +class F extends ValidationTest { + createBindGroupLayoutForTest( + textureUsage: 'texture' | 'storage', + sampleType: 'float' | 'depth' | 'uint', + visibility: GPUShaderStage['FRAGMENT'] | GPUShaderStage['COMPUTE'] = GPUShaderStage['FRAGMENT'] + ): GPUBindGroupLayout { + const bindGroupLayoutEntry: GPUBindGroupLayoutEntry = { + binding: 0, + visibility, + }; + + switch (textureUsage) { + case 'texture': + bindGroupLayoutEntry.texture = { viewDimension: '2d-array', sampleType }; + break; + case 'storage': + bindGroupLayoutEntry.storageTexture = { + access: 'write-only', + format: 'rgba8unorm', + viewDimension: '2d-array', + }; + break; + default: + unreachable(); + break; + } + return this.device.createBindGroupLayout({ + entries: [bindGroupLayoutEntry], + }); + } + + createBindGroupForTest( + textureView: GPUTextureView, + textureUsage: 'texture' | 'storage', + sampleType: 'float' | 'depth' | 'uint', + visibility: GPUShaderStage['FRAGMENT'] | GPUShaderStage['COMPUTE'] = GPUShaderStage['FRAGMENT'] + ) { + return this.device.createBindGroup({ + layout: this.createBindGroupLayoutForTest(textureUsage, sampleType, visibility), + entries: [{ binding: 0, resource: textureView }], + }); + } +} + +export const g = makeTestGroup(F); + +const kTextureSize = 16; +const kTextureLayers = 3; + +g.test('subresources,set_bind_group_on_same_index_color_texture') + .desc( + ` + Test that when one color texture subresource is bound to different bind groups, whether the bind + groups are reset by another compatible ones or not, its list of internal usages within one usage + scope can only be a compatible usage list.` + ) + .params(u => + u + .combineWithParams([ + { useDifferentTextureAsTexture2: true, baseLayer2: 0, view2Binding: 'texture' }, + { useDifferentTextureAsTexture2: false, baseLayer2: 0, view2Binding: 'texture' }, + { useDifferentTextureAsTexture2: false, baseLayer2: 1, view2Binding: 'texture' }, + { useDifferentTextureAsTexture2: false, baseLayer2: 0, view2Binding: 'storage' }, + { useDifferentTextureAsTexture2: false, baseLayer2: 1, view2Binding: 'storage' }, + ] as const) + .combine('hasConflict', [true, false]) + ) + .fn(async t => { + const { useDifferentTextureAsTexture2, baseLayer2, view2Binding, hasConflict } = t.params; + + const texture0 = t.device.createTexture({ + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, + size: [kTextureSize, kTextureSize, kTextureLayers], + }); + // We always bind the first layer of the texture to bindGroup0. + const textureView0 = texture0.createView({ + dimension: '2d-array', + baseArrayLayer: 0, + arrayLayerCount: 1, + }); + const bindGroup0 = t.createBindGroupForTest(textureView0, view2Binding, 'float'); + + // In one renderPassEncoder it is an error to set both bindGroup0 and bindGroup1. + const view1Binding = hasConflict + ? view2Binding === 'texture' + ? 'storage' + : 'texture' + : view2Binding; + const bindGroup1 = t.createBindGroupForTest(textureView0, view1Binding, 'float'); + + const texture2 = useDifferentTextureAsTexture2 + ? t.device.createTexture({ + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, + size: [kTextureSize, kTextureSize, kTextureLayers], + }) + : texture0; + const textureView2 = texture2.createView({ + dimension: '2d-array', + baseArrayLayer: baseLayer2, + arrayLayerCount: kTextureLayers - baseLayer2, + }); + // There should be no conflict between bindGroup0 and validBindGroup2. + const validBindGroup2 = t.createBindGroupForTest(textureView2, view2Binding, 'float'); + + const colorTexture = t.device.createTexture({ + format: 'rgba8unorm', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + size: [kTextureSize, kTextureSize, 1], + }); + const encoder = t.device.createCommandEncoder(); + const renderPassEncoder = encoder.beginRenderPass({ + colorAttachments: [ + { + view: colorTexture.createView(), + loadOp: 'load', + storeOp: 'store', + }, + ], + }); + renderPassEncoder.setBindGroup(0, bindGroup0); + renderPassEncoder.setBindGroup(1, bindGroup1); + renderPassEncoder.setBindGroup(1, validBindGroup2); + renderPassEncoder.end(); + + t.expectValidationError(() => { + encoder.finish(); + }, hasConflict); + }); + +g.test('subresources,set_bind_group_on_same_index_depth_stencil_texture') + .desc( + ` + Test that when one depth stencil texture subresource is bound to different bind groups, whether + the bind groups are reset by another compatible ones or not, its list of internal usages within + one usage scope can only be a compatible usage list.` + ) + .params(u => + u + .combine('bindAspect', ['depth-only', 'stencil-only'] as const) + .combine('depthStencilReadOnly', [true, false]) + ) + .fn(async t => { + const { bindAspect, depthStencilReadOnly } = t.params; + const depthStencilTexture = t.device.createTexture({ + format: 'depth24plus-stencil8', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT, + size: [kTextureSize, kTextureSize, 1], + }); + + const conflictedToNonReadOnlyAttachmentBindGroup = t.createBindGroupForTest( + depthStencilTexture.createView({ + dimension: '2d-array', + aspect: bindAspect, + }), + 'texture', + bindAspect === 'depth-only' ? 'depth' : 'uint' + ); + + const colorTexture = t.device.createTexture({ + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, + size: [kTextureSize, kTextureSize, 1], + }); + const validBindGroup = t.createBindGroupForTest( + colorTexture.createView({ + dimension: '2d-array', + }), + 'texture', + 'float' + ); + + const encoder = t.device.createCommandEncoder(); + const renderPassEncoder = encoder.beginRenderPass({ + colorAttachments: [], + depthStencilAttachment: { + view: depthStencilTexture.createView(), + depthReadOnly: depthStencilReadOnly, + stencilReadOnly: depthStencilReadOnly, + }, + }); + renderPassEncoder.setBindGroup(0, conflictedToNonReadOnlyAttachmentBindGroup); + renderPassEncoder.setBindGroup(0, validBindGroup); + renderPassEncoder.end(); + + t.expectValidationError(() => { + encoder.finish(); + }, !depthStencilReadOnly); + }); + +g.test('subresources,set_unused_bind_group') + .desc( + ` + Test that when one texture subresource is bound to different bind groups and the bind groups are + used in the same render or compute pass encoder, its list of internal usages within one usage + scope can only be a compatible usage list.` + ) + .params(u => u.combine('inRenderPass', [true, false]).combine('hasConflict', [true, false])) + .fn(async t => { + const { inRenderPass, hasConflict } = t.params; + + const texture0 = t.device.createTexture({ + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, + size: [kTextureSize, kTextureSize, kTextureLayers], + }); + // We always bind the first layer of the texture to bindGroup0. + const textureView0 = texture0.createView({ + dimension: '2d-array', + baseArrayLayer: 0, + arrayLayerCount: 1, + }); + const visibility = inRenderPass ? GPUShaderStage.FRAGMENT : GPUShaderStage.COMPUTE; + // bindGroup0 is used by the pipelines, and bindGroup1 is not used by the pipelines. + const textureUsage0 = inRenderPass ? 'texture' : 'storage'; + const textureUsage1 = hasConflict ? (inRenderPass ? 'storage' : 'texture') : textureUsage0; + const bindGroup0 = t.createBindGroupForTest(textureView0, textureUsage0, 'float', visibility); + const bindGroup1 = t.createBindGroupForTest(textureView0, textureUsage1, 'float', visibility); + + const encoder = t.device.createCommandEncoder(); + const colorTexture = t.device.createTexture({ + format: 'rgba8unorm', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + size: [kTextureSize, kTextureSize, 1], + }); + const pipelineLayout = t.device.createPipelineLayout({ + bindGroupLayouts: [t.createBindGroupLayoutForTest(textureUsage0, 'float', visibility)], + }); + if (inRenderPass) { + const renderPipeline = t.device.createRenderPipeline({ + layout: pipelineLayout, + vertex: { + module: t.device.createShaderModule({ + code: t.getNoOpShaderCode('VERTEX'), + }), + entryPoint: 'main', + }, + fragment: { + module: t.device.createShaderModule({ + code: ` + @group(0) @binding(0) var texture0 : texture_2d_array<f32>; + @fragment fn main() + -> @location(0) vec4<f32> { + return textureLoad(texture0, vec2<i32>(), 0, 0); + }`, + }), + entryPoint: 'main', + targets: [{ format: 'rgba8unorm' }], + }, + }); + + const renderPassEncoder = encoder.beginRenderPass({ + colorAttachments: [ + { + view: colorTexture.createView(), + loadOp: 'load', + storeOp: 'store', + }, + ], + }); + renderPassEncoder.setBindGroup(0, bindGroup0); + renderPassEncoder.setBindGroup(1, bindGroup1); + renderPassEncoder.setPipeline(renderPipeline); + renderPassEncoder.draw(1); + renderPassEncoder.end(); + } else { + const computePipeline = t.device.createComputePipeline({ + layout: pipelineLayout, + compute: { + module: t.device.createShaderModule({ + code: ` + @group(0) @binding(0) var texture0 : texture_storage_2d_array<rgba8unorm, write>; + @compute @workgroup_size(1) + fn main() { + textureStore(texture0, vec2<i32>(), 0, vec4<f32>()); + }`, + }), + entryPoint: 'main', + }, + }); + const computePassEncoder = encoder.beginComputePass(); + computePassEncoder.setBindGroup(0, bindGroup0); + computePassEncoder.setBindGroup(1, bindGroup1); + computePassEncoder.setPipeline(computePipeline); + computePassEncoder.dispatchWorkgroups(1); + computePassEncoder.end(); + } + + // In WebGPU SPEC (Chapter 3.4.5, Synchronization): + // This specification defines the following usage scopes: + // - In a compute pass, each dispatch command (dispatchWorkgroups() or + // dispatchWorkgroupsIndirect()) is one usage scope. A subresource is "used" in the usage + // scope if it is potentially accessible by the command. State-setting compute pass commands, + // like setBindGroup(index, bindGroup, dynamicOffsets), do not contribute directly to a usage + // scope. + // - One render pass is one usage scope. A subresource is "used" in the usage scope if it’s + // referenced by any (state-setting or non-state-setting) command. For example, in + // setBindGroup(index, bindGroup, dynamicOffsets), every subresource in bindGroup is "used" in + // the render pass’s usage scope. + const success = !inRenderPass || !hasConflict; + t.expectValidationError(() => { + encoder.finish(); + }, !success); + }); + +g.test('subresources,texture_usages_in_copy_and_render_pass') + .desc( + ` + Test that using one texture subresource in a render pass encoder and a copy command is always + allowed as WebGPU SPEC (chapter 3.4.5) defines that out of any pass encoder, each command always + belongs to one usage scope.` + ) + .params(u => + u + .combine('usage0', [ + 'copy-src', + 'copy-dst', + 'texture', + 'storage', + 'color-attachment', + ] as const) + .combine('usage1', [ + 'copy-src', + 'copy-dst', + 'texture', + 'storage', + 'color-attachment', + ] as const) + .filter( + ({ usage0, usage1 }) => + usage0 === 'copy-src' || + usage0 === 'copy-dst' || + usage1 === 'copy-src' || + usage1 === 'copy-dst' + ) + ) + .fn(async t => { + const { usage0, usage1 } = t.params; + + const texture = t.device.createTexture({ + format: 'rgba8unorm', + usage: + GPUTextureUsage.COPY_SRC | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.STORAGE_BINDING | + GPUTextureUsage.RENDER_ATTACHMENT, + size: [kTextureSize, kTextureSize, 1], + }); + + const UseTextureOnCommandEncoder = ( + texture: GPUTexture, + usage: 'copy-src' | 'copy-dst' | 'texture' | 'storage' | 'color-attachment', + encoder: GPUCommandEncoder + ) => { + switch (usage) { + case 'copy-src': { + const buffer = t.createBufferWithState('valid', { + size: 4, + usage: GPUBufferUsage.COPY_DST, + }); + encoder.copyTextureToBuffer({ texture }, { buffer }, [1, 1, 1]); + break; + } + case 'copy-dst': { + const buffer = t.createBufferWithState('valid', { + size: 4, + usage: GPUBufferUsage.COPY_SRC, + }); + encoder.copyBufferToTexture({ buffer }, { texture }, [1, 1, 1]); + break; + } + case 'color-attachment': { + const renderPassEncoder = encoder.beginRenderPass({ + colorAttachments: [{ view: texture.createView(), loadOp: 'load', storeOp: 'store' }], + }); + renderPassEncoder.end(); + break; + } + case 'texture': + case 'storage': { + const colorTexture = t.device.createTexture({ + format: 'rgba8unorm', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + size: [kTextureSize, kTextureSize, 1], + }); + const renderPassEncoder = encoder.beginRenderPass({ + colorAttachments: [ + { view: colorTexture.createView(), loadOp: 'load', storeOp: 'store' }, + ], + }); + const bindGroup = t.createBindGroupForTest( + texture.createView({ + dimension: '2d-array', + }), + usage, + 'float' + ); + renderPassEncoder.setBindGroup(0, bindGroup); + renderPassEncoder.end(); + break; + } + } + }; + const encoder = t.device.createCommandEncoder(); + UseTextureOnCommandEncoder(texture, usage0, encoder); + UseTextureOnCommandEncoder(texture, usage1, encoder); + t.expectValidationError(() => { + encoder.finish(); + }, false); + }); |