diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-13 04:41:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-13 05:08:29 +0200 |
| commit | cad88ee4d1c9e06e7529b88ef5e5d3fdf3f89149 (patch) | |
| tree | 0bfc1fcb18b3da4969afa2836bb65e6a5e25d123 /apps/web/src/services/renderer/compositor/wasm-compositor.ts | |
| parent | 36efdf66e707c818b81009acf8a00630a0309ca1 (diff) | |
| download | opencut-cad88ee4d1c9e06e7529b88ef5e5d3fdf3f89149.tar.gz | |
refactor: rendering pipeline
Made-with: Cursor
Diffstat (limited to 'apps/web/src/services/renderer/compositor/wasm-compositor.ts')
| -rw-r--r-- | apps/web/src/services/renderer/compositor/wasm-compositor.ts | 142 |
1 files changed, 142 insertions, 0 deletions
diff --git a/apps/web/src/services/renderer/compositor/wasm-compositor.ts b/apps/web/src/services/renderer/compositor/wasm-compositor.ts new file mode 100644 index 00000000..1faecc32 --- /dev/null +++ b/apps/web/src/services/renderer/compositor/wasm-compositor.ts @@ -0,0 +1,142 @@ +import { + getCompositorCanvas, + initCompositor, + releaseTexture, + renderFrame, + resizeCompositor, + uploadTexture, +} from "opencut-wasm"; +import type { FrameDescriptor } from "./types"; + +function ensureOffscreenCanvas({ + source, + width, + height, + label, +}: { + source: CanvasImageSource; + width: number; + height: number; + label: string; +}): OffscreenCanvas { + if (source instanceof OffscreenCanvas) { + return source; + } + + if (typeof OffscreenCanvas === "undefined") { + throw new Error(`OffscreenCanvas is required for ${label}`); + } + + const canvas = new OffscreenCanvas(width, height); + const context = canvas.getContext("2d"); + if (!context) { + throw new Error(`Failed to get 2d context for ${label}`); + } + context.clearRect(0, 0, width, height); + context.drawImage(source, 0, 0, width, height); + return canvas; +} + +export type TextureUploadDescriptor = { + id: string; + source: CanvasImageSource; + width: number; + height: number; +}; + +class WasmCompositor { + private canvas: HTMLCanvasElement | null = null; + private initializedSize: { width: number; height: number } | null = null; + private retainedTextureIds = new Set<string>(); + private uploadedTextures = new Map< + string, + { source: CanvasImageSource; width: number; height: number } + >(); + private _debugLogged = false; + + ensureInitialized({ width, height }: { width: number; height: number }) { + if (!this.canvas) { + initCompositor(width, height); + this.canvas = getCompositorCanvas(); + this.initializedSize = { width, height }; + return; + } + + if ( + !this.initializedSize || + this.initializedSize.width !== width || + this.initializedSize.height !== height + ) { + resizeCompositor(width, height); + this.initializedSize = { width, height }; + } + } + + getCanvas(): HTMLCanvasElement { + if (!this.canvas) { + throw new Error("Compositor is not initialized"); + } + return this.canvas; + } + + syncTextures(textures: TextureUploadDescriptor[]) { + const nextIds = new Set(textures.map((texture) => texture.id)); + for (const previousId of this.retainedTextureIds) { + if (!nextIds.has(previousId)) { + releaseTexture(previousId); + this.uploadedTextures.delete(previousId); + } + } + + for (const texture of textures) { + const previousTexture = this.uploadedTextures.get(texture.id); + if ( + previousTexture?.source === texture.source && + previousTexture.width === texture.width && + previousTexture.height === texture.height + ) { + continue; + } + + const sourceCanvas = ensureOffscreenCanvas({ + source: texture.source, + width: texture.width, + height: texture.height, + label: `texture upload ${texture.id}`, + }); + uploadTexture({ + id: texture.id, + source: sourceCanvas, + width: texture.width, + height: texture.height, + }); + this.uploadedTextures.set(texture.id, { + source: texture.source, + width: texture.width, + height: texture.height, + }); + } + + this.retainedTextureIds = nextIds; + } + + render(frame: FrameDescriptor) { + if (!this._debugLogged) { + this._debugLogged = true; + const firstLayer = frame.items.find((item) => item.type === "layer"); + console.log( + "[compositor] first frame — canvas size:", + JSON.stringify({ width: frame.width, height: frame.height }), + "| first layer transform:", + firstLayer && firstLayer.type === "layer" + ? JSON.stringify(firstLayer.transform) + : "none", + "| webgpu available:", + typeof navigator !== "undefined" && "gpu" in navigator, + ); + } + renderFrame(frame); + } +} + +export const wasmCompositor = new WasmCompositor(); |
