summaryrefslogtreecommitdiff
path: root/apps/web/src/services/renderer/compositor/wasm-compositor.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-13 04:41:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-13 05:08:29 +0200
commitcad88ee4d1c9e06e7529b88ef5e5d3fdf3f89149 (patch)
tree0bfc1fcb18b3da4969afa2836bb65e6a5e25d123 /apps/web/src/services/renderer/compositor/wasm-compositor.ts
parent36efdf66e707c818b81009acf8a00630a0309ca1 (diff)
downloadopencut-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.ts142
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();