From e579ae120290329fd697b50ea9138ffccb65a805 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 1 Apr 2026 13:57:32 +0200 Subject: feat: migrate GPU renderer from WebGL to wgpu/WASM --- apps/web/src/services/renderer/gpu-renderer.ts | 118 +++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 apps/web/src/services/renderer/gpu-renderer.ts (limited to 'apps/web/src/services/renderer/gpu-renderer.ts') diff --git a/apps/web/src/services/renderer/gpu-renderer.ts b/apps/web/src/services/renderer/gpu-renderer.ts new file mode 100644 index 00000000..91b03603 --- /dev/null +++ b/apps/web/src/services/renderer/gpu-renderer.ts @@ -0,0 +1,118 @@ +import { + applyEffectPasses, + applyMaskFeather as applyMaskFeatherWasm, + initializeGpu, +} from "opencut-wasm"; +import type { EffectPass, EffectUniformValue } from "@/lib/effects/types"; + +let initializeGpuRendererPromise: Promise | null = null; + +export function initializeGpuRenderer(): Promise { + if (!initializeGpuRendererPromise) { + initializeGpuRendererPromise = initializeGpu(); + } + const promise = initializeGpuRendererPromise; + if (!promise) { + throw new Error("GPU renderer initialization promise was not created"); + } + return promise; +} + +export const gpuRenderer = { + applyEffect({ + source, + width, + height, + passes, + }: { + source: CanvasImageSource; + width: number; + height: number; + passes: EffectPass[]; + }): CanvasImageSource { + if (passes.length === 0) { + return source; + } + + const sourceCanvas = ensureOffscreenCanvas({ + source, + width, + height, + label: "effect source", + }); + return applyEffectPasses({ + source: sourceCanvas, + width, + height, + passes: serializeEffectPasses(passes), + }); + }, + + applyMaskFeather({ + maskCanvas, + width, + height, + feather, + }: { + maskCanvas: CanvasImageSource; + width: number; + height: number; + feather: number; + }): CanvasImageSource { + const sourceCanvas = ensureOffscreenCanvas({ + source: maskCanvas, + width, + height, + label: "mask source", + }); + return applyMaskFeatherWasm({ + mask: sourceCanvas, + width, + height, + feather, + }); + }, +}; + +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 the GPU ${label}`); + } + + const canvas = new OffscreenCanvas(width, height); + const context = canvas.getContext("2d"); + if (!context) { + throw new Error(`Failed to get 2d context for the GPU ${label}`); + } + context.clearRect(0, 0, width, height); + context.drawImage(source, 0, 0, width, height); + return canvas; +} + +function serializeEffectPasses(passes: EffectPass[]) { + return passes.map((pass) => ({ + shader: pass.shader, + uniforms: Object.entries(pass.uniforms).map(([name, value]) => ({ + name, + value: normalizeUniformValue(value), + })), + })); +} + +function normalizeUniformValue(value: EffectUniformValue): number[] { + return typeof value === "number" ? [value] : value; +} -- cgit v1.3.1