Repository: denoland/deno
Pull request: fix(ext/webgpu): `GPUDevice.createTexture` works when the argument `descriptor.size` is an `Array`

fixes https://github.com/denoland/deno/issues/22733

## validation

BTW, Should we implement according to the spec?

The [spec](https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpudevice-createtexture) indicates `GPUDevice.createTexture` needs validations like `validate GPUExtent3D shape(descriptor.size).` and other.

Here is a minimal example of the validation example.

- index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <script>
        (async () => {
            const adapter = await navigator.gpu.requestAdapter();
            const device = await adapter.requestDevice();
            const texture = device.createTexture({
                label: "Capture",
                // this array length overflows the spec
                size: [256, 256, 1, 0],
                format: "rgba8unorm-srgb",
                usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
            });
        })()
    </script>
</body>
</html>
```

It runs on Google Chrome(122.0.6261.112), then console shows the message as follows.

```
Uncaught (in promise) TypeError: Failed to execute 'createTexture' on 'GPUDevice': A sequence of number used as a GPUExtent3D must have between 1 and 3 elements.
    at index.html:12:36
```

And, same JavaScript code throws no error by `deno 1.42.3`.

Changed files:
filename: ext/webgpu/01_webgpu.js
patch: @@ -1136,10 +1136,11 @@ class GPUDevice extends EventTarget {
       "Argument 1",
     );
     const device = assertDevice(this, prefix, "this");
+    // assign normalized size to descriptor due to createGPUTexture needs it
+    descriptor.size = normalizeGPUExtent3D(descriptor.size);
     const { rid, err } = op_webgpu_create_texture({
       deviceRid: device.rid,
       ...descriptor,
-      size: normalizeGPUExtent3D(descriptor.size),
     });
     device.pushError(err);
 
@@ -5501,6 +5502,16 @@ webidl.converters["GPUExtent3D"] = (V, opts) => {
   if (typeof V === "object") {
     const method = V[SymbolIterator];
     if (method !== undefined) {
+      // validate length of GPUExtent3D
+      const min = 1;
+      const max = 3;
+      if (V.length < min || V.length > max) {
+        throw webidl.makeException(
+          TypeError,
+          `A sequence of number used as a GPUExtent3D must have between ${min} and ${max} elements.`,
+          opts,
+        );
+      }
       return webidl.converters["sequence<GPUIntegerCoordinate>"](V, opts);
     }
     return webidl.converters["GPUExtent3DDict"](V, opts);
@@ -6836,6 +6847,15 @@ webidl.converters["GPUOrigin3D"] = (V, opts) => {
   if (typeof V === "object") {
     const method = V[SymbolIterator];
     if (method !== undefined) {
+      // validate length of GPUOrigin3D
+      const length = 3;
+      if (V.length > length) {
+        throw webidl.makeException(
+          TypeError,
+          `A sequence of number used as a GPUOrigin3D must have at most ${length} elements.`,
+          opts,
+        );
+      }
       return webidl.converters["sequence<GPUIntegerCoordinate>"](V, opts);
     }
     return webidl.converters["GPUOrigin3DDict"](V, opts);
@@ -6904,6 +6924,15 @@ webidl.converters["GPUOrigin2D"] = (V, opts) => {
   if (typeof V === "object") {
     const method = V[SymbolIterator];
     if (method !== undefined) {
+      // validate length of GPUOrigin2D
+      const length = 2;
+      if (V.length > length) {
+        throw webidl.makeException(
+          TypeError,
+          `A sequence of number used as a GPUOrigin2D must have at most ${length} elements.`,
+          opts,
+        );
+      }
       return webidl.converters["sequence<GPUIntegerCoordinate>"](V, opts);
     }
     return webidl.converters["GPUOrigin2DDict"](V, opts);
@@ -6989,6 +7018,15 @@ webidl.converters["GPUColor"] = (V, opts) => {
   if (typeof V === "object") {
     const method = V[SymbolIterator];
     if (method !== undefined) {
+      // validate length of GPUColor
+      const length = 4;
+      if (V.length !== length) {
+        throw webidl.makeException(
+          TypeError,
+          `A sequence of number used as a GPUColor must have exactly ${length} elements.`,
+          opts,
+        );
+      }
       return webidl.converters["sequence<double>"](V, opts);
     }
     return webidl.converters["GPUColorDict"](V, opts);

filename: tests/unit/webgpu_test.ts
patch: @@ -252,6 +252,268 @@ Deno.test(function getPreferredCanvasFormat() {
   assert(preferredFormat === "bgra8unorm" || preferredFormat === "rgba8unorm");
 });
 
+Deno.test({
+  ignore: isWsl || isLinuxOrMacCI,
+}, async function validateGPUColor() {
+  const adapter = await navigator.gpu.requestAdapter();
+  assert(adapter);
+  const device = await adapter.requestDevice();
+  assert(device);
+
+  const format = "rgba8unorm-srgb";
+  const encoder = device.createCommandEncoder();
+  const texture = device.createTexture({
+    size: [256, 256],
+    format,
+    usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+  });
+  const view = texture.createView();
+  const storeOp = "store";
+  const loadOp = "clear";
+
+  // values for validating GPUColor
+  const invalidSize = [0, 0, 0];
+
+  const msgIncludes =
+    "A sequence of number used as a GPUColor must have exactly 4 elements.";
+
+  // validate the argument of descriptor.colorAttachments[@@iterator].clearValue property's length of GPUCommandEncoder.beginRenderPass when its a sequence
+  // https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-beginrenderpass
+  assertThrows(
+    () =>
+      encoder.beginRenderPass({
+        colorAttachments: [
+          {
+            view,
+            storeOp,
+            loadOp,
+            clearValue: invalidSize,
+          },
+        ],
+      }),
+    TypeError,
+    msgIncludes,
+  );
+  const renderPass = encoder.beginRenderPass({
+    colorAttachments: [
+      {
+        view,
+        storeOp,
+        loadOp,
+        clearValue: [0, 0, 0, 1],
+      },
+    ],
+  });
+  // validate the argument of color length of GPURenderPassEncoder.setBlendConstant when its a sequence
+  // https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpurenderpassencoder-setblendconstant
+  assertThrows(
+    () => renderPass.setBlendConstant(invalidSize),
+    TypeError,
+    msgIncludes,
+  );
+
+  device.destroy();
+  const resources = Object.keys(Deno.resources());
+  Deno.close(Number(resources[resources.length - 1]));
+});
+
+Deno.test({
+  ignore: isWsl || isLinuxOrMacCI,
+}, async function validateGPUExtent3D() {
+  const adapter = await navigator.gpu.requestAdapter();
+  assert(adapter);
+  const device = await adapter.requestDevice();
+  assert(device);
+
+  const format = "rgba8unorm-srgb";
+  const encoder = device.createCommandEncoder();
+  const buffer = device.createBuffer({
+    size: new Uint32Array([1, 4, 3, 295]).byteLength,
+    usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
+  });
+  const usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC;
+  const texture = device.createTexture({
+    size: [256, 256],
+    format,
+    usage,
+  });
+
+  // values for validating GPUExtent3D
+  const belowSize: Array<number> = [];
+  const overSize = [256, 256, 1, 1];
+
+  const msgIncludes =
+    "A sequence of number used as a GPUExtent3D must have between 1 and 3 elements.";
+
+  // validate the argument of descriptor.size property's length of GPUDevice.createTexture when its a sequence
+  // https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpudevice-createtexture
+  assertThrows(
+    () => device.createTexture({ size: belowSize, format, usage }),
+    TypeError,
+    msgIncludes,
+  );
+  assertThrows(
+    () => device.createTexture({ size: overSize, format, usage }),
+    TypeError,
+    msgIncludes,
+  );
+  // validate the argument of copySize property's length of GPUCommandEncoder.copyBufferToTexture when its a sequence
+  // https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-copybuffertotexture
+  assertThrows(
+    () => encoder.copyBufferToTexture({ buffer }, { texture }, belowSize),
+    TypeError,
+    msgIncludes,
+  );
+  assertThrows(
+    () => encoder.copyBufferToTexture({ buffer }, { texture }, overSize),
+    TypeError,
+    msgIncludes,
+  );
+  // validate the argument of copySize property's length of GPUCommandEncoder.copyTextureToBuffer when its a sequence
+  // https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-copytexturetobuffer
+  assertThrows(
+    () => encoder.copyTextureToBuffer({ texture }, { buffer }, belowSize),
+    TypeError,
+    msgIncludes,
+  );
+  assertThrows(
+    () => encoder.copyTextureToBuffer({ texture }, { buffer }, overSize),
+    TypeError,
+    msgIncludes,
+  );
+  // validate the argument of copySize property's length of GPUCommandEncoder.copyTextureToTexture when its a sequence
+  // https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-copytexturetotexture
+  assertThrows(
+    () => encoder.copyTextureToTexture({ texture }, { texture }, belowSize),
+    TypeError,
+    msgIncludes,
+  );
+  assertThrows(
+    () => encoder.copyTextureToTexture({ texture }, { texture }, overSize),
+    TypeError,
+    msgIncludes,
+  );
+  const data = new Uint8Array([1 * 255, 1 * 255, 1 * 255, 1 * 255]);
+  // validate the argument of size property's length of GPUQueue.writeTexture when its a sequence
+  // https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpuqueue-writetexture
+  assertThrows(
+    () => device.queue.writeTexture({ texture }, data, {}, belowSize),
+    TypeError,
+    msgIncludes,
+  );
+  assertThrows(
+    () => device.queue.writeTexture({ texture }, data, {}, overSize),
+    TypeError,
+    msgIncludes,
+  );
+  // NOTE: GPUQueue.copyExternalImageToTexture needs to be validated the argument of copySize property's length when its a sequence, but it is not implemented yet
+
+  device.destroy();
+  const resources = Object.keys(Deno.resources());
+  Deno.close(Number(resources[resources.length - 1]));
+});
+
+Deno.test({
+  ignore: true,
+}, async function validateGPUOrigin2D() {
+  // NOTE: GPUQueue.copyExternalImageToTexture needs to be validated the argument of source.origin property's length when its a sequence, but it is not implemented yet
+});
+
+Deno.test({
+  ignore: isWsl || isLinuxOrMacCI,
+}, async function validateGPUOrigin3D() {
+  const adapter = await navigator.gpu.requestAdapter();
+  assert(adapter);
+  const device = await adapter.requestDevice();
+  assert(device);
+
+  const format = "rgba8unorm-srgb";
+  const encoder = device.createCommandEncoder();
+  const buffer = device.createBuffer({
+    size: new Uint32Array([1, 4, 3, 295]).byteLength,
+    usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
+  });
+  const usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC;
+  const size = [256, 256, 1];
+  const texture = device.createTexture({
+    size,
+    format,
+    usage,
+  });
+
+  // value for validating GPUOrigin3D
+  const overSize = [256, 256, 1, 1];
+
+  const msgIncludes =
+    "A sequence of number used as a GPUOrigin3D must have at most 3 elements.";
+
+  // validate the argument of destination.origin property's length of GPUCommandEncoder.copyBufferToTexture when its a sequence
+  // https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-copybuffertotexture
+  assertThrows(
+    () =>
+      encoder.copyBufferToTexture(
+        { buffer },
+        { texture, origin: overSize },
+        size,
+      ),
+    TypeError,
+    msgIncludes,
+  );
+  // validate the argument of source.origin property's length of GPUCommandEncoder.copyTextureToBuffer when its a sequence
+  // https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-copytexturetobuffer
+  assertThrows(
+    () =>
+      encoder.copyTextureToBuffer(
+        { texture, origin: overSize },
+        { buffer },
+        size,
+      ),
+    TypeError,
+    msgIncludes,
+  );
+  // validate the argument of source.origin property's length of GPUCommandEncoder.copyTextureToTexture when its a sequence
+  // https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-copytexturetotexture
+  assertThrows(
+    () =>
+      encoder.copyTextureToTexture(
+        { texture, origin: overSize },
+        { texture },
+        size,
+      ),
+    TypeError,
+    msgIncludes,
+  );
+  // validate the argument of destination.origin property's length of GPUCommandEncoder.copyTextureToTexture when its a sequence
+  assertThrows(
+    () =>
+      encoder.copyTextureToTexture(
+        { texture },
+        { texture, origin: overSize },
+        size,
+      ),
+    TypeError,
+    msgIncludes,
+  );
+  // validate the argument of destination.origin property's length of GPUQueue.writeTexture when its a sequence
+  // https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpuqueue-writetexture
+  assertThrows(
+    () =>
+      device.queue.writeTexture(
+        { texture, origin: overSize },
+        new Uint8Array([1 * 255, 1 * 255, 1 * 255, 1 * 255]),
+        {},
+        size,
+      ),
+    TypeError,
+    msgIncludes,
+  );
+  // NOTE: GPUQueue.copyExternalImageToTexture needs to be validated the argument of destination.origin property's length when its a sequence, but it is not implemented yet
+
+  device.destroy();
+  const resources = Object.keys(Deno.resources());
+  Deno.close(Number(resources[resources.length - 1]));
+});
+
 async function checkIsWsl() {
   return Deno.build.os === "linux" && await hasMicrosoftProcVersion();