diff --git a/src/core/platforms/Platform.ts b/src/core/platforms/Platform.ts index be02710a..b090a7ba 100644 --- a/src/core/platforms/Platform.ts +++ b/src/core/platforms/Platform.ts @@ -42,6 +42,16 @@ export interface PlatformSettings { * Optional provided canvas element to use for rendering. If not provided, the platform will create its own canvas element. */ canvas?: HTMLCanvasElement | null; + + /** + * Request Animation Frame Error Callback + * + * @remarks + * If the render loop throws an error, this callback will be called. + * + * @param error The error that was thrown + */ + handleLoopError?: (error: unknown) => void; } export abstract class Platform { diff --git a/src/core/platforms/web/WebPlatform.ts b/src/core/platforms/web/WebPlatform.ts index 1024e1be..77588c22 100644 --- a/src/core/platforms/web/WebPlatform.ts +++ b/src/core/platforms/web/WebPlatform.ts @@ -120,46 +120,57 @@ export class WebPlatform extends Platform { return; } - stage.updateFrameTime(); - stage.updateAnimations(); - - if (!stage.hasSceneUpdates()) { - // We still need to calculate the fps else it looks like the app is frozen - stage.calculateFps(); - - if (targetFrameTime > 0) { - // Use setTimeout for throttled idle frames - setTimeout( - () => requestAnimationFrame(runLoop), - Math.max(targetFrameTime, 16.666666666666668), - ); - } else { - // Use standard idle timeout when not throttling - setTimeout(() => requestAnimationFrame(runLoop), 16.666666666666668); - } + try { + stage.updateFrameTime(); + stage.updateAnimations(); + + if (!stage.hasSceneUpdates()) { + // We still need to calculate the fps else it looks like the app is frozen + stage.calculateFps(); + + if (targetFrameTime > 0) { + // Use setTimeout for throttled idle frames + setTimeout( + () => requestAnimationFrame(runLoop), + Math.max(targetFrameTime, 16.666666666666668), + ); + } else { + // Use standard idle timeout when not throttling + setTimeout( + () => requestAnimationFrame(runLoop), + 16.666666666666668, + ); + } + + if (isIdle === false) { + stage.shManager.cleanup(); + stage.eventBus.emit('idle'); + isIdle = true; + } + + if (stage.txMemManager.checkCleanup() === true) { + stage.txMemManager.cleanup(); + } - if (isIdle === false) { - stage.shManager.cleanup(); - stage.eventBus.emit('idle'); - isIdle = true; + stage.flushFrameEvents(); + return; } - if (stage.txMemManager.checkCleanup() === true) { - stage.txMemManager.cleanup(); + if (isIdle === true) { + stage.eventBus.emit('active'); + isIdle = false; } + stage.drawFrame(); stage.flushFrameEvents(); - return; - } - - if (isIdle === true) { - stage.eventBus.emit('active'); - isIdle = false; + } catch (error: unknown) { + if (this.settings.handleLoopError) { + this.settings.handleLoopError(error); + } else { + throw error; + } } - stage.drawFrame(); - stage.flushFrameEvents(); - // Schedule next frame if (targetFrameTime > 0) { // Use setTimeout + rAF combination for precise FPS control diff --git a/src/main-api/Renderer.ts b/src/main-api/Renderer.ts index 5facc967..8b99950e 100644 --- a/src/main-api/Renderer.ts +++ b/src/main-api/Renderer.ts @@ -552,6 +552,7 @@ export class RendererMain extends EventEmitter { createImageBitmapSupport: settings.createImageBitmapSupport || 'full', platform: settings.platform || WebPlatform, maxRetryCount: settings.maxRetryCount ?? 5, + handleLoopError: settings.handleLoopError, }; const {