From 920aed35c7bd8bc0fe06b36014c64083c563e04b Mon Sep 17 00:00:00 2001 From: William Candillon Date: Tue, 24 Sep 2024 17:35:52 +0200 Subject: [PATCH] OffscreenCanvas (#127) --- apps/paper/src/Tests.tsx | 13 +- apps/paper/src/components/DrawingContext.ts | 11 -- .../src/components/NativeDrawingContext.ts | 52 ------ packages/webgpu/.eslintrc | 3 +- packages/webgpu/src/Offscreen.ts | 158 ++++++++++++++++++ .../src/__tests__/ExternalTexture.spec.ts | 8 +- packages/webgpu/src/__tests__/Texture.spec.ts | 10 +- .../__tests__/components/DrawingContext.ts | 11 -- .../src/__tests__/demos/ABuffer.spec.ts | 29 ++-- .../webgpu/src/__tests__/demos/Blur.spec.ts | 3 +- .../webgpu/src/__tests__/demos/Cube.spec.ts | 28 ++-- .../src/__tests__/demos/FractalCube.spec.ts | 11 +- .../__tests__/demos/OcclusionQuery.spec.ts | 6 +- .../src/__tests__/demos/RenderBundles.spec.ts | 7 +- .../src/__tests__/demos/Triangle.spec.ts | 36 +++- packages/webgpu/src/__tests__/setup.ts | 27 ++- packages/webgpu/src/index.tsx | 1 + 17 files changed, 279 insertions(+), 135 deletions(-) delete mode 100644 apps/paper/src/components/DrawingContext.ts delete mode 100644 apps/paper/src/components/NativeDrawingContext.ts create mode 100644 packages/webgpu/src/Offscreen.ts delete mode 100644 packages/webgpu/src/__tests__/components/DrawingContext.ts diff --git a/apps/paper/src/Tests.tsx b/apps/paper/src/Tests.tsx index 95f7de206..dc5ea1a32 100644 --- a/apps/paper/src/Tests.tsx +++ b/apps/paper/src/Tests.tsx @@ -2,19 +2,19 @@ import React, { useEffect, useState } from "react"; import { Dimensions, Text, View, Image } from "react-native"; -import "react-native-wgpu"; +import { GPUOffscreenCanvas } from "react-native-wgpu"; import { mat4, vec3, mat3 } from "wgpu-matrix"; import { useClient } from "./useClient"; import { cubeVertexArray } from "./components/cube"; import { redFragWGSL, triangleVertWGSL } from "./Triangle/triangle"; -import { NativeDrawingContext } from "./components/NativeDrawingContext"; import type { AssetProps } from "./components/useAssets"; import { Texture } from "./components/Texture"; export const CI = process.env.CI === "true"; const { width } = Dimensions.get("window"); +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); const useWebGPU = () => { const [adapter, setAdapter] = useState(null); @@ -42,7 +42,13 @@ export const Tests = ({ assets: { di3D, saturn, moon } }: AssetProps) => { client.onmessage = (e) => { const tree = JSON.parse(e.data); if (tree.code) { - const ctx = new NativeDrawingContext(device, 1024, 1024); + const canvas = new GPUOffscreenCanvas(1024, 1024); + const ctx = canvas.getContext("webgpu")!; + ctx.configure({ + device, + format: presentationFormat, + alphaMode: "premultiplied", + }); const result = eval( `(function Main() { return (${tree.code})(this.ctx); @@ -67,6 +73,7 @@ export const Tests = ({ assets: { di3D, saturn, moon } }: AssetProps) => { redFragWGSL, }, ctx, + canvas: ctx.canvas, mat4, vec3, mat3, diff --git a/apps/paper/src/components/DrawingContext.ts b/apps/paper/src/components/DrawingContext.ts deleted file mode 100644 index cd9f8b0e6..000000000 --- a/apps/paper/src/components/DrawingContext.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface DrawingContext { - width: number; - height: number; - getCurrentTexture(): GPUTexture; - getImageData(): Promise<{ - data: number[]; - width: number; - height: number; - format: string; - }>; -} diff --git a/apps/paper/src/components/NativeDrawingContext.ts b/apps/paper/src/components/NativeDrawingContext.ts deleted file mode 100644 index 1756256cf..000000000 --- a/apps/paper/src/components/NativeDrawingContext.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { DrawingContext } from "./DrawingContext"; - -export class NativeDrawingContext implements DrawingContext { - private texture: GPUTexture; - private buffer: GPUBuffer; - constructor( - public device: GPUDevice, - public width: number, - public height: number, - ) { - const bytesPerRow = this.width * 4; - this.texture = device.createTexture({ - size: [width, height], - format: navigator.gpu.getPreferredCanvasFormat(), - usage: - GPUTextureUsage.RENDER_ATTACHMENT | - GPUTextureUsage.COPY_SRC | - GPUTextureUsage.TEXTURE_BINDING, - }); - this.buffer = device.createBuffer({ - size: bytesPerRow * this.height, - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, - }); - } - - getCurrentTexture() { - return this.texture; - } - getImageData() { - const commandEncoder = this.device.createCommandEncoder(); - const bytesPerRow = this.width * 4; - commandEncoder.copyTextureToBuffer( - { texture: this.texture }, - { buffer: this.buffer, bytesPerRow }, - [this.width, this.height], - ); - this.device.queue.submit([commandEncoder.finish()]); - - return this.buffer.mapAsync(GPUMapMode.READ).then(() => { - const arrayBuffer = this.buffer.getMappedRange(); - const uint8Array = new Uint8Array(arrayBuffer); - const data = Array.from(uint8Array); - this.buffer.unmap(); - return { - data, - width: this.width, - height: this.height, - format: navigator.gpu.getPreferredCanvasFormat(), - }; - }); - } -} diff --git a/packages/webgpu/.eslintrc b/packages/webgpu/.eslintrc index dd155609e..99a8bcdea 100644 --- a/packages/webgpu/.eslintrc +++ b/packages/webgpu/.eslintrc @@ -3,6 +3,7 @@ "ignorePatterns": ["**/*/components/meshes"], "rules": { "no-bitwise": "off", - "@typescript-eslint/no-require-imports": "off" + "@typescript-eslint/no-require-imports": "off", + "no-dupe-class-members": "off" } } \ No newline at end of file diff --git a/packages/webgpu/src/Offscreen.ts b/packages/webgpu/src/Offscreen.ts new file mode 100644 index 000000000..3d5d65c96 --- /dev/null +++ b/packages/webgpu/src/Offscreen.ts @@ -0,0 +1,158 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export class GPUOffscreenCanvas implements OffscreenCanvas { + width: number; + height: number; + oncontextlost: ((this: OffscreenCanvas, ev: Event) => any) | null = null; + oncontextrestored: ((this: OffscreenCanvas, ev: Event) => any) | null = null; + + private context: GPUOffscreenCanvasContext; + + constructor(width: number, height: number) { + this.width = width; + this.height = height; + this.context = new GPUOffscreenCanvasContext(this); + } + + convertToBlob(_options?: ImageEncodeOptions): Promise { + // Implementation for converting the canvas content to a Blob + throw new Error("Method not implemented."); + } + + // Overloaded method signatures + getContext( + contextId: "2d", + options?: any, + ): OffscreenCanvasRenderingContext2D | null; + getContext( + contextId: "bitmaprenderer", + options?: any, + ): ImageBitmapRenderingContext | null; + getContext(contextId: "webgl", options?: any): WebGLRenderingContext | null; + getContext(contextId: "webgl2", options?: any): WebGL2RenderingContext | null; + getContext( + contextId: OffscreenRenderingContextId, + options?: any, + ): OffscreenRenderingContext | null; + getContext(contextId: "webgpu"): GPUCanvasContext | null; + getContext( + contextId: unknown, + _options?: any, + ): OffscreenRenderingContext | GPUCanvasContext | null { + if (contextId === "webgpu") { + return this.context; + } + // Implement other context types if necessary + return null; + } + + transferToImageBitmap(): ImageBitmap { + // Implementation for transferring the canvas content to an ImageBitmap + throw new Error("Method not implemented."); + } + + addEventListener( + _type: K, + _listener: (this: OffscreenCanvas, ev: OffscreenCanvasEventMap[K]) => any, + _options?: boolean | AddEventListenerOptions, + ): void { + // Event listener implementation + throw new Error("Method not implemented."); + } + + removeEventListener( + _type: K, + _listener: (this: OffscreenCanvas, ev: OffscreenCanvasEventMap[K]) => any, + _options?: boolean | EventListenerOptions, + ): void { + // Remove event listener implementation + throw new Error("Method not implemented."); + } + + dispatchEvent(_event: Event): boolean { + // Event dispatch implementation + throw new Error("Method not implemented."); + } + + getImageData() { + const device = this.context.getDevice(); + const texture = this.context.getTexture(); + const commandEncoder = device.createCommandEncoder(); + const bytesPerRow = this.width * 4; + const buffer = device.createBuffer({ + size: bytesPerRow * this.height, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + commandEncoder.copyTextureToBuffer( + { texture: texture }, + { buffer: buffer, bytesPerRow }, + [this.width, this.height], + ); + device.queue.submit([commandEncoder.finish()]); + + return buffer.mapAsync(GPUMapMode.READ).then(() => { + const arrayBuffer = buffer.getMappedRange(); + const uint8Array = new Uint8Array(arrayBuffer); + const data = Array.from(uint8Array); + buffer.unmap(); + return { + data, + width: this.width, + height: this.height, + format: navigator.gpu.getPreferredCanvasFormat(), + }; + }); + } +} + +class GPUOffscreenCanvasContext implements GPUCanvasContext { + __brand = "GPUCanvasContext" as const; + + private textureFormat: GPUTextureFormat = "bgra8unorm"; + private texture: GPUTexture | null = null; + private device: GPUDevice | null = null; + + constructor(public readonly canvas: OffscreenCanvas) {} + + getDevice() { + if (!this.device) { + throw new Error("Device is not configured."); + } + return this.device; + } + + getTexture() { + if (!this.texture) { + throw new Error("Texture is not configured"); + } + return this.texture; + } + + configure(config: GPUCanvasConfiguration) { + // Configure the canvas context with the device and format + this.device = config.device; + this.texture = config.device.createTexture({ + size: [this.canvas.width, this.canvas.height], + format: this.textureFormat, + usage: + GPUTextureUsage.RENDER_ATTACHMENT | + GPUTextureUsage.COPY_SRC | + GPUTextureUsage.TEXTURE_BINDING, + }); + return undefined; + } + + unconfigure() { + // Unconfigure the canvas context + if (this.texture) { + this.texture.destroy(); + } + return undefined; + } + + getCurrentTexture(): GPUTexture { + if (!this.texture) { + throw new Error("Texture is not configured"); + } + return this.texture; + } +} diff --git a/packages/webgpu/src/__tests__/ExternalTexture.spec.ts b/packages/webgpu/src/__tests__/ExternalTexture.spec.ts index 040da402c..9e293a0aa 100644 --- a/packages/webgpu/src/__tests__/ExternalTexture.spec.ts +++ b/packages/webgpu/src/__tests__/ExternalTexture.spec.ts @@ -3,7 +3,7 @@ import { checkImage, client, encodeImage } from "./setup"; describe("External Textures", () => { it("Simple (1)", async () => { const result = await client.eval( - ({ gpu, device, ctx, urls: { fTexture } }) => { + ({ gpu, device, ctx, canvas, urls: { fTexture } }) => { const module = device.createShaderModule({ label: "our hardcoded textured quad shaders", code: /* wgsl */ ` @@ -131,7 +131,7 @@ describe("External Textures", () => { device.queue.submit([commandBuffer]); } render(); - return ctx.getImageData(); + return canvas.getImageData(); }); }); }); @@ -143,7 +143,7 @@ describe("External Textures", () => { }); it("Simple (2)", async () => { const result = await client.eval( - ({ gpu, device, ctx, urls: { fTexture } }) => { + ({ gpu, device, ctx, canvas, urls: { fTexture } }) => { const module = device.createShaderModule({ label: "our hardcoded textured quad shaders", code: /* wgsl */ ` @@ -271,7 +271,7 @@ describe("External Textures", () => { device.queue.submit([commandBuffer]); } render(); - return ctx.getImageData(); + return canvas.getImageData(); }); }); }); diff --git a/packages/webgpu/src/__tests__/Texture.spec.ts b/packages/webgpu/src/__tests__/Texture.spec.ts index 2cd08f75e..8f7680800 100644 --- a/packages/webgpu/src/__tests__/Texture.spec.ts +++ b/packages/webgpu/src/__tests__/Texture.spec.ts @@ -138,7 +138,13 @@ describe("Texture", () => { }); it("Create texture and reads it", async () => { const result = await client.eval( - ({ device, shaders: { triangleVertWGSL, redFragWGSL }, gpu, ctx }) => { + ({ + device, + shaders: { triangleVertWGSL, redFragWGSL }, + gpu, + ctx, + canvas, + }) => { const pipeline = device.createRenderPipeline({ layout: "auto", vertex: { @@ -182,7 +188,7 @@ describe("Texture", () => { passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - return ctx.getImageData(); + return canvas.getImageData(); }, ); const image = encodeImage(result); diff --git a/packages/webgpu/src/__tests__/components/DrawingContext.ts b/packages/webgpu/src/__tests__/components/DrawingContext.ts deleted file mode 100644 index cd9f8b0e6..000000000 --- a/packages/webgpu/src/__tests__/components/DrawingContext.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface DrawingContext { - width: number; - height: number; - getCurrentTexture(): GPUTexture; - getImageData(): Promise<{ - data: number[]; - width: number; - height: number; - format: string; - }>; -} diff --git a/packages/webgpu/src/__tests__/demos/ABuffer.spec.ts b/packages/webgpu/src/__tests__/demos/ABuffer.spec.ts index cc532515f..b55d8dbd6 100644 --- a/packages/webgpu/src/__tests__/demos/ABuffer.spec.ts +++ b/packages/webgpu/src/__tests__/demos/ABuffer.spec.ts @@ -248,6 +248,7 @@ describe("A Buffer", () => { translucentWGSL, mat4, vec3, + canvas, }) => { const presentationFormat = gpu.getPreferredCanvasFormat(); const settings = { @@ -579,7 +580,7 @@ describe("A Buffer", () => { } const depthTexture = device.createTexture({ - size: [ctx.width, ctx.height], + size: [ctx.canvas.width, ctx.canvas.height], format: "depth24plus", usage: GPUTextureUsage.RENDER_ATTACHMENT | @@ -605,12 +606,12 @@ describe("A Buffer", () => { // We want to keep the linked-list buffer size under the maxStorageBufferBindingSize. // Split the frame into enough slices to meet that constraint. const bytesPerline = - ctx.width * averageLayersPerFragment * linkedListElementSize; + ctx.canvas.width * averageLayersPerFragment * linkedListElementSize; const maxLinesSupported = Math.floor( device.limits.maxStorageBufferBindingSize / bytesPerline, ); - const numSlices = Math.ceil(ctx.height / maxLinesSupported); - const sliceHeight = Math.ceil(ctx.height / numSlices); + const numSlices = Math.ceil(ctx.canvas.height / maxLinesSupported); + const sliceHeight = Math.ceil(ctx.canvas.height / numSlices); const linkedListBufferSize = sliceHeight * bytesPerline; const linkedListBuffer = device.createBuffer({ @@ -645,13 +646,17 @@ describe("A Buffer", () => { // * numFragments : u32 // * data : array const headsBuffer = device.createBuffer({ - size: (1 + ctx.width * sliceHeight) * Uint32Array.BYTES_PER_ELEMENT, + size: + (1 + ctx.canvas.width * sliceHeight) * + Uint32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, label: "headsBuffer", }); const headsInitBuffer = device.createBuffer({ - size: (1 + ctx.width * sliceHeight) * Uint32Array.BYTES_PER_ELEMENT, + size: + (1 + ctx.canvas.width * sliceHeight) * + Uint32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.COPY_SRC, mappedAtCreation: true, label: "headsInitBuffer", @@ -745,7 +750,7 @@ describe("A Buffer", () => { // Rotates the camera around the origin based on time. function getCameraViewProjMatrix() { - const aspect = ctx.width / ctx.height; + const aspect = ctx.canvas.width / ctx.canvas.height; const projectionMatrix = mat4.perspective( (2 * Math.PI) / 5, @@ -776,8 +781,8 @@ describe("A Buffer", () => { new Float32Array(buffer).set(getCameraViewProjMatrix()); new Uint32Array(buffer, 16 * Float32Array.BYTES_PER_ELEMENT).set([ - averageLayersPerFragment * ctx.width * sliceHeight, - ctx.width, + averageLayersPerFragment * ctx.canvas.width * sliceHeight, + ctx.canvas.width, ]); device.queue.writeBuffer(uniformBuffer, 0, buffer); @@ -810,9 +815,9 @@ describe("A Buffer", () => { const scissorX = 0; const scissorY = slice * sliceHeight; - const scissorWidth = ctx.width; + const scissorWidth = ctx.canvas.width; const scissorHeight = - Math.min((slice + 1) * sliceHeight, ctx.height) - + Math.min((slice + 1) * sliceHeight, ctx.canvas.height) - slice * sliceHeight; // Draw the translucent objects @@ -870,7 +875,7 @@ describe("A Buffer", () => { doDraw(); - return ctx.getImageData(); + return canvas.getImageData(); }, { mesh: teapotMesh, diff --git a/packages/webgpu/src/__tests__/demos/Blur.spec.ts b/packages/webgpu/src/__tests__/demos/Blur.spec.ts index 6948072ed..d3d85291f 100644 --- a/packages/webgpu/src/__tests__/demos/Blur.spec.ts +++ b/packages/webgpu/src/__tests__/demos/Blur.spec.ts @@ -133,6 +133,7 @@ describe("Blur", () => { assets: { di3D: imageBitmap }, fullscreenTexturedQuadWGSL, blurWGSL, + canvas, }) => { const tileDim = 128; const batch = [4, 4]; @@ -387,7 +388,7 @@ describe("Blur", () => { device.queue.submit([commandEncoder.finish()]); } frame(); - return ctx.getImageData(); + return canvas.getImageData(); }, { blurWGSL: blur, fullscreenTexturedQuadWGSL: fullscreenTexturedQuad }, ); diff --git a/packages/webgpu/src/__tests__/demos/Cube.spec.ts b/packages/webgpu/src/__tests__/demos/Cube.spec.ts index 4440df4d0..87664baeb 100644 --- a/packages/webgpu/src/__tests__/demos/Cube.spec.ts +++ b/packages/webgpu/src/__tests__/demos/Cube.spec.ts @@ -84,6 +84,7 @@ describe("Cube", () => { vec3, basicVertWGSL, vertexPositionColorWGSL, + canvas, }) => { const cubeVertexSize = 4 * 10; // Byte size of one cube vertex. const cubePositionOffset = 0; @@ -156,7 +157,7 @@ describe("Cube", () => { }); const depthTexture = device.createTexture({ - size: [ctx.width, ctx.height], + size: [ctx.canvas.width, ctx.canvas.height], format: "depth24plus", usage: GPUTextureUsage.RENDER_ATTACHMENT, }); @@ -196,7 +197,7 @@ describe("Cube", () => { }, }; - const aspect = ctx.width / ctx.height; + const aspect = ctx.canvas.width / ctx.canvas.height; const projectionMatrix = mat4.perspective( (2 * Math.PI) / 5, aspect, @@ -243,7 +244,7 @@ describe("Cube", () => { passEncoder.draw(cubeVertexCount); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - return ctx.getImageData(); + return canvas.getImageData(); }, { basicVertWGSL: basicVert, @@ -264,6 +265,7 @@ describe("Cube", () => { vec3, basicVertWGSL, vertexPositionColorWGSL, + canvas, }) => { const cubeVertexSize = 4 * 10; // Byte size of one cube vertex. const cubePositionOffset = 0; @@ -334,7 +336,7 @@ describe("Cube", () => { }); const depthTexture = device.createTexture({ - size: [ctx.width, ctx.height], + size: [ctx.canvas.width, ctx.canvas.height], format: "depth24plus", usage: GPUTextureUsage.RENDER_ATTACHMENT, }); @@ -395,7 +397,7 @@ describe("Cube", () => { }, }; - const aspect = ctx.width / ctx.height; + const aspect = ctx.canvas.width / ctx.canvas.height; const projectionMatrix = mat4.perspective( (2 * Math.PI) / 5, aspect, @@ -475,7 +477,7 @@ describe("Cube", () => { passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - return ctx.getImageData(); + return canvas.getImageData(); }, { basicVertWGSL: basicVert, @@ -496,6 +498,7 @@ describe("Cube", () => { vec3, basicVertWGSL, sampleTextureMixColorWGSL, + canvas, }) => { const cubeVertexSize = 4 * 10; // Byte size of one cube vertex. const cubePositionOffset = 0; @@ -567,7 +570,7 @@ describe("Cube", () => { }); const depthTexture = device.createTexture({ - size: [ctx.width, ctx.height], + size: [ctx.canvas.width, ctx.canvas.height], format: "depth24plus", usage: GPUTextureUsage.RENDER_ATTACHMENT, }); @@ -638,7 +641,7 @@ describe("Cube", () => { }, }; - const aspect = ctx.width / ctx.height; + const aspect = ctx.canvas.width / ctx.canvas.height; const projectionMatrix = mat4.perspective( (2 * Math.PI) / 5, aspect, @@ -686,7 +689,7 @@ describe("Cube", () => { passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - return ctx.getImageData(); + return canvas.getImageData(); }, { basicVertWGSL: basicVert, @@ -708,6 +711,7 @@ describe("Cube", () => { vec3, vertexPositionColorWGSL, instancedVertWGSL, + canvas, }) => { const cubeVertexSize = 4 * 10; // Byte size of one cube vertex. const cubePositionOffset = 0; @@ -778,7 +782,7 @@ describe("Cube", () => { }); const depthTexture = device.createTexture({ - size: [ctx.width, ctx.height], + size: [ctx.canvas.width, ctx.canvas.height], format: "depth24plus", usage: GPUTextureUsage.RENDER_ATTACHMENT, }); @@ -809,7 +813,7 @@ describe("Cube", () => { ], }); - const aspect = ctx.width / ctx.height; + const aspect = ctx.canvas.width / ctx.canvas.height; const projectionMatrix = mat4.perspective( (2 * Math.PI) / 5, aspect, @@ -912,7 +916,7 @@ describe("Cube", () => { passEncoder.draw(cubeVertexCount, numInstances, 0, 0); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - return ctx.getImageData(); + return canvas.getImageData(); }, { vertexPositionColorWGSL: vertexPositionColor, diff --git a/packages/webgpu/src/__tests__/demos/FractalCube.spec.ts b/packages/webgpu/src/__tests__/demos/FractalCube.spec.ts index c7ba0b0fb..70dd2cff4 100644 --- a/packages/webgpu/src/__tests__/demos/FractalCube.spec.ts +++ b/packages/webgpu/src/__tests__/demos/FractalCube.spec.ts @@ -28,6 +28,7 @@ describe("Fractal Cube", () => { vec3, sampleSelfWGSL, basicVertWGSL, + canvas, }) => { const cubeVertexSize = 4 * 10; // Byte size of one cube vertex. const cubePositionOffset = 0; @@ -98,7 +99,7 @@ describe("Fractal Cube", () => { }); const depthTexture = device.createTexture({ - size: [ctx.width, ctx.height], + size: [ctx.canvas.width, ctx.canvas.height], format: "depth24plus", usage: GPUTextureUsage.RENDER_ATTACHMENT, }); @@ -112,7 +113,7 @@ describe("Fractal Cube", () => { // We will copy the frame's rendering results into this texture and // sample it on the next frame. const cubeTexture = device.createTexture({ - size: [ctx.width, ctx.height], + size: [ctx.canvas.width, ctx.canvas.height], format: presentationFormat, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, }); @@ -163,7 +164,7 @@ describe("Fractal Cube", () => { }, }; - const aspect = ctx.width / ctx.height; + const aspect = ctx.canvas.width / ctx.canvas.height; const projectionMatrix = mat4.perspective( (2 * Math.PI) / 5, aspect, @@ -217,7 +218,7 @@ describe("Fractal Cube", () => { { texture: cubeTexture, }, - [ctx.width, ctx.height], + [ctx.canvas.width, ctx.canvas.height], ); device.queue.submit([commandEncoder.finish()]); @@ -226,7 +227,7 @@ describe("Fractal Cube", () => { for (let i = 0; i < 10; i++) { frame(now + i * 16); } - return ctx.getImageData(); + return canvas.getImageData(); }, { basicVertWGSL: basicVert, diff --git a/packages/webgpu/src/__tests__/demos/OcclusionQuery.spec.ts b/packages/webgpu/src/__tests__/demos/OcclusionQuery.spec.ts index 508a23f1b..e02ffffea 100644 --- a/packages/webgpu/src/__tests__/demos/OcclusionQuery.spec.ts +++ b/packages/webgpu/src/__tests__/demos/OcclusionQuery.spec.ts @@ -56,7 +56,7 @@ export type TypedArrayConstructor = describe("OcclusionQuery", () => { it("occlusionQuery", async () => { const result = await client.eval( - ({ device, ctx, gpu, solidColorLitWGSL, mat4 }) => { + ({ device, ctx, gpu, solidColorLitWGSL, mat4, canvas }) => { const depthFormat = "depth24plus"; const presentationFormat = gpu.getPreferredCanvasFormat(); const animate = true; @@ -262,7 +262,7 @@ describe("OcclusionQuery", () => { const projection = mat4.perspective( (30 * Math.PI) / 180, - ctx.width / ctx.height, + ctx.canvas.width / ctx.canvas.height, 0.5, 100, ); @@ -365,7 +365,7 @@ describe("OcclusionQuery", () => { } } render(1721766068905 + 150 * 16); - return ctx.getImageData().then((image) => ({ image, visible })); + return canvas.getImageData().then((image) => ({ image, visible })); }, { solidColorLitWGSL: solidColorLit }, ); diff --git a/packages/webgpu/src/__tests__/demos/RenderBundles.spec.ts b/packages/webgpu/src/__tests__/demos/RenderBundles.spec.ts index 0e621c246..6d4ab2f68 100644 --- a/packages/webgpu/src/__tests__/demos/RenderBundles.spec.ts +++ b/packages/webgpu/src/__tests__/demos/RenderBundles.spec.ts @@ -75,6 +75,7 @@ describe("Render Bundles", () => { vec3, assets: { saturn, moon }, vals, + canvas, }) => { interface SphereMesh { vertices: Float32Array; @@ -255,7 +256,7 @@ describe("Render Bundles", () => { }); const depthTexture = device.createTexture({ - size: [ctx.width, ctx.height], + size: [ctx.canvas.width, ctx.canvas.height], format: "depth24plus", usage: GPUTextureUsage.RENDER_ATTACHMENT, }); @@ -447,7 +448,7 @@ describe("Render Bundles", () => { }, }; - const aspect = ctx.width / ctx.height; + const aspect = ctx.canvas.width / ctx.canvas.height; const projectionMatrix = mat4.perspective( (2 * Math.PI) / 5, aspect, @@ -569,7 +570,7 @@ describe("Render Bundles", () => { device.queue.submit([commandEncoder.finish()]); } frame(); - return ctx.getImageData(); + return canvas.getImageData(); }, { meshWGSL: mesh, vals: randomValues }, ); diff --git a/packages/webgpu/src/__tests__/demos/Triangle.spec.ts b/packages/webgpu/src/__tests__/demos/Triangle.spec.ts index c0e04e31d..2e28d85d3 100644 --- a/packages/webgpu/src/__tests__/demos/Triangle.spec.ts +++ b/packages/webgpu/src/__tests__/demos/Triangle.spec.ts @@ -3,7 +3,13 @@ import { checkImage, client, encodeImage } from "../setup"; describe("Triangle", () => { it("Simple Triangle", async () => { const result = await client.eval( - ({ device, shaders: { triangleVertWGSL, redFragWGSL }, gpu, ctx }) => { + ({ + device, + shaders: { triangleVertWGSL, redFragWGSL }, + gpu, + ctx, + canvas, + }) => { const pipeline = device.createRenderPipeline({ layout: "auto", vertex: { @@ -46,7 +52,7 @@ describe("Triangle", () => { passEncoder.draw(3); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - return ctx.getImageData(); + return canvas.getImageData(); }, ); const image = encodeImage(result); @@ -54,7 +60,13 @@ describe("Triangle", () => { }); it("Async Simple Triangle", async () => { const result = await client.eval( - ({ device, shaders: { triangleVertWGSL, redFragWGSL }, gpu, ctx }) => { + ({ + device, + shaders: { triangleVertWGSL, redFragWGSL }, + gpu, + ctx, + canvas, + }) => { return device .createRenderPipelineAsync({ layout: "auto", @@ -98,7 +110,7 @@ describe("Triangle", () => { passEncoder.draw(3); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - return ctx.getImageData(); + return canvas.getImageData(); }); }, ); @@ -107,7 +119,13 @@ describe("Triangle", () => { }); it("Triangle MSAA", async () => { const result = await client.eval( - ({ device, shaders: { triangleVertWGSL, redFragWGSL }, gpu, ctx }) => { + ({ + device, + shaders: { triangleVertWGSL, redFragWGSL }, + gpu, + ctx, + canvas, + }) => { const sampleCount = 4; const presentationFormat = gpu.getPreferredCanvasFormat(); const pipeline = device.createRenderPipeline({ @@ -136,7 +154,7 @@ describe("Triangle", () => { }); const texture = device.createTexture({ - size: [ctx.width, ctx.height], + size: [ctx.canvas.width, ctx.canvas.height], sampleCount, format: presentationFormat, usage: GPUTextureUsage.RENDER_ATTACHMENT, @@ -163,7 +181,7 @@ describe("Triangle", () => { passEncoder.draw(3); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - return ctx.getImageData(); + return canvas.getImageData(); }, ); const image = encodeImage(result); @@ -171,7 +189,7 @@ describe("Triangle", () => { }); it("Triangle with constants", async () => { const result = await client.eval( - ({ device, shaders: { triangleVertWGSL }, gpu, ctx }) => { + ({ device, shaders: { triangleVertWGSL }, gpu, ctx, canvas }) => { const pipeline = device.createRenderPipeline({ layout: "auto", vertex: { @@ -239,7 +257,7 @@ describe("Triangle", () => { passEncoder.draw(3); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - return ctx.getImageData(); + return canvas.getImageData(); }, ); const image = encodeImage(result); diff --git a/packages/webgpu/src/__tests__/setup.ts b/packages/webgpu/src/__tests__/setup.ts index 2a9b28e13..48ec0da2c 100644 --- a/packages/webgpu/src/__tests__/setup.ts +++ b/packages/webgpu/src/__tests__/setup.ts @@ -11,7 +11,8 @@ import type { mat4, vec3, mat3 } from "wgpu-matrix"; import type { Server, WebSocket } from "ws"; import type { Browser, Page } from "puppeteer"; -import type { DrawingContext } from "./components/DrawingContext"; +import type { GPUOffscreenCanvas } from "../Offscreen"; + import { cubeVertexArray } from "./components/cube"; import { redFragWGSL, triangleVertWGSL } from "./components/triangle"; import { DEBUG, REFERENCE } from "./config"; @@ -26,7 +27,7 @@ declare global { var testOS: TestOS; } -interface GPUContext { +interface GPUTestingContext { gpu: GPU; adapter: GPUAdapter; device: GPUDevice; @@ -43,7 +44,8 @@ interface GPUContext { moon: ImageData; saturn: ImageData; }; - ctx: DrawingContext; + ctx: GPUCanvasContext; + canvas: GPUOffscreenCanvas; mat4: typeof mat4; vec3: typeof vec3; mat3: typeof mat3; @@ -61,7 +63,7 @@ type JSONValue = interface TestingClient { eval( - fn: (ctx: GPUContext & C) => R | Promise, + fn: (ctx: GPUTestingContext & C) => R | Promise, ctx?: C, ): Promise; OS: TestOS; @@ -86,7 +88,7 @@ class RemoteTestingClient implements TestingClient { readonly arch = global.testArch; eval( - fn: (ctx: GPUContext & C) => R | Promise, + fn: (ctx: GPUTestingContext & C) => R | Promise, context?: C, ): Promise { const ctx = this.prepareContext(context ?? {}); @@ -131,7 +133,7 @@ class ReferenceTestingClient implements TestingClient { private page: Page | null = null; async eval( - fn: (ctx: GPUContext & C) => R | Promise, + fn: (ctx: GPUTestingContext & C) => R | Promise, ctx?: C, ): Promise { if (!this.page) { @@ -168,6 +170,14 @@ class ReferenceTestingClient implements TestingClient { getCurrentTexture() { return this.texture; } + + get canvas() { + return { + width: this.width, + height: this.height, + }; + } + getImageData() { const commandEncoder = this.device.createCommandEncoder(); const bytesPerRow = this.width * 4; @@ -206,6 +216,11 @@ class ReferenceTestingClient implements TestingClient { redFragWGSL, }, ctx, + canvas: { + getImageData: ctx.getImageData.bind(ctx), + width: ctx.width, + height: ctx.height, + }, mat4, vec3, mat3, diff --git a/packages/webgpu/src/index.tsx b/packages/webgpu/src/index.tsx index 2c9b61d9d..d6268d4dd 100644 --- a/packages/webgpu/src/index.tsx +++ b/packages/webgpu/src/index.tsx @@ -2,6 +2,7 @@ import WebGPUNativeModule from "./NativeWebGPUModule"; export * from "./Canvas"; +export * from "./Offscreen"; export * from "./WebGPUViewNativeComponent"; export * from "./utils"; export { default as WebGPUModule } from "./NativeWebGPUModule";