summaryrefslogtreecommitdiff
path: root/apps/web/src/services/renderer/compositor/wasm-compositor.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/services/renderer/compositor/wasm-compositor.ts')
-rw-r--r--apps/web/src/services/renderer/compositor/wasm-compositor.ts220
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;
+}