| import { makeTestGroup } from '../../../../common/framework/test_group.js'; |
| import { unreachable } from '../../../../common/util/util.js'; |
| import { GPUConst } from '../../../constants.js'; |
| import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js'; |
| import { getTextureCopyLayout } from '../../../util/texture/layout.js'; |
| import { PerTexelComponent } from '../../../util/texture/texel_data.js'; |
| |
| export const description = ` |
| Test uninitialized buffers are initialized to zero when read |
| (or read-written, e.g. with depth write or atomics). |
| |
| Note that: |
| - We don't need 'copy_buffer_to_buffer_copy_destination' here because there has already been an |
| operation test 'command_buffer.copyBufferToBuffer.single' that provides the same functionality. |
| `; |
| |
| const kMapModeOptions = [GPUConst.MapMode.READ, GPUConst.MapMode.WRITE]; |
| const kBufferUsagesForMappedAtCreationTests = [ |
| GPUConst.BufferUsage.COPY_DST | GPUConst.BufferUsage.MAP_READ, |
| GPUConst.BufferUsage.COPY_SRC | GPUConst.BufferUsage.MAP_WRITE, |
| GPUConst.BufferUsage.COPY_SRC, |
| ]; |
| |
| class F extends AllFeaturesMaxLimitsGPUTest { |
| getBufferUsageFromMapMode(mapMode: GPUMapModeFlags): number { |
| switch (mapMode) { |
| case GPUMapMode.READ: |
| return GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ; |
| case GPUMapMode.WRITE: |
| return GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE; |
| default: |
| unreachable(); |
| return 0; |
| } |
| } |
| |
| checkGPUBufferContent( |
| buffer: GPUBuffer, |
| bufferUsage: GPUBufferUsageFlags, |
| expectedData: Uint8Array |
| ): void { |
| const mappable = bufferUsage & GPUBufferUsage.MAP_READ; |
| this.expectGPUBufferValuesEqual(buffer, expectedData, 0, { method: mappable ? 'map' : 'copy' }); |
| } |
| |
| testBufferZeroInitInBindGroup( |
| computeShaderModule: GPUShaderModule, |
| buffer: GPUBuffer, |
| bufferOffset: number, |
| boundBufferSize: number |
| ): void { |
| this.TestBufferZeroInitInBindGroupInternal( |
| computeShaderModule, |
| buffer, |
| false, |
| bufferOffset, |
| boundBufferSize |
| ); |
| const bindBufferResource = bufferOffset === 0 && boundBufferSize === buffer.size; |
| if (bindBufferResource) { |
| this.TestBufferZeroInitInBindGroupInternal( |
| computeShaderModule, |
| buffer, |
| true, |
| bufferOffset, |
| boundBufferSize |
| ); |
| } |
| } |
| |
| TestBufferZeroInitInBindGroupInternal( |
| computeShaderModule: GPUShaderModule, |
| buffer: GPUBuffer, |
| bindBufferResource: boolean, |
| bufferOffset: number, |
| boundBufferSize: number |
| ): void { |
| const computePipeline = this.device.createComputePipeline({ |
| layout: 'auto', |
| compute: { |
| module: computeShaderModule, |
| entryPoint: 'main', |
| }, |
| }); |
| const outputTexture = this.createTextureTracked({ |
| format: 'rgba8unorm', |
| size: [1, 1, 1], |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.STORAGE_BINDING, |
| }); |
| const resource = bindBufferResource |
| ? buffer |
| : { |
| buffer, |
| offset: bufferOffset, |
| size: boundBufferSize, |
| }; |
| const bindGroup = this.device.createBindGroup({ |
| layout: computePipeline.getBindGroupLayout(0), |
| entries: [ |
| { |
| binding: 0, |
| resource, |
| }, |
| { |
| binding: 1, |
| resource: outputTexture.createView(), |
| }, |
| ], |
| }); |
| |
| const encoder = this.device.createCommandEncoder(); |
| const computePass = encoder.beginComputePass(); |
| computePass.setBindGroup(0, bindGroup); |
| computePass.setPipeline(computePipeline); |
| computePass.dispatchWorkgroups(1); |
| computePass.end(); |
| this.queue.submit([encoder.finish()]); |
| |
| this.checkBufferAndOutputTexture(buffer, boundBufferSize + bufferOffset, outputTexture); |
| } |
| |
| createRenderPipelineForTest( |
| vertexShaderModule: GPUShaderModule, |
| testVertexBuffer: boolean |
| ): GPURenderPipeline { |
| const renderPipelineDescriptor: GPURenderPipelineDescriptor = { |
| layout: 'auto', |
| vertex: { |
| module: vertexShaderModule, |
| entryPoint: 'main', |
| }, |
| fragment: { |
| module: this.device.createShaderModule({ |
| code: ` |
| @fragment |
| fn main(@location(0) i_color : vec4<f32>) -> @location(0) vec4<f32> { |
| return i_color; |
| }`, |
| }), |
| entryPoint: 'main', |
| targets: [{ format: 'rgba8unorm' }], |
| }, |
| primitive: { |
| topology: 'point-list', |
| }, |
| }; |
| if (testVertexBuffer) { |
| renderPipelineDescriptor.vertex.buffers = [ |
| { |
| arrayStride: 16, |
| attributes: [{ format: 'float32x4', offset: 0, shaderLocation: 0 }], |
| }, |
| ]; |
| } |
| |
| return this.device.createRenderPipeline(renderPipelineDescriptor); |
| } |
| |
| recordInitializeTextureColor( |
| encoder: GPUCommandEncoder, |
| texture: GPUTexture, |
| color: GPUColor |
| ): void { |
| const renderPass = encoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| view: texture.createView(), |
| clearValue: color, |
| loadOp: 'clear', |
| storeOp: 'store', |
| }, |
| ], |
| }); |
| renderPass.end(); |
| } |
| |
| checkBufferAndOutputTexture( |
| buffer: GPUBuffer, |
| bufferSize: number, |
| outputTexture: GPUTexture, |
| outputTextureSize: [number, number, number] = [1, 1, 1], |
| outputTextureColor: PerTexelComponent<number> = { R: 0.0, G: 1.0, B: 0.0, A: 1.0 } |
| ): void { |
| this.expectSingleColor(outputTexture, 'rgba8unorm', { |
| size: outputTextureSize, |
| exp: outputTextureColor, |
| }); |
| |
| const expectedBufferData = new Uint8Array(bufferSize); |
| this.expectGPUBufferValuesEqual(buffer, expectedBufferData); |
| } |
| } |
| |
| export const g = makeTestGroup(F); |
| |
| g.test('partial_write_buffer') |
| .desc( |
| `Verify when we upload data to a part of a buffer with writeBuffer() just after the creation of |
| the buffer, the remaining part of that buffer will be initialized to 0.` |
| ) |
| .paramsSubcasesOnly(u => u.combine('offset', [0, 8, -12])) |
| .fn(t => { |
| const { offset } = t.params; |
| const bufferSize = 32; |
| const appliedOffset = offset >= 0 ? offset : bufferSize + offset; |
| |
| const buffer = t.createBufferTracked({ |
| size: bufferSize, |
| usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, |
| }); |
| |
| const copySize = 12; |
| const writeData = new Uint8Array(copySize); |
| const expectedData = new Uint8Array(bufferSize); |
| for (let i = 0; i < copySize; ++i) { |
| expectedData[appliedOffset + i] = writeData[i] = i + 1; |
| } |
| t.queue.writeBuffer(buffer, appliedOffset, writeData, 0); |
| |
| t.expectGPUBufferValuesEqual(buffer, expectedData); |
| }); |
| |
| g.test('map_whole_buffer') |
| .desc( |
| `Verify when we map the whole range of a mappable GPUBuffer to a typed array buffer just after |
| creating the GPUBuffer, the contents of both the typed array buffer and the GPUBuffer itself |
| have already been initialized to 0.` |
| ) |
| .params(u => u.combine('mapMode', kMapModeOptions)) |
| .fn(async t => { |
| const { mapMode } = t.params; |
| |
| const bufferSize = 32; |
| const bufferUsage = t.getBufferUsageFromMapMode(mapMode); |
| const buffer = t.createBufferTracked({ |
| size: bufferSize, |
| usage: bufferUsage, |
| }); |
| |
| await buffer.mapAsync(mapMode); |
| const readData = new Uint8Array(buffer.getMappedRange()); |
| for (let i = 0; i < bufferSize; ++i) { |
| t.expect(readData[i] === 0); |
| } |
| buffer.unmap(); |
| |
| const expectedData = new Uint8Array(bufferSize); |
| t.checkGPUBufferContent(buffer, bufferUsage, expectedData); |
| }); |
| |
| g.test('map_partial_buffer') |
| .desc( |
| `Verify when we map a subrange of a mappable GPUBuffer to a typed array buffer just after the |
| creation of the GPUBuffer, the contents of both the typed array buffer and the GPUBuffer have |
| already been initialized to 0.` |
| ) |
| .params(u => u.combine('mapMode', kMapModeOptions).beginSubcases().combine('offset', [0, 8, -16])) |
| .fn(async t => { |
| const { mapMode, offset } = t.params; |
| const bufferSize = 32; |
| const appliedOffset = offset >= 0 ? offset : bufferSize + offset; |
| |
| const bufferUsage = t.getBufferUsageFromMapMode(mapMode); |
| const buffer = t.createBufferTracked({ |
| size: bufferSize, |
| usage: bufferUsage, |
| }); |
| |
| const expectedData = new Uint8Array(bufferSize); |
| { |
| const mapSize = 16; |
| await buffer.mapAsync(mapMode, appliedOffset, mapSize); |
| const mappedData = new Uint8Array(buffer.getMappedRange(appliedOffset, mapSize)); |
| for (let i = 0; i < mapSize; ++i) { |
| t.expect(mappedData[i] === 0); |
| if (mapMode === GPUMapMode.WRITE) { |
| mappedData[i] = expectedData[appliedOffset + i] = i + 1; |
| } |
| } |
| buffer.unmap(); |
| } |
| |
| t.checkGPUBufferContent(buffer, bufferUsage, expectedData); |
| }); |
| |
| g.test('mapped_at_creation_whole_buffer') |
| .desc( |
| `Verify when we call getMappedRange() at the whole range of a GPUBuffer created with |
| mappedAtCreation === true just after its creation, the contents of both the returned typed |
| array buffer of getMappedRange() and the GPUBuffer itself have all been initialized to 0.` |
| ) |
| .params(u => u.combine('bufferUsage', kBufferUsagesForMappedAtCreationTests)) |
| .fn(t => { |
| const { bufferUsage } = t.params; |
| |
| const bufferSize = 32; |
| const buffer = t.createBufferTracked({ |
| mappedAtCreation: true, |
| size: bufferSize, |
| usage: bufferUsage, |
| }); |
| |
| const mapped = new Uint8Array(buffer.getMappedRange()); |
| for (let i = 0; i < bufferSize; ++i) { |
| t.expect(mapped[i] === 0); |
| } |
| buffer.unmap(); |
| |
| const expectedData = new Uint8Array(bufferSize); |
| t.checkGPUBufferContent(buffer, bufferUsage, expectedData); |
| }); |
| |
| g.test('mapped_at_creation_partial_buffer') |
| .desc( |
| `Verify when we call getMappedRange() at a subrange of a GPUBuffer created with |
| mappedAtCreation === true just after its creation, the contents of both the returned typed |
| array buffer of getMappedRange() and the GPUBuffer itself have all been initialized to 0.` |
| ) |
| .params(u => |
| u |
| .combine('bufferUsage', kBufferUsagesForMappedAtCreationTests) |
| .beginSubcases() |
| .combine('offset', [0, 8, -16]) |
| ) |
| .fn(t => { |
| const { bufferUsage, offset } = t.params; |
| const bufferSize = 32; |
| const appliedOffset = offset >= 0 ? offset : bufferSize + offset; |
| |
| const buffer = t.createBufferTracked({ |
| mappedAtCreation: true, |
| size: bufferSize, |
| usage: bufferUsage, |
| }); |
| |
| const expectedData = new Uint8Array(bufferSize); |
| { |
| const mappedSize = 12; |
| const mapped = new Uint8Array(buffer.getMappedRange(appliedOffset, mappedSize)); |
| for (let i = 0; i < mappedSize; ++i) { |
| t.expect(mapped[i] === 0); |
| if (!(bufferUsage & GPUBufferUsage.MAP_READ)) { |
| mapped[i] = expectedData[appliedOffset + i] = i + 1; |
| } |
| } |
| buffer.unmap(); |
| } |
| |
| t.checkGPUBufferContent(buffer, bufferUsage, expectedData); |
| }); |
| |
| g.test('copy_buffer_to_buffer_copy_source') |
| .desc( |
| `Verify when the first usage of a GPUBuffer is being used as the source buffer of |
| CopyBufferToBuffer(), the contents of the GPUBuffer have already been initialized to 0.` |
| ) |
| .fn(t => { |
| const bufferSize = 32; |
| const bufferUsage = GPUBufferUsage.COPY_SRC; |
| const buffer = t.createBufferTracked({ |
| size: bufferSize, |
| usage: bufferUsage, |
| }); |
| |
| const expectedData = new Uint8Array(bufferSize); |
| // copyBufferToBuffer() is called inside t.CheckGPUBufferContent(). |
| t.checkGPUBufferContent(buffer, bufferUsage, expectedData); |
| }); |
| |
| g.test('copy_buffer_to_texture') |
| .desc( |
| `Verify when the first usage of a GPUBuffer is being used as the source buffer of |
| CopyBufferToTexture(), the contents of the GPUBuffer have already been initialized to 0.` |
| ) |
| .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 8])) |
| .fn(t => { |
| const { bufferOffset } = t.params; |
| const textureSize: [number, number, number] = [8, 8, 1]; |
| const dstTextureFormat = 'rgba8unorm'; |
| |
| const dstTexture = t.createTextureTracked({ |
| size: textureSize, |
| format: dstTextureFormat, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST, |
| }); |
| const layout = getTextureCopyLayout(dstTextureFormat, '2d', textureSize); |
| const srcBufferSize = layout.byteLength + bufferOffset; |
| const srcBufferUsage = GPUBufferUsage.COPY_SRC; |
| const srcBuffer = t.createBufferTracked({ |
| size: srcBufferSize, |
| usage: srcBufferUsage, |
| }); |
| |
| const encoder = t.device.createCommandEncoder(); |
| encoder.copyBufferToTexture( |
| { |
| buffer: srcBuffer, |
| offset: bufferOffset, |
| bytesPerRow: layout.bytesPerRow, |
| rowsPerImage: layout.rowsPerImage, |
| }, |
| { texture: dstTexture }, |
| textureSize |
| ); |
| t.queue.submit([encoder.finish()]); |
| |
| t.checkBufferAndOutputTexture(srcBuffer, srcBufferSize, dstTexture, textureSize, { |
| R: 0.0, |
| G: 0.0, |
| B: 0.0, |
| A: 0.0, |
| }); |
| }); |
| |
| g.test('resolve_query_set_to_partial_buffer') |
| .desc( |
| `Verify when we resolve a query set into a GPUBuffer just after creating that GPUBuffer, the |
| remaining part of it will be initialized to 0.` |
| ) |
| .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 256])) |
| .fn(t => { |
| const { bufferOffset } = t.params; |
| const bufferSize = bufferOffset + 8; |
| const bufferUsage = GPUBufferUsage.COPY_SRC | GPUBufferUsage.QUERY_RESOLVE; |
| const dstBuffer = t.createBufferTracked({ |
| size: bufferSize, |
| usage: bufferUsage, |
| }); |
| |
| const querySet = t.createQuerySetTracked({ type: 'occlusion', count: 1 }); |
| const encoder = t.device.createCommandEncoder(); |
| encoder.resolveQuerySet(querySet, 0, 1, dstBuffer, bufferOffset); |
| t.queue.submit([encoder.finish()]); |
| |
| const expectedBufferData = new Uint8Array(bufferSize); |
| t.checkGPUBufferContent(dstBuffer, bufferUsage, expectedBufferData); |
| }); |
| |
| g.test('copy_texture_to_partial_buffer') |
| .desc( |
| `Verify when we copy from a GPUTexture into a GPUBuffer just after creating that GPUBuffer, the |
| remaining part of it will be initialized to 0.` |
| ) |
| .paramsSubcasesOnly(u => |
| u |
| .combine('bufferOffset', [0, 8, -16]) |
| .combine('arrayLayerCount', [1, 3]) |
| .combine('copyMipLevel', [0, 2]) |
| .combine('rowsPerImage', [16, 20]) |
| .filter(t => { |
| // We don't need to test the copies that will cover the whole GPUBuffer. |
| return !(t.bufferOffset === 0 && t.rowsPerImage === 16); |
| }) |
| ) |
| .fn(t => { |
| const { bufferOffset, arrayLayerCount, copyMipLevel, rowsPerImage } = t.params; |
| const srcTextureFormat = 'r8uint'; |
| const textureSize = [32, 16, arrayLayerCount] as const; |
| |
| const srcTexture = t.createTextureTracked({ |
| format: srcTextureFormat, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| size: textureSize, |
| mipLevelCount: copyMipLevel + 1, |
| }); |
| |
| const bytesPerRow = 256; |
| const layout = getTextureCopyLayout(srcTextureFormat, '2d', textureSize, { |
| mipLevel: copyMipLevel, |
| bytesPerRow, |
| rowsPerImage, |
| }); |
| |
| const dstBufferSize = layout.byteLength + Math.abs(bufferOffset); |
| const dstBuffer = t.createBufferTracked({ |
| size: dstBufferSize, |
| usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, |
| }); |
| |
| const encoder = t.device.createCommandEncoder(); |
| |
| // Initialize srcTexture |
| for (let layer = 0; layer < arrayLayerCount; ++layer) { |
| const renderPass = encoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| view: srcTexture.createView({ |
| baseArrayLayer: layer, |
| arrayLayerCount: 1, |
| baseMipLevel: copyMipLevel, |
| }), |
| clearValue: { r: layer + 1, g: 0, b: 0, a: 0 }, |
| loadOp: 'clear', |
| storeOp: 'store', |
| }, |
| ], |
| }); |
| renderPass.end(); |
| } |
| |
| // Do texture-to-buffer copy |
| const appliedOffset = Math.max(bufferOffset, 0); |
| encoder.copyTextureToBuffer( |
| { texture: srcTexture, mipLevel: copyMipLevel }, |
| { buffer: dstBuffer, offset: appliedOffset, bytesPerRow, rowsPerImage }, |
| layout.mipSize |
| ); |
| t.queue.submit([encoder.finish()]); |
| |
| // Check if the contents of the destination buffer are what we expect. |
| const expectedData = new Uint8Array(dstBufferSize); |
| for (let layer = 0; layer < arrayLayerCount; ++layer) { |
| for (let y = 0; y < layout.mipSize[1]; ++y) { |
| for (let x = 0; x < layout.mipSize[0]; ++x) { |
| expectedData[appliedOffset + layer * bytesPerRow * rowsPerImage + y * bytesPerRow + x] = |
| layer + 1; |
| } |
| } |
| } |
| t.expectGPUBufferValuesEqual(dstBuffer, expectedData); |
| }); |
| |
| g.test('uniform_buffer') |
| .desc( |
| `Verify when we use a GPUBuffer as a uniform buffer just after the creation of that GPUBuffer, |
| all the contents in that GPUBuffer have been initialized to 0.` |
| ) |
| .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 256])) |
| .fn(t => { |
| const { bufferOffset } = t.params; |
| |
| const boundBufferSize = 16; |
| const buffer = t.createBufferTracked({ |
| size: bufferOffset + boundBufferSize, |
| usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.UNIFORM, |
| }); |
| |
| const computeShaderModule = t.device.createShaderModule({ |
| code: ` |
| struct UBO { |
| value : vec4<u32> |
| }; |
| @group(0) @binding(0) var<uniform> ubo : UBO; |
| @group(0) @binding(1) var outImage : texture_storage_2d<rgba8unorm, write>; |
| |
| @compute @workgroup_size(1) fn main() { |
| if (all(ubo.value == vec4<u32>(0u, 0u, 0u, 0u))) { |
| textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(0.0, 1.0, 0.0, 1.0)); |
| } else { |
| textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(1.0, 0.0, 0.0, 1.0)); |
| } |
| }`, |
| }); |
| |
| // Verify the whole range of the buffer has been initialized to 0 in a compute shader. |
| t.testBufferZeroInitInBindGroup(computeShaderModule, buffer, bufferOffset, boundBufferSize); |
| }); |
| |
| g.test('readonly_storage_buffer') |
| .desc( |
| `Verify when we use a GPUBuffer as a read-only storage buffer just after the creation of that |
| GPUBuffer, all the contents in that GPUBuffer have been initialized to 0.` |
| ) |
| .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 256])) |
| .fn(t => { |
| const { bufferOffset } = t.params; |
| const boundBufferSize = 16; |
| const buffer = t.createBufferTracked({ |
| size: bufferOffset + boundBufferSize, |
| usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE, |
| }); |
| |
| const computeShaderModule = t.device.createShaderModule({ |
| code: ` |
| struct SSBO { |
| value : vec4<u32> |
| }; |
| @group(0) @binding(0) var<storage, read> ssbo : SSBO; |
| @group(0) @binding(1) var outImage : texture_storage_2d<rgba8unorm, write>; |
| |
| @compute @workgroup_size(1) fn main() { |
| if (all(ssbo.value == vec4<u32>(0u, 0u, 0u, 0u))) { |
| textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(0.0, 1.0, 0.0, 1.0)); |
| } else { |
| textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(1.0, 0.0, 0.0, 1.0)); |
| } |
| }`, |
| }); |
| |
| // Verify the whole range of the buffer has been initialized to 0 in a compute shader. |
| t.testBufferZeroInitInBindGroup(computeShaderModule, buffer, bufferOffset, boundBufferSize); |
| }); |
| |
| g.test('storage_buffer') |
| .desc( |
| `Verify when we use a GPUBuffer as a storage buffer just after the creation of that |
| GPUBuffer, all the contents in that GPUBuffer have been initialized to 0.` |
| ) |
| .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 256])) |
| .fn(t => { |
| const { bufferOffset } = t.params; |
| const boundBufferSize = 16; |
| const buffer = t.createBufferTracked({ |
| size: bufferOffset + boundBufferSize, |
| usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE, |
| }); |
| |
| const computeShaderModule = t.device.createShaderModule({ |
| code: ` |
| struct SSBO { |
| value : vec4<u32> |
| }; |
| @group(0) @binding(0) var<storage, read_write> ssbo : SSBO; |
| @group(0) @binding(1) var outImage : texture_storage_2d<rgba8unorm, write>; |
| |
| @compute @workgroup_size(1) fn main() { |
| if (all(ssbo.value == vec4<u32>(0u, 0u, 0u, 0u))) { |
| textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(0.0, 1.0, 0.0, 1.0)); |
| } else { |
| textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(1.0, 0.0, 0.0, 1.0)); |
| } |
| }`, |
| }); |
| |
| // Verify the whole range of the buffer has been initialized to 0 in a compute shader. |
| t.testBufferZeroInitInBindGroup(computeShaderModule, buffer, bufferOffset, boundBufferSize); |
| }); |
| |
| g.test('vertex_buffer') |
| .desc( |
| `Verify when we use a GPUBuffer as a vertex buffer just after the creation of that |
| GPUBuffer, all the contents in that GPUBuffer have been initialized to 0.` |
| ) |
| .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 16])) |
| .fn(t => { |
| const { bufferOffset } = t.params; |
| |
| const renderPipeline = t.createRenderPipelineForTest( |
| t.device.createShaderModule({ |
| code: ` |
| struct VertexOut { |
| @location(0) color : vec4<f32>, |
| @builtin(position) position : vec4<f32>, |
| }; |
| |
| @vertex fn main(@location(0) pos : vec4<f32>) -> VertexOut { |
| var output : VertexOut; |
| if (all(pos == vec4<f32>(0.0, 0.0, 0.0, 0.0))) { |
| output.color = vec4<f32>(0.0, 1.0, 0.0, 1.0); |
| } else { |
| output.color = vec4<f32>(1.0, 0.0, 0.0, 1.0); |
| } |
| output.position = vec4<f32>(0.0, 0.0, 0.0, 1.0); |
| return output; |
| }`, |
| }), |
| true |
| ); |
| |
| const bufferSize = 16 + bufferOffset; |
| const vertexBuffer = t.createBufferTracked({ |
| size: bufferSize, |
| usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_SRC, |
| }); |
| |
| const outputTexture = t.createTextureTracked({ |
| format: 'rgba8unorm', |
| size: [1, 1, 1], |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| }); |
| |
| const encoder = t.device.createCommandEncoder(); |
| const renderPass = encoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| view: outputTexture.createView(), |
| clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, |
| loadOp: 'clear', |
| storeOp: 'store', |
| }, |
| ], |
| }); |
| renderPass.setVertexBuffer(0, vertexBuffer, bufferOffset); |
| renderPass.setPipeline(renderPipeline); |
| renderPass.draw(1); |
| renderPass.end(); |
| t.queue.submit([encoder.finish()]); |
| |
| t.checkBufferAndOutputTexture(vertexBuffer, bufferSize, outputTexture); |
| }); |
| |
| g.test('index_buffer') |
| .desc( |
| `Verify when we use a GPUBuffer as an index buffer just after the creation of that |
| GPUBuffer, all the contents in that GPUBuffer have been initialized to 0.` |
| ) |
| .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 16])) |
| .fn(t => { |
| const { bufferOffset } = t.params; |
| |
| const renderPipeline = t.createRenderPipelineForTest( |
| t.device.createShaderModule({ |
| code: ` |
| struct VertexOut { |
| @location(0) color : vec4<f32>, |
| @builtin(position) position : vec4<f32>, |
| }; |
| |
| @vertex |
| fn main(@builtin(vertex_index) VertexIndex : u32) -> VertexOut { |
| var output : VertexOut; |
| if (VertexIndex == 0u) { |
| output.color = vec4<f32>(0.0, 1.0, 0.0, 1.0); |
| } else { |
| output.color = vec4<f32>(1.0, 0.0, 0.0, 1.0); |
| } |
| output.position = vec4<f32>(0.0, 0.0, 0.0, 1.0); |
| return output; |
| }`, |
| }), |
| false |
| ); |
| |
| // The size of GPUBuffer must be at least 4. |
| const bufferSize = 4 + bufferOffset; |
| const indexBuffer = t.createBufferTracked({ |
| size: bufferSize, |
| usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_SRC, |
| }); |
| |
| const outputTexture = t.createTextureTracked({ |
| format: 'rgba8unorm', |
| size: [1, 1, 1], |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| }); |
| |
| const encoder = t.device.createCommandEncoder(); |
| const renderPass = encoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| view: outputTexture.createView(), |
| clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, |
| loadOp: 'clear', |
| storeOp: 'store', |
| }, |
| ], |
| }); |
| renderPass.setPipeline(renderPipeline); |
| renderPass.setIndexBuffer(indexBuffer, 'uint16', bufferOffset, 4); |
| renderPass.drawIndexed(1); |
| renderPass.end(); |
| t.queue.submit([encoder.finish()]); |
| |
| t.checkBufferAndOutputTexture(indexBuffer, bufferSize, outputTexture); |
| }); |
| |
| g.test('indirect_buffer_for_draw_indirect') |
| .desc( |
| `Verify when we use a GPUBuffer as an indirect buffer for drawIndirect() or |
| drawIndexedIndirect() just after the creation of that GPUBuffer, all the contents in that GPUBuffer |
| have been initialized to 0.` |
| ) |
| .params(u => |
| u.combine('test_indexed_draw', [true, false]).beginSubcases().combine('bufferOffset', [0, 16]) |
| ) |
| .fn(t => { |
| const { test_indexed_draw, bufferOffset } = t.params; |
| |
| const renderPipeline = t.createRenderPipelineForTest( |
| t.device.createShaderModule({ |
| code: ` |
| struct VertexOut { |
| @location(0) color : vec4<f32>, |
| @builtin(position) position : vec4<f32>, |
| }; |
| |
| @vertex fn main() -> VertexOut { |
| var output : VertexOut; |
| output.color = vec4<f32>(1.0, 0.0, 0.0, 1.0); |
| output.position = vec4<f32>(0.0, 0.0, 0.0, 1.0); |
| return output; |
| }`, |
| }), |
| false |
| ); |
| |
| const kDrawIndirectParametersSize = 16; |
| const kDrawIndexedIndirectParametersSize = 20; |
| const bufferSize = |
| Math.max(kDrawIndirectParametersSize, kDrawIndexedIndirectParametersSize) + bufferOffset; |
| const indirectBuffer = t.createBufferTracked({ |
| size: bufferSize, |
| usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.INDIRECT, |
| }); |
| |
| const outputTexture = t.createTextureTracked({ |
| format: 'rgba8unorm', |
| size: [1, 1, 1], |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| }); |
| |
| // Initialize outputTexture to green. |
| const encoder = t.device.createCommandEncoder(); |
| t.recordInitializeTextureColor(encoder, outputTexture, { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }); |
| |
| const renderPass = encoder.beginRenderPass({ |
| colorAttachments: [ |
| { |
| view: outputTexture.createView(), |
| loadOp: 'load', |
| storeOp: 'store', |
| }, |
| ], |
| }); |
| renderPass.setPipeline(renderPipeline); |
| |
| let indexBuffer = undefined; |
| if (test_indexed_draw) { |
| indexBuffer = t.createBufferTracked({ |
| size: 4, |
| usage: GPUBufferUsage.INDEX, |
| }); |
| renderPass.setIndexBuffer(indexBuffer, 'uint16'); |
| renderPass.drawIndexedIndirect(indirectBuffer, bufferOffset); |
| } else { |
| renderPass.drawIndirect(indirectBuffer, bufferOffset); |
| } |
| |
| renderPass.end(); |
| t.queue.submit([encoder.finish()]); |
| |
| // The indirect buffer should be lazily cleared to 0, so we actually draw nothing and the color |
| // attachment will keep its original color (green) after we end the render pass. |
| t.checkBufferAndOutputTexture(indirectBuffer, bufferSize, outputTexture); |
| }); |
| |
| g.test('indirect_buffer_for_dispatch_indirect') |
| .desc( |
| `Verify when we use a GPUBuffer as an indirect buffer for dispatchWorkgroupsIndirect() just |
| after the creation of that GPUBuffer, all the contents in that GPUBuffer have been initialized |
| to 0.` |
| ) |
| .paramsSubcasesOnly(u => u.combine('bufferOffset', [0, 16])) |
| .fn(t => { |
| const { bufferOffset } = t.params; |
| |
| const computePipeline = t.device.createComputePipeline({ |
| layout: 'auto', |
| compute: { |
| module: t.device.createShaderModule({ |
| code: ` |
| @group(0) @binding(0) var outImage : texture_storage_2d<rgba8unorm, write>; |
| |
| @compute @workgroup_size(1) fn main() { |
| textureStore(outImage, vec2<i32>(0, 0), vec4<f32>(1.0, 0.0, 0.0, 1.0)); |
| }`, |
| }), |
| entryPoint: 'main', |
| }, |
| }); |
| |
| const kDispatchIndirectParametersSize = 12; |
| const bufferSize = kDispatchIndirectParametersSize + bufferOffset; |
| const indirectBuffer = t.createBufferTracked({ |
| size: bufferSize, |
| usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.INDIRECT, |
| }); |
| |
| const outputTexture = t.createTextureTracked({ |
| format: 'rgba8unorm', |
| size: [1, 1, 1], |
| usage: |
| GPUTextureUsage.COPY_SRC | |
| GPUTextureUsage.RENDER_ATTACHMENT | |
| GPUTextureUsage.STORAGE_BINDING, |
| }); |
| |
| // Initialize outputTexture to green. |
| const encoder = t.device.createCommandEncoder(); |
| t.recordInitializeTextureColor(encoder, outputTexture, { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }); |
| |
| const bindGroup = t.device.createBindGroup({ |
| layout: computePipeline.getBindGroupLayout(0), |
| entries: [ |
| { |
| binding: 0, |
| resource: outputTexture.createView(), |
| }, |
| ], |
| }); |
| |
| // The indirect buffer should be lazily cleared to 0, so we actually don't execute the compute |
| // shader and the output texture should keep its original color (green). |
| const computePass = encoder.beginComputePass(); |
| computePass.setBindGroup(0, bindGroup); |
| computePass.setPipeline(computePipeline); |
| computePass.dispatchWorkgroupsIndirect(indirectBuffer, bufferOffset); |
| computePass.end(); |
| t.queue.submit([encoder.finish()]); |
| |
| // The indirect buffer should be lazily cleared to 0, so we actually draw nothing and the color |
| // attachment will keep its original color (green) after we end the compute pass. |
| t.checkBufferAndOutputTexture(indirectBuffer, bufferSize, outputTexture); |
| }); |