blob: 08e12702eba3e69fb5df000234bea2f4459f36ec [file] [log] [blame]
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);
});