From 9f50b136145e4f003649bdc234aec28fb77151ca Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Fri, 10 Jul 2026 15:28:37 +0700 Subject: [PATCH 1/2] fix(export): preserve Pixi fallback after renderer init failure --- src/components/video-editor/VideoPlayback.tsx | 22 ++--- src/lib/exporter/frameRenderer.ts | 11 +-- src/lib/exporter/modernFrameRenderer.test.ts | 55 ++++++++++- src/lib/exporter/modernFrameRenderer.ts | 11 +-- src/lib/pixiApplicationLifecycle.test.ts | 92 +++++++++++++++++++ src/lib/pixiApplicationLifecycle.ts | 86 +++++++++++++++++ 6 files changed, 248 insertions(+), 29 deletions(-) create mode 100644 src/lib/pixiApplicationLifecycle.test.ts create mode 100644 src/lib/pixiApplicationLifecycle.ts diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 499179f3..2a635e9e 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -17,6 +17,7 @@ import { enablePitchPreservingPlayback, getMediaSyncPlaybackRate, } from "@/lib/mediaTiming"; +import { destroyPixiApplication, initializePixiApplication } from "@/lib/pixiApplicationLifecycle"; import { DEFAULT_WALLPAPER_PATH, DEFAULT_WALLPAPER_RELATIVE_PATH, @@ -276,7 +277,7 @@ async function initApplicationWithTimeout( }); try { - await Promise.race([app.init(options), timeoutPromise]); + await Promise.race([initializePixiApplication(app, options), timeoutPromise]); } finally { if (timeoutId !== undefined) { clearTimeout(timeoutId); @@ -723,7 +724,10 @@ const VideoPlayback = forwardRef( `[VideoPlayback] Failed to init ${backend} renderer (${statusMessage}) after ${elapsed}ms; trying fallback.`, error, ); - rendererApp.destroy(true); + destroyPixiApplication( + rendererApp, + `${backend} preview renderer initialization`, + ); } } @@ -2127,11 +2131,7 @@ const VideoPlayback = forwardRef( app.ticker.maxFPS = 60; if (!mounted) { - app.destroy(true, { - children: true, - texture: false, - textureSource: false, - }); + destroyPixiApplication(app, "unmounted preview renderer"); return; } @@ -2227,13 +2227,7 @@ const VideoPlayback = forwardRef( motionBlurFilterRef.current?.destroy(); zoomBlurFilterRef.current = null; motionBlurFilterRef.current = null; - if (app && app.renderer) { - app.destroy(true, { - children: true, - texture: false, - textureSource: false, - }); - } + destroyPixiApplication(app, "preview renderer"); appRef.current = null; cameraContainerRef.current = null; videoEffectsContainerRef.current = null; diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 3fa2c1f3..6f891628 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -72,6 +72,7 @@ import { clampMediaTimeToDuration, getEffectiveVideoStreamDurationSeconds, } from "@/lib/mediaTiming"; +import { destroyPixiApplication, initializePixiApplication } from "@/lib/pixiApplicationLifecycle"; import { isVideoWallpaperSource } from "@/lib/wallpapers"; import { renderAnnotations } from "./annotationRenderer"; import { renderCaptions } from "./captionRenderer"; @@ -189,7 +190,7 @@ async function initApplicationWithTimeout( }); try { - await Promise.race([app.init(options), timeoutPromise]); + await Promise.race([initializePixiApplication(app, options), timeoutPromise]); } finally { if (timeoutId !== undefined) { clearTimeout(timeoutId); @@ -389,7 +390,7 @@ export class FrameRenderer { `[FrameRenderer] ${backend} renderer unavailable after ${elapsed}ms; trying next backend.`, error, ); - app.destroy(true); + destroyPixiApplication(app, `${backend} export renderer initialization`); } } @@ -2587,11 +2588,7 @@ export class FrameRenderer { } this.backgroundSprite = null; if (this.app) { - this.app.destroy(true, { - children: true, - texture: false, - textureSource: false, - }); + destroyPixiApplication(this.app, "legacy export renderer"); this.app = null; } this.zoomBlurFilter?.destroy(); diff --git a/src/lib/exporter/modernFrameRenderer.test.ts b/src/lib/exporter/modernFrameRenderer.test.ts index 688e51f2..612a5898 100644 --- a/src/lib/exporter/modernFrameRenderer.test.ts +++ b/src/lib/exporter/modernFrameRenderer.test.ts @@ -6,12 +6,21 @@ const { destroyForwardFrameSourceMock, getForwardFrameAtTimeMock, initializeForwardFrameSourceMock, + pixiApplicationInstancesMock, + pixiInitializationErrorsMock, resolveMediaElementSourceMock, } = vi.hoisted(() => ({ cancelForwardFrameSourceMock: vi.fn(), destroyForwardFrameSourceMock: vi.fn(async () => undefined), getForwardFrameAtTimeMock: vi.fn(async () => null), initializeForwardFrameSourceMock: vi.fn(async () => undefined), + pixiApplicationInstancesMock: [] as Array<{ + destroy: ReturnType; + init: ReturnType; + renderer: { destroy: ReturnType }; + stage: { destroy: ReturnType }; + }>, + pixiInitializationErrorsMock: [] as Array, resolveMediaElementSourceMock: vi.fn(async () => ({ src: "blob:background", revoke: vi.fn(), @@ -19,7 +28,21 @@ const { })); vi.mock("pixi.js", () => ({ - Application: class {}, + Application: class { + destroy = vi.fn(() => { + throw new TypeError("this._cancelResize is not a function"); + }); + init = vi.fn(async () => { + const error = pixiInitializationErrorsMock.shift(); + if (error) throw error; + }); + renderer = { destroy: vi.fn() }; + stage = { destroy: vi.fn() }; + + constructor() { + pixiApplicationInstancesMock.push(this); + } + }, BlurFilter: class {}, Container: class { visible = true; @@ -179,6 +202,36 @@ function createRenderer() { }); } +describe("ModernFrameRenderer Pixi lifecycle", () => { + it("continues to the next backend when failed-init cleanup would throw", async () => { + pixiApplicationInstancesMock.length = 0; + pixiInitializationErrorsMock.length = 0; + pixiInitializationErrorsMock.push(new Error("WebGPU initialization failed"), undefined); + vi.stubGlobal("navigator", { gpu: {} }); + + try { + const renderer = createRenderer() as unknown as { + config: { preferredRenderBackend?: "webgl" | "webgpu" }; + createPixiApplication: ( + canvas: HTMLCanvasElement, + ) => Promise<{ backend: "webgl" | "webgpu" }>; + }; + renderer.config.preferredRenderBackend = "webgpu"; + + await expect(renderer.createPixiApplication({} as HTMLCanvasElement)).resolves.toMatchObject({ + backend: "webgl", + }); + + expect(pixiApplicationInstancesMock).toHaveLength(2); + expect(pixiApplicationInstancesMock[0].destroy).not.toHaveBeenCalled(); + expect(pixiApplicationInstancesMock[0].stage.destroy).toHaveBeenCalledTimes(1); + expect(pixiApplicationInstancesMock[0].renderer.destroy).toHaveBeenCalledTimes(1); + } finally { + vi.unstubAllGlobals(); + } + }); +}); + describe("ModernFrameRenderer blur export path", () => { beforeEach(() => { Object.assign(globalThis, { diff --git a/src/lib/exporter/modernFrameRenderer.ts b/src/lib/exporter/modernFrameRenderer.ts index 529b2109..892a5338 100644 --- a/src/lib/exporter/modernFrameRenderer.ts +++ b/src/lib/exporter/modernFrameRenderer.ts @@ -82,6 +82,7 @@ import { clampMediaTimeToDuration, getEffectiveVideoStreamDurationSeconds, } from "@/lib/mediaTiming"; +import { destroyPixiApplication, initializePixiApplication } from "@/lib/pixiApplicationLifecycle"; import { isVideoWallpaperSource } from "@/lib/wallpapers"; import { type AnnotationRenderAssets, @@ -300,7 +301,7 @@ async function initApplicationWithTimeout( }); try { - await Promise.race([app.init(options), timeoutPromise]); + await Promise.race([initializePixiApplication(app, options), timeoutPromise]); } finally { if (timeoutId !== undefined) { clearTimeout(timeoutId); @@ -754,7 +755,7 @@ export class FrameRenderer { `[FrameRenderer] ${backend} export renderer unavailable (${rendererMessage}) after ${elapsed}ms; trying next backend:`, error, ); - app.destroy(true); + destroyPixiApplication(app, `${backend} export renderer initialization`); } } @@ -3921,11 +3922,7 @@ export class FrameRenderer { this.motionBlurFilter?.destroy(); this.backgroundBlurFilter?.destroy(); - this.app?.destroy(true, { - children: true, - texture: false, - textureSource: false, - }); + destroyPixiApplication(this.app, "Lightning export renderer"); for (const texture of texturesToDestroy) { try { diff --git a/src/lib/pixiApplicationLifecycle.test.ts b/src/lib/pixiApplicationLifecycle.test.ts new file mode 100644 index 00000000..53fd09ee --- /dev/null +++ b/src/lib/pixiApplicationLifecycle.test.ts @@ -0,0 +1,92 @@ +import type { Application } from "pixi.js"; +import { describe, expect, it, vi } from "vitest"; +import { destroyPixiApplication, initializePixiApplication } from "./pixiApplicationLifecycle"; + +function createApplication(init: () => Promise = async () => undefined) { + return { + init: vi.fn(init), + destroy: vi.fn(), + stage: { destroy: vi.fn() }, + renderer: { destroy: vi.fn() }, + } as unknown as Application; +} + +describe("Pixi application lifecycle", () => { + it("cleans a failed initialization without running uninitialized plugins", async () => { + const initializationError = new Error("No available renderer"); + const app = createApplication(async () => { + throw initializationError; + }); + const applicationDestroy = vi.mocked(app.destroy); + applicationDestroy.mockImplementation(() => { + throw new TypeError("this._cancelResize is not a function"); + }); + + await expect(initializePixiApplication(app, {})).rejects.toBe(initializationError); + expect(() => destroyPixiApplication(app, "test renderer init")).not.toThrow(); + + expect(applicationDestroy).not.toHaveBeenCalled(); + expect(app.stage.destroy).toHaveBeenCalledWith({ + children: true, + texture: false, + textureSource: false, + }); + expect(app.renderer.destroy).toHaveBeenCalledWith({ + removeView: true, + releaseGlobalResources: false, + }); + }); + + it("destroys a successfully initialized application at most once", async () => { + const app = createApplication(); + + await initializePixiApplication(app, {}); + destroyPixiApplication(app, "test renderer"); + destroyPixiApplication(app, "test renderer"); + + expect(app.destroy).toHaveBeenCalledTimes(1); + expect(app.destroy).toHaveBeenCalledWith( + { removeView: true, releaseGlobalResources: false }, + { children: true, texture: false, textureSource: false }, + ); + }); + + it("defers teardown until an in-flight initialization settles", async () => { + let finishInitialization: (() => void) | undefined; + const app = createApplication( + () => + new Promise((resolve) => { + finishInitialization = resolve; + }), + ); + const initialization = initializePixiApplication(app, {}); + + destroyPixiApplication(app, "timed-out renderer init"); + expect(app.destroy).not.toHaveBeenCalled(); + + finishInitialization?.(); + await initialization; + + expect(app.destroy).toHaveBeenCalledTimes(1); + }); + + it("reports cleanup errors without throwing or retrying unsafe teardown", async () => { + const app = createApplication(); + const cleanupError = new Error("renderer cleanup failed"); + vi.mocked(app.destroy).mockImplementation(() => { + throw cleanupError; + }); + const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined); + + await initializePixiApplication(app, {}); + expect(() => destroyPixiApplication(app, "test renderer")).not.toThrow(); + expect(() => destroyPixiApplication(app, "test renderer")).not.toThrow(); + + expect(app.destroy).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith( + "[PixiApplication] Failed to clean up test renderer:", + cleanupError, + ); + warn.mockRestore(); + }); +}); diff --git a/src/lib/pixiApplicationLifecycle.ts b/src/lib/pixiApplicationLifecycle.ts new file mode 100644 index 00000000..87fdb648 --- /dev/null +++ b/src/lib/pixiApplicationLifecycle.ts @@ -0,0 +1,86 @@ +import type { Application } from "pixi.js"; + +type PixiInitializationState = "initializing" | "initialized" | "failed"; +type PixiInitOptions = Parameters[0]; + +const initializationStates = new WeakMap(); +const destroyRequests = new WeakSet(); +const destroyContexts = new WeakMap(); +const completedCleanups = new WeakSet(); + +const RENDERER_DESTROY_OPTIONS = { + removeView: true, + releaseGlobalResources: false, +} as const; + +const STAGE_DESTROY_OPTIONS = { + children: true, + texture: false, + textureSource: false, +} as const; + +function reportCleanupError(app: Application, error: unknown): void { + const context = destroyContexts.get(app) ?? "Pixi application"; + console.warn(`[PixiApplication] Failed to clean up ${context}:`, error); +} + +function destroyFailedApplication(app: Application): void { + const partialApp = app as Partial; + + try { + partialApp.stage?.destroy(STAGE_DESTROY_OPTIONS); + } catch (error) { + reportCleanupError(app, error); + } + + try { + partialApp.renderer?.destroy(RENDERER_DESTROY_OPTIONS); + } catch (error) { + reportCleanupError(app, error); + } +} + +function completeDestroy(app: Application): void { + if (completedCleanups.has(app)) return; + completedCleanups.add(app); + + if (initializationStates.get(app) !== "initialized") { + destroyFailedApplication(app); + return; + } + + try { + app.destroy(RENDERER_DESTROY_OPTIONS, STAGE_DESTROY_OPTIONS); + } catch (error) { + reportCleanupError(app, error); + } +} + +export async function initializePixiApplication( + app: Application, + options: PixiInitOptions, +): Promise { + if (initializationStates.has(app) || destroyRequests.has(app)) { + throw new Error("Pixi application lifecycle has already started"); + } + + initializationStates.set(app, "initializing"); + try { + await app.init(options); + initializationStates.set(app, "initialized"); + } catch (error) { + initializationStates.set(app, "failed"); + if (destroyRequests.has(app)) completeDestroy(app); + throw error; + } + + if (destroyRequests.has(app)) completeDestroy(app); +} + +export function destroyPixiApplication(app: Application | null, context: string): void { + if (!app || destroyRequests.has(app) || completedCleanups.has(app)) return; + + destroyRequests.add(app); + destroyContexts.set(app, context); + if (initializationStates.get(app) !== "initializing") completeDestroy(app); +} From d3ef314f568d195d729c9eb0f94815f77cb0c5a7 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Sat, 11 Jul 2026 11:51:06 +0700 Subject: [PATCH 2/2] refactor(pixi): centralize initialization timeout --- src/components/video-editor/VideoPlayback.tsx | 32 ++++--------------- src/lib/exporter/frameRenderer.ts | 32 ++++--------------- src/lib/exporter/modernFrameRenderer.ts | 32 ++++--------------- src/lib/pixiApplicationLifecycle.test.ts | 23 ++++++++++++- src/lib/pixiApplicationLifecycle.ts | 22 +++++++++++++ 5 files changed, 62 insertions(+), 79 deletions(-) diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 653e6161..7490f454 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -17,7 +17,10 @@ import { enablePitchPreservingPlayback, getMediaSyncPlaybackRate, } from "@/lib/mediaTiming"; -import { destroyPixiApplication, initializePixiApplication } from "@/lib/pixiApplicationLifecycle"; +import { + destroyPixiApplication, + initializePixiApplicationWithTimeout, +} from "@/lib/pixiApplicationLifecycle"; import { DEFAULT_WALLPAPER_PATH, DEFAULT_WALLPAPER_RELATIVE_PATH, @@ -260,30 +263,6 @@ function summarizeRendererAttempts(attempts: readonly PixiRendererAttempt[]): st return `No supported Pixi preview renderer was available. Attempted: ${details}`; } -type PixiInitOptions = Parameters[0]; - -async function initApplicationWithTimeout( - app: Application, - options: PixiInitOptions, - backend: PixiPreviewBackend, -): Promise { - const timeoutErrorMessage = `Initialization timed out after ${PIXI_RENDERER_INIT_TIMEOUT_MS}ms for ${backend} renderer`; - let timeoutId: ReturnType | undefined; - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => { - reject(new Error(timeoutErrorMessage)); - }, PIXI_RENDERER_INIT_TIMEOUT_MS); - }); - - try { - await Promise.race([initializePixiApplication(app, options), timeoutPromise]); - } finally { - if (timeoutId !== undefined) { - clearTimeout(timeoutId); - } - } -} - function getCursorPositionAtTime( telemetry: CursorTelemetryPoint[], timeMs: number, @@ -681,7 +660,7 @@ const VideoPlayback = forwardRef( const initStarted = typeof performance === "undefined" ? Date.now() : performance.now(); try { - await initApplicationWithTimeout( + await initializePixiApplicationWithTimeout( rendererApp, { width: container.clientWidth, @@ -695,6 +674,7 @@ const VideoPlayback = forwardRef( autoStart: true, sharedTicker: false, }, + PIXI_RENDERER_INIT_TIMEOUT_MS, backend, ); const elapsed = Math.round( diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 6f891628..018bd176 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -72,7 +72,10 @@ import { clampMediaTimeToDuration, getEffectiveVideoStreamDurationSeconds, } from "@/lib/mediaTiming"; -import { destroyPixiApplication, initializePixiApplication } from "@/lib/pixiApplicationLifecycle"; +import { + destroyPixiApplication, + initializePixiApplicationWithTimeout, +} from "@/lib/pixiApplicationLifecycle"; import { isVideoWallpaperSource } from "@/lib/wallpapers"; import { renderAnnotations } from "./annotationRenderer"; import { renderCaptions } from "./captionRenderer"; @@ -174,30 +177,6 @@ function toErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error ?? "Unknown renderer init error"); } -type PixiInitOptions = Parameters[0]; - -async function initApplicationWithTimeout( - app: Application, - options: PixiInitOptions, - backend: ExportRenderBackend, -): Promise { - const timeoutErrorMessage = `Initialization timed out after ${PIXI_RENDERER_INIT_TIMEOUT_MS}ms for ${backend} renderer`; - let timeoutId: ReturnType | undefined; - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => { - reject(new Error(timeoutErrorMessage)); - }, PIXI_RENDERER_INIT_TIMEOUT_MS); - }); - - try { - await Promise.race([initializePixiApplication(app, options), timeoutPromise]); - } finally { - if (timeoutId !== undefined) { - clearTimeout(timeoutId); - } - } -} - function summarizeRendererAttempts(attempts: readonly PixiRendererAttempt[]): string { const details = attempts.map((attempt) => `${attempt.backend}: ${attempt.message}`).join(" | "); return `No supported Pixi export backend was available. Attempted: ${details}`; @@ -359,12 +338,13 @@ export class FrameRenderer { const app = new Application(); const initStarted = typeof performance === "undefined" ? Date.now() : performance.now(); try { - await initApplicationWithTimeout( + await initializePixiApplicationWithTimeout( app, { ...baseOptions, preference: backend, }, + PIXI_RENDERER_INIT_TIMEOUT_MS, backend, ); const elapsed = Math.round( diff --git a/src/lib/exporter/modernFrameRenderer.ts b/src/lib/exporter/modernFrameRenderer.ts index 892a5338..33aad0dd 100644 --- a/src/lib/exporter/modernFrameRenderer.ts +++ b/src/lib/exporter/modernFrameRenderer.ts @@ -82,7 +82,10 @@ import { clampMediaTimeToDuration, getEffectiveVideoStreamDurationSeconds, } from "@/lib/mediaTiming"; -import { destroyPixiApplication, initializePixiApplication } from "@/lib/pixiApplicationLifecycle"; +import { + destroyPixiApplication, + initializePixiApplicationWithTimeout, +} from "@/lib/pixiApplicationLifecycle"; import { isVideoWallpaperSource } from "@/lib/wallpapers"; import { type AnnotationRenderAssets, @@ -285,30 +288,6 @@ function isKnownRendererUnavailableError(error: unknown): boolean { ); } -type PixiInitOptions = Parameters[0]; - -async function initApplicationWithTimeout( - app: Application, - options: PixiInitOptions, - backend: ExportRenderBackend, -): Promise { - const timeoutErrorMessage = `Initialization timed out after ${PIXI_RENDERER_INIT_TIMEOUT_MS}ms for ${backend} renderer`; - let timeoutId: ReturnType | undefined; - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => { - reject(new Error(timeoutErrorMessage)); - }, PIXI_RENDERER_INIT_TIMEOUT_MS); - }); - - try { - await Promise.race([initializePixiApplication(app, options), timeoutPromise]); - } finally { - if (timeoutId !== undefined) { - clearTimeout(timeoutId); - } - } -} - interface RenderSnapshot { timeMs: number; cursorTimeMs: number; @@ -721,12 +700,13 @@ export class FrameRenderer { const app = new Application(); const initStarted = typeof performance === "undefined" ? Date.now() : performance.now(); try { - await initApplicationWithTimeout( + await initializePixiApplicationWithTimeout( app, { ...baseOptions, preference: backend, }, + PIXI_RENDERER_INIT_TIMEOUT_MS, backend, ); const elapsed = Math.round( diff --git a/src/lib/pixiApplicationLifecycle.test.ts b/src/lib/pixiApplicationLifecycle.test.ts index 53fd09ee..6b576242 100644 --- a/src/lib/pixiApplicationLifecycle.test.ts +++ b/src/lib/pixiApplicationLifecycle.test.ts @@ -1,6 +1,10 @@ import type { Application } from "pixi.js"; import { describe, expect, it, vi } from "vitest"; -import { destroyPixiApplication, initializePixiApplication } from "./pixiApplicationLifecycle"; +import { + destroyPixiApplication, + initializePixiApplication, + initializePixiApplicationWithTimeout, +} from "./pixiApplicationLifecycle"; function createApplication(init: () => Promise = async () => undefined) { return { @@ -89,4 +93,21 @@ describe("Pixi application lifecycle", () => { ); warn.mockRestore(); }); + + it("reports the backend when initialization times out", async () => { + vi.useFakeTimers(); + try { + const app = createApplication(() => new Promise(() => undefined)); + const initialization = initializePixiApplicationWithTimeout(app, {}, 250, "webgpu"); + const rejection = initialization.catch((error: unknown) => error); + + await vi.advanceTimersByTimeAsync(250); + + await expect(rejection).resolves.toEqual( + new Error("Initialization timed out after 250ms for webgpu renderer"), + ); + } finally { + vi.useRealTimers(); + } + }); }); diff --git a/src/lib/pixiApplicationLifecycle.ts b/src/lib/pixiApplicationLifecycle.ts index 87fdb648..62a0f743 100644 --- a/src/lib/pixiApplicationLifecycle.ts +++ b/src/lib/pixiApplicationLifecycle.ts @@ -77,6 +77,28 @@ export async function initializePixiApplication( if (destroyRequests.has(app)) completeDestroy(app); } +export async function initializePixiApplicationWithTimeout( + app: Application, + options: PixiInitOptions, + timeoutMs: number, + backendLabel: string, +): Promise { + let timeoutId: ReturnType | undefined; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject( + new Error(`Initialization timed out after ${timeoutMs}ms for ${backendLabel} renderer`), + ); + }, timeoutMs); + }); + + try { + await Promise.race([initializePixiApplication(app, options), timeoutPromise]); + } finally { + if (timeoutId !== undefined) clearTimeout(timeoutId); + } +} + export function destroyPixiApplication(app: Application | null, context: string): void { if (!app || destroyRequests.has(app) || completedCleanups.has(app)) return;