chromium/third_party/blink/web_tests/wpt_internal/webgpu/canvas_webgpu_transfer/webgpu-helpers.js

/**
 * Invokes `callback` with a valid GPUAdapter, GPUAdapterInfo and GPUDevice, or
 * causes an assertion.
 */
function with_webgpu(callback) {
  return navigator.gpu.requestAdapter().then((adapter) => {
    if (!(adapter instanceof GPUAdapter)) {
      assert_unreached('Failed to request WebGPU adapter.');
      return;
    }
    return adapter.requestAdapterInfo().then((adapterInfo) => {
      if (!(adapterInfo instanceof GPUAdapterInfo)) {
        assert_unreached('Failed to request WebGPU adapter info.');
        return;
      }
      return adapter.requestDevice().then((device) => {
        if (!(device instanceof GPUDevice)) {
          assert_unreached('Failed to request WebGPU device.');
          return;
        }
        return callback(adapter, adapterInfo, device);
      });
    });
  });
}

/** Returns true if we are running on a Mac with a SwiftShader GPU adapter. */
function isMacSwiftShader(adapterInfo) {
  assert_true(adapterInfo instanceof GPUAdapterInfo);
  return adapterInfo.architecture == 'swiftshader' &&
         navigator.platform.startsWith('Mac');
}

/** Returns true if we are running on Linux. */
function isLinux() {
  return navigator.platform.startsWith('Linux');
}

/** Returns the upper-left hand pixel from the texture in a buffer. */
function copyOnePixelFromTextureAndSubmit(device, tex) {
  // Make a buffer which will allow us to copy one pixel from the texture, and
  // later read that pixel back.
  const buf = device.createBuffer({usage: GPUBufferUsage.COPY_DST |
                                          GPUBufferUsage.MAP_READ,
                                   size: 4});

  // Copy one pixel out of our texture.
  const encoder = device.createCommandEncoder();
  encoder.copyTextureToBuffer({texture: tex}, {buffer: buf}, [1, 1]);
  device.queue.submit([encoder.finish()]);

  // Return the buffer in case the caller wants to use it.
  return buf;
}

/** Replaces the upper-left hand pixel in the texture with transparent black. */
function copyOnePixelToTextureAndSubmit(device, tex) {
  // Make a buffer which will allow us to copy one pixel to the texture.
  const buf = device.createBuffer({usage: GPUBufferUsage.COPY_SRC, size: 4});

  // Copy a pixel into the upper-left pixel of our texture. The buffer is never
  // initialized, so it's going to default to transparent black (all zeros).
  const encoder = device.createCommandEncoder();
  encoder.copyBufferToTexture({buffer: buf}, {texture: tex}, [1, 1]);
  device.queue.submit([encoder.finish()]);
}

/** Using the passed-in texture as a render attachment, clears the texture. */
function clearTextureToColor(device, tex, gpuColor) {
  const encoder = device.createCommandEncoder();
  const pass = encoder.beginRenderPass({
    colorAttachments: [{
       view: tex.createView(),
       loadOp: 'clear',
       storeOp: 'store',
       clearValue: gpuColor,
    }]
  });
  pass.end();
  device.queue.submit([encoder.finish()]);
}

/** Verifies that every pixel on a Canvas2D contains a particular RGBA color. */
function checkCanvasColor(ctx, expectedRGBA) {
  // The ImageData `data` array holds RGBA elements in uint8 format.
  const w = ctx.canvas.width;
  const h = ctx.canvas.height;
  const imageData = ctx.getImageData(0, 0, w, h);
  for (let idx = 0; idx < w * h * 4; idx += 4) {
    assert_array_equals([imageData.data[idx + 0],
                         imageData.data[idx + 1],
                         imageData.data[idx + 2],
                         imageData.data[idx + 3]],
                        expectedRGBA,
                        'RGBA @ ' + idx);
  }
}

/**
 * Returns true if the texture causes validation errors when it is read-from or
 * written-to. This is indicative of a destroyed texture.
 */
async function isTextureDestroyed(device, texture) {
  // Unfortunately, there isn't a simple way to test if a texture has been
  // destroyed or not in WebGPU. The best we can do is try to use the texture in
  // various ways and check for GPUValidationErrors. So we verify whether or not
  // we are able to read-from or write-to the texture. If we get GPU validation
  // errors for both, we consider it to be destroyed.
  device.pushErrorScope('validation');
  copyOnePixelFromTextureAndSubmit(device, texture);

  device.pushErrorScope('validation');
  copyOnePixelToTextureAndSubmit(device, texture);

  writeError = await device.popErrorScope();
  readError = await device.popErrorScope();

  return readError instanceof GPUValidationError &&
         writeError instanceof GPUValidationError;
}

/**
 * Creates a bind group which uses the passed-in texture as a resource.
 * This will cause a GPUValidationError if TEXTURE_BINDING usage isn't set.
 * The caller is responsible for pushing and popping an error scope.
 */
function createBindGroupUsingTexture(device, tex) {
  const layout = device.createBindGroupLayout({
    entries: [
      {
        binding: 1,
        visibility: GPUShaderStage.FRAGMENT,
        texture: {},
      },
    ],
  });

  const group = device.createBindGroup({
    layout: layout,
    entries: [
      {
        binding: 1,
        resource: tex.createView(),
      },
    ],
  });
}

/** getTextureFormat() should return RGBA8 or BGRA8 for a typical context. */
function test_getTextureFormat_rgba8(device, canvas) {
  const ctx = canvas.getContext('2d');
  assert_regexp_match(ctx.getTextureFormat(), /^rgba8unorm$|^bgra8unorm$/);
}

/** getTextureFormat() should return RGBA16F for a float16 context. */
function test_getTextureFormat_rgba16f(device, canvas) {
  const ctx = canvas.getContext('2d', {colorSpace: 'display-p3',
                                       pixelFormat: 'float16'});
  assert_equals(ctx.getTextureFormat(), 'rgba16float');
}

/**
 * transferToGPUTexture() should create a texture from an uninitialized canvas.
 */
function test_transferToGPUTexture_untouched_canvas(device, canvas) {
  // Leave the canvas untouched; it won't have a Canvas2DLayerBridge yet.

  // Next, call transferToGPUTexture.
  const ctx = canvas.getContext('2d');
  const tex = ctx.transferToGPUTexture({device: device,
                                         label: "hello, webgpu!"});

  // Confirm that we now have a GPU texture that matches our request.
  assert_true(tex instanceof GPUTexture, 'not a GPUTexture');
  assert_equals(tex.label, "hello, webgpu!");
  assert_equals(tex.width, canvas.width);
  assert_equals(tex.height, canvas.height);
}

/**
 * transferToGPUTexture() should create a texture from an initialized canvas.
 */
function test_transferToGPUTexture_initialized_canvas(device, canvas) {
  // Paint into the canvas to ensure it has a valid Canvas2DLayerBridge.
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = "#FFFFFF";
  ctx.fillRect(0, 0, 100, 50);

  // Next, call transferToGPUTexture.
  const tex = ctx.transferToGPUTexture({device: device,
                                         label: "hello, webgpu!"});

  // Confirm that we now have a GPU texture that matches our request.
  assert_true(tex instanceof GPUTexture, 'not a GPUTexture');
  assert_equals(tex.label, 'hello, webgpu!');
  assert_equals(tex.width, canvas.width);
  assert_equals(tex.height, canvas.height);
}

/**
 * transferToGPUTexture() texture should retain the contents of the canvas, and
 * readback works. Returns a promise.
 */
function test_transferToGPUTexture_texture_readback(device, canvas) {
  // Fill the canvas with a color containing distinct values in each channel.
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = "#4080C0";
  ctx.fillRect(0, 0, 50, 50);

  // Convert the canvas to a texture.
  const tex = ctx.transferToGPUTexture({device: device,
                                     usage: GPUTextureUsage.COPY_SRC});

  // Copy the top-left pixel from the texture into a buffer.
  const buf = copyOnePixelFromTextureAndSubmit(device, tex);

  // Map the buffer and read it back.
  return buf.mapAsync(GPUMapMode.READ).then(() => {
    const data = new Uint8Array(buf.getMappedRange());

    if (tex.format == 'rgba8unorm') {
      assert_array_equals(data, [64, 128, 192, 255]);
    } else {
      assert_equals(tex.format, 'bgra8unorm');
      assert_array_equals(data, [192, 128, 64, 255]);
    }
  });
};

/**
 * Calling transferBackFromGPUTexture() without any preceding call to
 * transferToGPUTexture should raise an exception.
 */
function test_transferBackFromGPUTexture_first_throws(device, canvas) {
  const ctx = canvas.getContext('2d');

  try {
    ctx.transferBackFromGPUTexture();
    assert_unreached('transferBackFromGPUTexture should have thrown.');
  } catch (ex) {
    assert_true(ex instanceof DOMException);
    assert_equals(ex.name, 'InvalidStateError');
  }
}

/**
 * Unbalanced calls to transferToGPUTexture() will destroy the old WebGPU access
 * texture.
 */
async function test_transferToGPUTexture_unbalanced_access(
      adapterInfo, device, canvas) {
  // Skip this test on Mac Swiftshader.
  if (isMacSwiftShader(adapterInfo)) {
    return;
  }

  // Begin a WebGPU access session.
  const ctx = canvas.getContext('2d');
  const tex1 = ctx.transferToGPUTexture({device: device,
                                      usage: GPUTextureUsage.COPY_DST |
                                             GPUTextureUsage.COPY_SRC});

  // Start a second WebGPU access session, destroying the first texture.
  const tex2 = ctx.transferToGPUTexture({device: device,
                                      usage: GPUTextureUsage.COPY_DST |
                                             GPUTextureUsage.COPY_SRC});

  // Only the second texture should remain in an undestroyed state.
  assert_true(await isTextureDestroyed(device, tex1));
  assert_false(await isTextureDestroyed(device, tex2));
}

/**
 * transferToGPUTexture() should allow repeated calls after a call to
 * transferBackFromGPUTexture().
 */
function test_transferToGPUTexture_balanced_access(device, canvas) {
  const ctx = canvas.getContext('2d');

  // Begin and end a WebGPU access session several times.
  for (let count = 0; count < 10; ++count) {
    const tex = ctx.transferToGPUTexture({device: device});
    ctx.transferBackFromGPUTexture();
  }
}

/**
 * transferBackFromGPUTexture() should preserve texture changes on the 2D
 * canvas.
 */
function test_transferBackFromGPUTexture_canvas_readback(adapterInfo, device,
                                                     canvas, canvasFormat) {
  // Skip this test on Mac Swiftshader due to "Invalid Texture" errors.
  if (isMacSwiftShader(adapterInfo)) {
    return;
  }

  // Convert the canvas to a texture.
  const ctx = canvas.getContext('2d', canvasFormat);
  const tex = ctx.transferToGPUTexture({device: device});

  // Fill the texture with a color containing distinct values in each channel.
  clearTextureToColor(device, tex,
                      { r: 64 / 255, g: 128 / 255, b: 192 / 255, a: 1.0 });

  // Finish our WebGPU pass and restore the canvas.
  ctx.transferBackFromGPUTexture(tex);

  // Verify that the canvas contains our chosen color across every pixel.
  checkCanvasColor(ctx, [0x40, 0x80, 0xC0, 0xFF]);
}

/**
 * transferBackFromGPUTexture() should be a no-op if the canvas context is lost.
 */
function test_transferBackFromGPUTexture_context_lost(device, canvas) {
  // Begin a WebGPU access session.
  const ctx = canvas.getContext('2d');
  const tex = ctx.transferToGPUTexture({device: device});

  // Forcibly lose the canvas context.
  assert_true(!!window.internals, 'Internal APIs unavailable.');
  internals.forceLoseCanvasContext(canvas, '2d');

  // End the WebGPU access session. Nothing should be thrown.
  try {
    ctx.transferBackFromGPUTexture(tex);
  } catch {
    assert_unreached('transferBackFromGPUTexture should be safe when context ' +
                     'is lost.');
  }
}

/**
 * transferBackFromGPUTexture() should cause the GPUTexture returned by
 * transferToGPUTexture() to enter a destroyed state.
 */
async function test_transferBackFromGPUTexture_destroys_texture(
    device, canvas) {
  // Briefly begin a WebGPU access session.
  const ctx = canvas.getContext('2d');
  const tex = ctx.transferToGPUTexture({device: device,
                                     usage: GPUTextureUsage.COPY_SRC |
                                            GPUTextureUsage.COPY_DST});
  ctx.transferBackFromGPUTexture();

  // `tex` should be in a destroyed state.
  assert_true(await isTextureDestroyed(device, tex));
}

/** Resizing a canvas during WebGPU access should destroy the texture. */
async function test_canvas_reset_destroys_texture(adapterInfo, device, canvas,
                                                  resetType) {
  // Skip this test on Mac Swiftshader due to "Invalid Texture" errors.
  if (isMacSwiftShader(adapterInfo)) {
    return;
  }

  // Begin a WebGPU access session.
  const ctx = canvas.getContext('2d');
  const tex = ctx.transferToGPUTexture({device: device,
                                     usage: GPUTextureUsage.COPY_SRC});

  // Reset the canvas. This should abort the WebGPU access session.
  if (resetType == 'resize') {
    canvas.width = canvas.width;
  } else {
    assert_equals(resetType, 'api');
    ctx.reset();
  }

  // The canvas' GPUTexture should appear to be destroyed.
  assert_true(await isTextureDestroyed(device, tex));
}

/**
 * transferToGPUTexture() should create a texture which honors the requested
 * usage flags.
 */
function test_transferToGPUTexture_usage_flags(adapter, adapterInfo, device,
                                           canvas) {
  // Create a base-case promise that just resolves immediately.
  let promise = new Promise((resolve, reject) => { resolve(true); });

  // Skip this test on Mac Swiftshader due to "Invalid Texture" errors.
  if (isMacSwiftShader(adapterInfo)) {
    return promise;
  }

  // Declare a helper function which tests individual usage flags.
  const testOneUsageFlag = function(device, canvas, usageToEnable,
                                    usageToTest) {
    const ctx = canvas.getContext('2d');
    const tex = ctx.transferToGPUTexture({device: device,
                                           usage: usageToEnable});

    // Check that the GPUTexture's usage flags match our requested flags.
    assert_equals(tex.usage, usageToEnable);

    // Confirm that we _actually_ have the requested usage by performing an
    // action requiring that usage, and verifying no GPUValidationError occurs.
    device.pushErrorScope('validation');

    if (usageToTest & GPUTextureUsage.COPY_SRC) {
      copyOnePixelFromTextureAndSubmit(device, tex);
    }
    if (usageToTest & GPUTextureUsage.COPY_DST) {
      copyOnePixelToTextureAndSubmit(device, tex);
    }
    if (usageToTest & GPUTextureUsage.TEXTURE_BINDING) {
      createBindGroupUsingTexture(device, tex);
    }
    if (usageToTest & GPUTextureUsage.RENDER_ATTACHMENT) {
      clearTextureToColor(device, tex, { r: 1.0, g: 1.0, b: 1.0, a: 1.0 });
    }

    return device.popErrorScope().then((errors) => {
      if (usageToTest == usageToEnable) {
        assert_equals(errors, null, 'enabled ' + usageToEnable +
                                    ', tested ' + usageToTest);
      } else {
        assert_not_equals(errors, null, 'enabled ' + usageToEnable +
                                        ', tested ' + usageToTest);
      }
      ctx.transferBackFromGPUTexture();
    });
  };

  // Build up a chain of promises which test each TextureUsage flag.
  const supportedUsageFlags = [GPUTextureUsage.COPY_SRC,
                               GPUTextureUsage.COPY_DST,
                               GPUTextureUsage.TEXTURE_BINDING,
                               GPUTextureUsage.RENDER_ATTACHMENT];

  for (const usageToEnable of supportedUsageFlags) {
    for (const usageToTest of supportedUsageFlags) {
      promise = promise.then(() => {
        return with_webgpu((adapter, adapterInfo, device) => {
          return testOneUsageFlag(device, canvas, usageToEnable, usageToTest);
        });
      });
    }
  }

  return promise;
}

/**
 * WebGPU access should work normally on a canvas which has been downgraded to
 * CPU, transparently bringing it back to the GPU.
 */
function test_canvas_works_after_cpu_downgrade(adapterInfo, device, canvas) {
  const ctx = canvas.getContext('2d');

  // Force the canvas into software.
  internals.disableCanvasAcceleration(canvas);

  // Perform a GPU clear to demonstrate that the canvas still works in WebGPU.
  const tex = ctx.transferToGPUTexture({device: device});
  clearTextureToColor(device, tex, { r: 0.0, g: 1.0, b: 0.0, a: 1.0 });
  ctx.transferBackFromGPUTexture();

  // Verify that WebGPU did its job; every pixel on the canvas should be green.
  // TODO(crbug.com/40218893): Linux with an Intel GPU returns the wrong color.
  if (!isLinux()) {
    checkCanvasColor(ctx, [0x00, 0xFF, 0x00, 0xFF]);
  }
}

/**
 * WebGPU access should not allow transferring back after drawing to the canvas.
 */
function test_canvas_disallows_transfer_back_after_draw(device, canvas) {
  const ctx = canvas.getContext('2d');

  // Transfer the GPU texture off the context and clear it to red.
  // This red is never transferred back, so it doesn't matter.
  const tex = ctx.transferToGPUTexture({device: device});
  clearTextureToColor(device, tex, { r: 1.0, g: 0.0, b: 0.0, a: 1.0 });

  // Draw a green rectangle using Canvas2D commands. Only cover the top half of
  // the canvas. The bottom half will be untouched by drawing commands. Note
  // that there should be no red pixels, despite the above call to
  // `clearTextureToColor`; the canvas should be treated as newly-initialized.
  const w = ctx.canvas.width;
  const h = ctx.canvas.height;
  ctx.fillStyle = "#00FF00";
  ctx.fillRect(0, 0, w, h / 2);

  // Attempting to transfer the GPU texture back should raise an exception.
  try {
    ctx.transferBackFromGPUTexture();
    assert_unreached('transferBackFromGPUTexture should have thrown.');
  } catch (ex) {
    assert_true(ex instanceof DOMException);
    assert_equals(ex.name, 'InvalidStateError');
  }

  // The canvas' image should contain the rectangle, and should have zero red
  // or blue pixels.
  const imageData = ctx.getImageData(0, 0, w, h);
  for (let idx = 0; idx < w * h * 4; idx += 4) {
    assert_equals(imageData.data[idx + 0], 0x00,
                  'Expected no red in the canvas image.');
    assert_equals(imageData.data[idx + 2], 0x00,
                  'Expected no blue in the canvas image.');
  }

  // We should be able to find some green pixels from the rectangle.
  let foundGreen = false;
  for (let idx = 0; idx < w * h * 4; idx += 4) {
    if (imageData.data[idx + 1] == 0xFF) {
      foundGreen = true;
      break;
    }
  }
  assert_true(foundGreen, 'Expected some green in the canvas image.');
}