// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Test "enum" allows for specifying tests in webGpuUnitTests below.
const WebGpuUnitTestId = {
RenderTest: 'render-test',
RenderTestAsync: 'render-test-async',
ComputeTest: 'compute-test',
ComputeTestAsync: 'compute-test-async',
};
// Implements a set of simple standalone unit tests to test WebGPU without
// depending on the canvas. Each test returns a pair consisting of a bool
// indicating whether the test passed (true) or failed (false), and a
// potentially empty array of messages detailing why the test may have
// failed.
export const webGpuUnitTests = function() {
//////////////////////////////////////////////////////////////////////////////
// Private internal helpers
// Initializes the adapter and devices for webgpu usage.
const init = async function() {
const adapter = navigator.gpu && await navigator.gpu.requestAdapter();
if (!adapter) {
console.error('navigator.gpu && navigator.gpu.requestAdapter failed');
return [
null,
null,
['WebGPU was unavailable and/or requesting adapter failed.']
];
}
const device = await adapter.requestDevice();
if (!device) {
console.error('adapter.requestDevice() failed');
return [
adapter,
null,
['Failed to request a WebGPU device.']
];
}
return [adapter, device];
};
// Compares an actual array (a) to an expected one (e), returning [true, []]
// iff the type and contents of the arrays are equal, otherwise returning
// [false, [description]].
const compareArrays = function(e, a) {
if (e.constructor !== a.constructor) {
return [
false,
[`Expected type '${e.constructor.name}', got '${a.constructor.name}'.`]
];
}
if (e.length !== a.length) {
return [
false,
[`Expected length ${e.length}, got ${a.length}.`]
];
}
var equal = true;
for (var i = 0; i !== e.length; i++) {
if (e[i] != a[i]) {
success = equal;
}
}
return equal ?
[true, []] :
[false, [`Expected [${e.toString()}], got [${a.toString()}].`]];
}
// Render test base which allows for specifying whether to use async pipeline
// creation. Renders a single pixel texture, copies it to a buffer, and
// verifies.
const renderTestBase = async function(useAsync) {
const [adapter, device, errors] = await init();
if (!adapter || !device) {
return [false, errors];
}
// Create the WebGPU primitives and execute the rendering and buffer copy.
const buffer = device.createBuffer({
size: 4,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
const texture = device.createTexture({
format: 'rgba8unorm',
size: { width: 1, height: 1 },
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const view = texture.createView();
const pipelineDesc = {
layout: 'auto',
vertex: {
module: device.createShaderModule({
code: `
@vertex fn main(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -3.0),
vec2<f32>(3.0, 1.0),
vec2<f32>(-1.0, 1.0));
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
`,
}),
entryPoint: 'main',
},
fragment: {
module: device.createShaderModule({
code: `
@fragment fn main() -> @location(0) vec4<f32> {
return vec4<f32>(0.0, 1.0, 0.0, 1.0);
}
`,
}),
entryPoint: 'main',
targets: [{ format: 'rgba8unorm' }],
},
primitive: { topology: 'triangle-list' },
};
const pipeline = useAsync
? await device.createRenderPipelineAsync(pipelineDesc)
: device.createRenderPipeline(pipelineDesc);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view,
storeOp: 'store',
clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: 'clear',
},
],
});
pass.setPipeline(pipeline);
pass.draw(3);
pass.end();
encoder.copyTextureToBuffer(
{ texture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
{ buffer, bytesPerRow: 256 },
{ width: 1, height: 1, depthOrArrayLayers: 1 }
);
device.queue.submit([encoder.finish()]);
// Verify the contents of the buffer that the texture was copied into.
var success = true;
const expected = new Uint8Array([0x00, 0xff, 0x00, 0xff]);
await buffer.mapAsync(GPUMapMode.READ);
const actual = new Uint8Array(buffer.getMappedRange());
return compareArrays(expected, actual);
};
// Compute test base which allows for specifying whether to use async pipeline
// creation. Fills a buffer with global_invocation_id.x and verifies the
// contents of the buffer.
const computeTestBase = async function(useAsync) {
const [adapter, device, errors] = await init();
if (!adapter || !device) {
return [false, errors];
}
// Test constants.
const n = 16;
const size = n * 4;
// Create the WebGPU primitives and execute the compute and buffer copy.
const pipelineDesc = {
layout: 'auto',
compute: {
module: device.createShaderModule({
code: `
@group(0) @binding(0) var<storage, read_write> buffer: array<u32>;
@compute @workgroup_size(1u) fn main(
@builtin(global_invocation_id) id: vec3<u32>
) {
buffer[id.x] = id.x;
}
`,
}),
entryPoint: 'main',
},
};
const pipeline = useAsync
? await device.createComputePipelineAsync(pipelineDesc)
: device.createComputePipeline(pipelineDesc);
const buffer = device.createBuffer({
size,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const result = device.createBuffer({
size,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer } }],
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(n);
pass.end();
encoder.copyBufferToBuffer(buffer, 0, result, 0, size);
device.queue.submit([encoder.finish()]);
// Verify the contents of the buffer that was copied into.
var success = true;
const expected = new Uint32Array([...Array(n).keys()]);
await result.mapAsync(GPUMapMode.READ);
const actual = new Uint32Array(result.getMappedRange());
return compareArrays(expected, actual);
};
return {
////////////////////////////////////////////////////////////////////////////
// Actual unit tests
renderTest: async function() {
return await renderTestBase(false);
},
renderTestAsync: async function() {
return await renderTestBase(true);
},
computeTest: async function() {
return await computeTestBase(false);
},
computeTestAsync: async function() {
return await computeTestBase(true);
},
////////////////////////////////////////////////////////////////////////////
// Test driver
runTest: async function(testId) {
// Test running wrapper to prefix error messages with test name.
const wrapper = async function(testId, testFunc) {
const [success, errors] = await testFunc();
if (success) {
return [true, []];
}
return [
false,
[`WebGPU test '${testId}' failed with the following errors:`] +
errors.map(function(e) { return ' ' + e; })];
};
switch (testId) {
case WebGpuUnitTestId.RenderTest:
return await wrapper(testId, this.renderTest);
break;
case WebGpuUnitTestId.RenderTestAsync:
return await wrapper(testId, this.renderTestAsync);
break;
case WebGpuUnitTestId.ComputeTest:
return await wrapper(testId, this.computeTest);
break;
case WebGpuUnitTestId.ComputeTestAsync:
return await wrapper(testId, this.computeTestAsync);
break;
default:
// Just fail for any undefined tests.
return [false, [`Undefined WebGPU test '${testId}' specified.`]];
}
},
};
}();