Repository: denoland/deno
Pull request: BREAKING(webgpu/unstable): move `width` and `height` options to `UnsafeWindowSurface` constructor

Fixes https://github.com/denoland/deno/issues/23508

`width` and `height` are required to configure the wgpu surface because Deno is headless and depends on user to create a window. The options were non-standard extension of `GPUCanvasConfiguration#configure`.

This PR adds a required options parameter with the `width` and `height` options to `Deno.UnsafeWindowSurface` constructor.

```typescript
// Old, non-standard extension of GPUCanvasConfiguration
const surface = new Deno.UnsafeWindowSurface("x11", displayHandle, windowHandle);

const context  = surface.getContext();
context.configure({ width: 600, height: 800, /* ... */ });
```

```typescript
// New
const surface = new Deno.UnsafeWindowSurface({
  system: "x11",
  windowHandle,
  displayHandle,
  width: 600,
  height: 800,
});

const context  = surface.getContext();
context.configure({ /* ... */ });
```

Changed files:
filename: cli/tsc/dts/lib.deno.unstable.d.ts
patch: @@ -28,9 +28,13 @@ declare namespace Deno {
    */
   export class UnsafeWindowSurface {
     constructor(
-      system: "cocoa" | "win32" | "x11" | "wayland",
-      windowHandle: Deno.PointerValue<unknown>,
-      displayHandle: Deno.PointerValue<unknown>,
+      options: {
+        system: "cocoa" | "win32" | "x11" | "wayland";
+        windowHandle: Deno.PointerValue<unknown>;
+        displayHandle: Deno.PointerValue<unknown>;
+        width: number;
+        height: number;
+      },
     );
     getContext(context: "webgpu"): GPUCanvasContext;
     present(): void;

filename: cli/tsc/dts/lib.deno_webgpu.d.ts
patch: @@ -1372,8 +1372,6 @@ declare interface GPUCanvasConfiguration {
   viewFormats?: GPUTextureFormat[];
   colorSpace?: "srgb" | "display-p3";
   alphaMode?: GPUCanvasAlphaMode;
-  width: number;
-  height: number;
 }
 /** @category GPU */
 declare interface GPUCanvasContext {

filename: ext/webgpu/01_webgpu.js
patch: @@ -7434,16 +7434,6 @@ const dictMembersGPUCanvasConfiguration = [
     key: "presentMode",
     converter: webidl.converters["GPUPresentMode"],
   },
-  {
-    key: "width",
-    converter: webidl.converters["long"],
-    required: true,
-  },
-  {
-    key: "height",
-    converter: webidl.converters["long"],
-    required: true,
-  },
   {
     key: "viewFormats",
     converter: webidl.createSequenceConverter(

filename: ext/webgpu/02_surface.js
patch: @@ -29,13 +29,16 @@ const _configuration = Symbol("[[configuration]]");
 const _canvas = Symbol("[[canvas]]");
 const _currentTexture = Symbol("[[currentTexture]]");
 const _present = Symbol("[[present]]");
+const _dim = Symbol("[[dimensions]]");
+
 class GPUCanvasContext {
   /** @type {number} */
   [_surfaceRid];
   [_configuration];
   [_canvas];
   /** @type {GPUTexture | undefined} */
   [_currentTexture];
+  [_dim];
 
   get canvas() {
     webidl.assertBranded(this, GPUCanvasContextPrototype);
@@ -69,8 +72,8 @@ class GPUCanvasContext {
       format: configuration.format,
       viewFormats: configuration.viewFormats,
       usage: configuration.usage,
-      width: configuration.width,
-      height: configuration.height,
+      width: this[_dim].width,
+      height: this[_dim].height,
       alphaMode: configuration.alphaMode,
     });
 
@@ -110,8 +113,8 @@ class GPUCanvasContext {
     const texture = createGPUTexture(
       {
         size: {
-          width: this[_configuration].width,
-          height: this[_configuration].height,
+          width: this[_dim].width,
+          height: this[_dim].height,
           depthOrArrayLayers: 1,
         },
         mipLevelCount: 1,
@@ -163,6 +166,8 @@ function createCanvasContext(options) {
   const canvasContext = webidl.createBranded(GPUCanvasContext);
   canvasContext[_surfaceRid] = options.surfaceRid;
   canvasContext[_canvas] = options.canvas;
+  canvasContext[_dim] = { width: options.width, height: options.height };
+
   return canvasContext;
 }
 
@@ -172,16 +177,34 @@ function createCanvasContext(options) {
 class UnsafeWindowSurface {
   #ctx;
   #surfaceRid;
+  #options;
 
-  constructor(system, win, display) {
-    this.#surfaceRid = op_webgpu_surface_create(system, win, display);
+  constructor(options) {
+    if (typeof options !== "object") {
+      throw new TypeError("options must be provided.");
+    }
+    if (
+      typeof options.width !== "number" || typeof options.height !== "number"
+    ) {
+      throw new TypeError("width and height must be provided.");
+    }
+
+    this.#surfaceRid = op_webgpu_surface_create(
+      options.system,
+      options.windowHandle,
+      options.displayHandle,
+    );
+    this.#options = options;
   }
 
   getContext(context) {
     if (context !== "webgpu") {
       throw new TypeError("Only 'webgpu' context is supported");
     }
-    this.#ctx = createCanvasContext({ surfaceRid: this.#surfaceRid });
+    this.#ctx = createCanvasContext({
+      surfaceRid: this.#surfaceRid,
+      ...this.#options,
+    });
     return this.#ctx;
   }
 

filename: tests/unit/webgpu_test.ts
patch: @@ -236,13 +236,32 @@ Deno.test({
 
   assertThrows(
     () => {
-      new Deno.UnsafeWindowSurface("cocoa", null, null);
+      new Deno.UnsafeWindowSurface({
+        system: "cocoa",
+        windowHandle: null,
+        displayHandle: null,
+        width: 0,
+        height: 0,
+      });
     },
   );
 
   device.destroy();
 });
 
+Deno.test(function webgpuWindowSurfaceNoWidthHeight() {
+  assertThrows(
+    () => {
+      // @ts-expect-error width and height are required
+      new Deno.UnsafeWindowSurface({
+        system: "x11",
+        windowHandle: null,
+        displayHandle: null,
+      });
+    },
+  );
+});
+
 Deno.test(function getPreferredCanvasFormat() {
   const preferredFormat = navigator.gpu.getPreferredCanvasFormat();
   assert(preferredFormat === "bgra8unorm" || preferredFormat === "rgba8unorm");