diff --git a/electron/ipc/export/native-video.ts b/electron/ipc/export/native-video.ts index 81b56b480..5fe2273a0 100644 --- a/electron/ipc/export/native-video.ts +++ b/electron/ipc/export/native-video.ts @@ -1944,6 +1944,8 @@ export function sanitizeExportGpuInfo( return { machineModel, gpus }; } +const GPU_INFO_TIMEOUT_MS = 3000; + /** Captures sanitized hardware and GPU acceleration details for export support reports. */ export async function getExportHardwareInfo(): Promise { let sanitizedGpuInfo: Pick = { @@ -1951,7 +1953,18 @@ export async function getExportHardwareInfo(): Promise { gpus: [], }; try { - sanitizedGpuInfo = sanitizeExportGpuInfo(await app.getGPUInfo("complete")); + // app.getGPUInfo("complete") can hang forever (never resolve or reject) + // when the GPU process is crashed, disabled, or blocklisted, which would + // otherwise stall the entire export pipeline at "preparing" with no + // error surfaced. Race it against a timeout so a stuck GPU process can + // never block exporting. + const gpuInfo = await Promise.race([ + app.getGPUInfo("complete"), + new Promise((_resolve, reject) => + setTimeout(() => reject(new Error("getGPUInfo timed out")), GPU_INFO_TIMEOUT_MS), + ), + ]); + sanitizedGpuInfo = sanitizeExportGpuInfo(gpuInfo); } catch { // Hardware diagnostics are best effort and must not affect exporting. } diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 842343dc3..bac26cf53 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -393,6 +393,11 @@ const VideoPlayback = forwardRef( const cameraContainerRef = useRef(null); const [pixiReady, setPixiReady] = useState(false); const videoReady = usePreviewVideoReady(videoRef, videoPath); + // Bumped to force a full teardown/recreate of the Pixi renderer after a + // lost WebGL/WebGPU context (e.g. Windows reclaiming GPU resources from a + // long-minimized window), since neither backend reliably self-recovers. + const [rendererGeneration, setRendererGeneration] = useState(0); + const contextLostCleanupRef = useRef<(() => void) | null>(null); const [previewViewportWidth, setPreviewViewportWidth] = useState(640); const [annotationSceneTransform, setAnnotationSceneTransform] = @@ -1743,6 +1748,21 @@ const VideoPlayback = forwardRef( appRef.current = app; container.appendChild(app.canvas); + // A lost WebGL context (common after Windows reclaims GPU resources + // from a window minimized/occluded for a long time) leaves the canvas + // permanently black with no further Pixi errors. Force a full + // teardown/recreate rather than relying on browser-level restore, + // which PixiJS does not reliably resume from. + const canvasEl = app.canvas as unknown as HTMLCanvasElement; + const handleContextLost = (event: Event) => { + event.preventDefault(); + console.warn("Preview renderer lost its GPU context; recreating."); + setRendererGeneration((generation) => generation + 1); + }; + canvasEl.addEventListener("webglcontextlost", handleContextLost, false); + contextLostCleanupRef.current = () => + canvasEl.removeEventListener("webglcontextlost", handleContextLost, false); + // Camera container - this will be scaled/positioned for zoom const cameraContainer = new Container(); cameraContainerRef.current = cameraContainer; @@ -1823,6 +1843,8 @@ const VideoPlayback = forwardRef( motionBlurFilterRef.current?.destroy(); zoomBlurFilterRef.current = null; motionBlurFilterRef.current = null; + contextLostCleanupRef.current?.(); + contextLostCleanupRef.current = null; destroyPixiApplication(app, "preview renderer"); appRef.current = null; cameraContainerRef.current = null; @@ -1831,7 +1853,32 @@ const VideoPlayback = forwardRef( cursorContainerRef.current = null; videoSpriteRef.current = null; }; - }, [initializePixiRenderer, onError, syncPreviewMotionBlurQuality]); + }, [initializePixiRenderer, onError, syncPreviewMotionBlurQuality, rendererGeneration]); + + // Safety net for GPU context loss that doesn't fire `webglcontextlost` + // (e.g. the WebGPU backend, or a stalled hidden