From 29f1c9932a5c50c36e6a35f6d6539c2aceee983e Mon Sep 17 00:00:00 2001 From: cashmere Date: Sun, 2 Aug 2026 19:59:59 +0200 Subject: fix: keep GPU effect previews from clobbering the preview canvas On the WebGL backend wgpu can only present to the single canvas its GL context is bound to, which is also the compositor's output canvas. The effect tile previews (and mask feather) rendered through that canvas and resized it behind the compositor's back, leaving the main preview showing the preview image stretched until a full reload. TS side (works with the published wasm): after every applyEffectPasses/ applyMaskFeather call the gpu renderer restores the canvas size, using a one-pixel intermediate resize to force a surface reconfigure, then notifies the preview component to re-render the current frame. Rust side (next wasm publish): the GL canvas detour saves and restores the canvas size, and resizeCompositor always reconfigures the surface. --- apps/web/src/preview/components/index.tsx | 9 ++++++++ .../renderer/compositor/wasm-compositor.ts | 8 +++++++ apps/web/src/services/renderer/gpu-renderer.ts | 27 ++++++++++++++++++++-- 3 files changed, 42 insertions(+), 2 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/preview/components/index.tsx b/apps/web/src/preview/components/index.tsx index 894eec78..643eb002 100644 --- a/apps/web/src/preview/components/index.tsx +++ b/apps/web/src/preview/components/index.tsx @@ -7,6 +7,7 @@ import { useRafLoop } from "@/hooks/use-raf-loop"; import { useContainerSize } from "@/hooks/use-container-size"; import { useFullscreen } from "@/hooks/use-fullscreen"; import { CanvasRenderer } from "@/services/renderer/canvas-renderer"; +import { onGpuCanvasRepaired } from "@/services/renderer/gpu-renderer"; import { TICKS_PER_SECOND } from "@/wasm"; import type { RootNode } from "@/services/renderer/nodes/root-node"; import { buildScene } from "@/services/renderer/scene-builder"; @@ -213,6 +214,14 @@ function PreviewCanvas({ useRafLoop(render); + useEffect(() => { + return onGpuCanvasRepaired({ + callback: () => { + lastFrameRef.current = -1; + }, + }); + }, []); + useEffect(() => { const container = viewportRef.current; if (!container) return; diff --git a/apps/web/src/services/renderer/compositor/wasm-compositor.ts b/apps/web/src/services/renderer/compositor/wasm-compositor.ts index 10684f11..5d2ddc3f 100644 --- a/apps/web/src/services/renderer/compositor/wasm-compositor.ts +++ b/apps/web/src/services/renderer/compositor/wasm-compositor.ts @@ -96,6 +96,14 @@ class WasmCompositor { } } + repairSharedCanvas() { + if (!this.canvas || !this.initializedSize) return; + const { width, height } = this.initializedSize; + if (this.canvas.width === width && this.canvas.height === height) return; + resizeCompositor(width + 1, height); + resizeCompositor(width, height); + } + private syncExternalTexture(texture: ExternalTextureDescriptor) { const previous = this.cache.get(texture.id); if ( diff --git a/apps/web/src/services/renderer/gpu-renderer.ts b/apps/web/src/services/renderer/gpu-renderer.ts index c1be3f1a..8ad11083 100644 --- a/apps/web/src/services/renderer/gpu-renderer.ts +++ b/apps/web/src/services/renderer/gpu-renderer.ts @@ -4,6 +4,25 @@ import { initializeGpu, } from "opencut-wasm"; import type { EffectPass, EffectUniformValue } from "@/effects/types"; +import { wasmCompositor } from "./compositor/wasm-compositor"; + +const canvasRepairListeners = new Set<() => void>(); + +export function onGpuCanvasRepaired({ + callback, +}: { + callback: () => void; +}): () => void { + canvasRepairListeners.add(callback); + return () => canvasRepairListeners.delete(callback); +} + +function repairSharedCompositorCanvas(): void { + wasmCompositor.repairSharedCanvas(); + for (const listener of canvasRepairListeners) { + listener(); + } +} let gpuAvailable = false; let initPromise: Promise | null = null; @@ -43,12 +62,14 @@ export const gpuRenderer = { return source; } - return applyEffectPasses({ + const result = applyEffectPasses({ source, width, height, passes: serializeEffectPasses(passes), }); + repairSharedCompositorCanvas(); + return result; }, applyMaskFeather({ @@ -66,12 +87,14 @@ export const gpuRenderer = { return maskCanvas; } - return applyMaskFeatherWasm({ + const result = applyMaskFeatherWasm({ mask: maskCanvas, width, height, feather, }); + repairSharedCompositorCanvas(); + return result; }, }; -- cgit v1.3.1