From 451ec339a5909c587875c033145e61105ccaf7da Mon Sep 17 00:00:00 2001 From: Hiren Kangad Date: Tue, 10 Mar 2026 10:45:14 +0530 Subject: [PATCH] fix(refboard): prevent WebGL OOM on large scenes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Skip thumbnail generation when scene has >50 items (extract.canvas on full viewport with many textures causes GPU OOM and context loss) - Add WebGL context lost/restored handlers — auto-reload scene on recovery - Reduce texture memory budget from 512MB to 256MB for GPU headroom --- frontend/src/canvas/PixiCanvas.tsx | 25 +++++++++++++++++++++++++ frontend/src/canvas/TextureManager.ts | 2 +- frontend/src/hooks/useSaveManager.ts | 7 +++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/frontend/src/canvas/PixiCanvas.tsx b/frontend/src/canvas/PixiCanvas.tsx index 2f44d97..5b4818b 100644 --- a/frontend/src/canvas/PixiCanvas.tsx +++ b/frontend/src/canvas/PixiCanvas.tsx @@ -224,6 +224,24 @@ const PixiCanvas = forwardRef( // Store observer for cleanup (container as any).__pixiRO = ro; + // -- WebGL context loss recovery ------------------------------------ + const canvas = app.canvas as HTMLCanvasElement; + const onContextLost = (e: Event) => { + e.preventDefault(); // allows context to be restored + console.warn('[PixiCanvas] WebGL context lost — waiting for restore'); + }; + const onContextRestored = () => { + console.warn('[PixiCanvas] WebGL context restored — reloading scene'); + // Re-load scene data so textures get re-uploaded to GPU + if (sceneRef.current && initialLoadDone.current) { + const data = sceneRef.current.serialize(); + sceneRef.current.loadScene(data, false); + } + }; + canvas.addEventListener('webglcontextlost', onContextLost); + canvas.addEventListener('webglcontextrestored', onContextRestored); + (container as any).__pixiContextHandlers = { onContextLost, onContextRestored, canvas }; + // Signal that PixiJS is ready for scene loading setPixiReady(true); @@ -254,6 +272,13 @@ const PixiCanvas = forwardRef( delete (container as any).__pixiWheelHandler; } + const ctxHandlers = (container as any).__pixiContextHandlers; + if (ctxHandlers) { + ctxHandlers.canvas.removeEventListener('webglcontextlost', ctxHandlers.onContextLost); + ctxHandlers.canvas.removeEventListener('webglcontextrestored', ctxHandlers.onContextRestored); + delete (container as any).__pixiContextHandlers; + } + textures.clear(); if (appRef.current) { diff --git a/frontend/src/canvas/TextureManager.ts b/frontend/src/canvas/TextureManager.ts index 96cb3a5..c1b147b 100644 --- a/frontend/src/canvas/TextureManager.ts +++ b/frontend/src/canvas/TextureManager.ts @@ -13,7 +13,7 @@ interface TextureEntry { */ export class TextureManager { private cache = new Map(); - private budget = 512 * 1024 * 1024; // 512 MB + private budget = 256 * 1024 * 1024; // 256 MB — keeps headroom for GPU ops private currentUsage = 0; /** Build the URL for a given asset. */ diff --git a/frontend/src/hooks/useSaveManager.ts b/frontend/src/hooks/useSaveManager.ts index fa15a6e..6859086 100644 --- a/frontend/src/hooks/useSaveManager.ts +++ b/frontend/src/hooks/useSaveManager.ts @@ -27,12 +27,15 @@ export function useSaveManager({ resolvedBoardId, isPublicView, canvasRef, setSa try { const state = JSON.stringify(scene.serialize()); - // Generate thumbnail from PixiJS renderer + // Generate thumbnail — skip for large scenes to avoid GPU OOM let thumbnail: string | undefined; try { const app = canvasRef.current?.getApp(); const viewport = canvasRef.current?.getViewport(); - if (app?.renderer?.extract && viewport) { + const itemCount = scene.getAllItems().length; + // Skip thumbnail extraction when >50 items — extract.canvas on + // the full viewport with many textures causes WebGL OOM. + if (app?.renderer?.extract && viewport && itemCount <= 50) { const fullCanvas = app.renderer.extract.canvas(viewport) as HTMLCanvasElement; const thumbMax = 400; const sw = fullCanvas.width;