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) -> @location(0) vec4 { 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 = { 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 }; @group(0) @binding(0) var ubo : UBO; @group(0) @binding(1) var outImage : texture_storage_2d; @compute @workgroup_size(1) fn main() { if (all(ubo.value == vec4(0u, 0u, 0u, 0u))) { textureStore(outImage, vec2(0, 0), vec4(0.0, 1.0, 0.0, 1.0)); } else { textureStore(outImage, vec2(0, 0), vec4(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 }; @group(0) @binding(0) var ssbo : SSBO; @group(0) @binding(1) var outImage : texture_storage_2d; @compute @workgroup_size(1) fn main() { if (all(ssbo.value == vec4(0u, 0u, 0u, 0u))) { textureStore(outImage, vec2(0, 0), vec4(0.0, 1.0, 0.0, 1.0)); } else { textureStore(outImage, vec2(0, 0), vec4(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 }; @group(0) @binding(0) var ssbo : SSBO; @group(0) @binding(1) var outImage : texture_storage_2d; @compute @workgroup_size(1) fn main() { if (all(ssbo.value == vec4(0u, 0u, 0u, 0u))) { textureStore(outImage, vec2(0, 0), vec4(0.0, 1.0, 0.0, 1.0)); } else { textureStore(outImage, vec2(0, 0), vec4(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, @builtin(position) position : vec4, }; @vertex fn main(@location(0) pos : vec4) -> VertexOut { var output : VertexOut; if (all(pos == vec4(0.0, 0.0, 0.0, 0.0))) { output.color = vec4(0.0, 1.0, 0.0, 1.0); } else { output.color = vec4(1.0, 0.0, 0.0, 1.0); } output.position = vec4(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, @builtin(position) position : vec4, }; @vertex fn main(@builtin(vertex_index) VertexIndex : u32) -> VertexOut { var output : VertexOut; if (VertexIndex == 0u) { output.color = vec4(0.0, 1.0, 0.0, 1.0); } else { output.color = vec4(1.0, 0.0, 0.0, 1.0); } output.position = vec4(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, @builtin(position) position : vec4, }; @vertex fn main() -> VertexOut { var output : VertexOut; output.color = vec4(1.0, 0.0, 0.0, 1.0); output.position = vec4(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; @compute @workgroup_size(1) fn main() { textureStore(outImage, vec2(0, 0), vec4(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); });