diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-22 16:48:37 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-22 16:48:37 +0200 |
| commit | cceb835a84c125990ad8e49784e01a7c52636bb3 (patch) | |
| tree | 8520cce9a203c72c7bcdc72ca138a36316a5a110 /apps/web/src/services/renderer/compositor/wasm-compositor.ts | |
| parent | f1b45b4328cff6a016720ce7b38b87ac2992dc2d (diff) | |
| download | opencut-cceb835a84c125990ad8e49784e01a7c52636bb3.tar.gz | |
perf: improve rendering performance by ~20x
Diffstat (limited to 'apps/web/src/services/renderer/compositor/wasm-compositor.ts')
| -rw-r--r-- | apps/web/src/services/renderer/compositor/wasm-compositor.ts | 220 |
1 files changed, 160 insertions, 60 deletions
diff --git a/apps/web/src/services/renderer/compositor/wasm-compositor.ts b/apps/web/src/services/renderer/compositor/wasm-compositor.ts index 16b03853..10684f11 100644 --- a/apps/web/src/services/renderer/compositor/wasm-compositor.ts +++ b/apps/web/src/services/renderer/compositor/wasm-compositor.ts @@ -1,44 +1,39 @@ import { getCompositorCanvas, + getLastFrameProfile, initCompositor, releaseTexture, renderFrame, resizeCompositor, uploadTexture, } from "opencut-wasm"; -import type { FrameDescriptor } from "./types"; +import { + incrementCounter, + isRenderPerfEnabled, + recordWasmFrameProfile, +} from "@/diagnostics/render-perf"; +import type { + ExternalTextureDescriptor, + FrameDescriptor, + RenderedTextureDescriptor, + TextureUploadDescriptor, +} from "./types"; -function ensureOffscreenCanvas({ - source, - width, - height, - label, -}: { - source: CanvasImageSource; +/** + * One slot in the derived-texture cache. The OffscreenCanvas is persistent — + * we reuse and redraw into it across frames, which is the change that takes + * the WebGL upload path off the per-frame critical path for static content. + */ +type RenderedCacheEntry = { + kind: "rendered"; + canvas: OffscreenCanvas; + contentHash: string; 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; +type ExternalCacheEntry = { + kind: "external"; source: CanvasImageSource; width: number; height: number; @@ -47,11 +42,7 @@ export type TextureUploadDescriptor = { 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 cache = new Map<string, RenderedCacheEntry | ExternalCacheEntry>(); ensureInitialized({ width, height }: { width: number; height: number }) { if (!this.canvas) { @@ -80,48 +71,157 @@ class WasmCompositor { syncTextures(textures: TextureUploadDescriptor[]) { const nextIds = new Set(textures.map((texture) => texture.id)); - for (const previousId of this.retainedTextureIds) { + for (const previousId of this.cache.keys()) { if (!nextIds.has(previousId)) { releaseTexture(previousId); - this.uploadedTextures.delete(previousId); + this.cache.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; + if (texture.kind === "external") { + this.syncExternalTexture(texture); + } else { + this.syncRenderedTexture(texture); } + } + } - const sourceCanvas = ensureOffscreenCanvas({ + render(frame: FrameDescriptor) { + renderFrame(frame); + if (isRenderPerfEnabled()) { + recordWasmFrameProfile( + getLastFrameProfile() as Array<{ name: string; durationMs: number }>, + ); + } + } + + private syncExternalTexture(texture: ExternalTextureDescriptor) { + const previous = this.cache.get(texture.id); + if ( + previous?.kind === "external" && + previous.source === texture.source && + previous.width === texture.width && + previous.height === texture.height + ) { + incrementCounter({ name: "textureCacheHit" }); + return; + } + + incrementCounter({ name: "textureUpload" }); + incrementCounter({ + name: "textureUploadPixels", + by: texture.width * texture.height, + }); + uploadTexture({ + id: texture.id, + source: 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, - }); + }), + width: texture.width, + height: texture.height, + }); + this.cache.set(texture.id, { + kind: "external", + source: texture.source, + width: texture.width, + height: texture.height, + }); + } + + private syncRenderedTexture(texture: RenderedTextureDescriptor) { + const previous = this.cache.get(texture.id); + if ( + previous?.kind === "rendered" && + previous.contentHash === texture.contentHash && + previous.width === texture.width && + previous.height === texture.height + ) { + incrementCounter({ name: "textureCacheHit" }); + return; } - this.retainedTextureIds = nextIds; - } + const canvas = + previous?.kind === "rendered" && + previous.width === texture.width && + previous.height === texture.height + ? previous.canvas + : createBackingCanvas({ + width: texture.width, + height: texture.height, + }); - render(frame: FrameDescriptor) { - renderFrame(frame); + const ctx = canvas.getContext("2d"); + if (!ctx) { + throw new Error(`Failed to get 2d context for texture ${texture.id}`); + } + ctx.clearRect(0, 0, texture.width, texture.height); + texture.draw(ctx); + + incrementCounter({ name: "textureUpload" }); + incrementCounter({ + name: "textureUploadPixels", + by: texture.width * texture.height, + }); + uploadTexture({ + id: texture.id, + source: canvas, + width: texture.width, + height: texture.height, + }); + this.cache.set(texture.id, { + kind: "rendered", + canvas, + contentHash: texture.contentHash, + width: texture.width, + height: texture.height, + }); } } export const wasmCompositor = new WasmCompositor(); + +function createBackingCanvas({ + width, + height, +}: { + width: number; + height: number; +}): OffscreenCanvas { + if (typeof OffscreenCanvas === "undefined") { + throw new Error("OffscreenCanvas is not supported in this environment"); + } + return new OffscreenCanvas(width, height); +} + +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; +} |
