chromium/content/test/data/gpu/webgpu-unittest-utils.js

// 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.`]];
      }
    },
  };
}();