From 67e8ff76f6f9195520e52efead98e8403a18a3df Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Tue, 13 Jan 2026 00:09:49 -0500 Subject: [PATCH 1/2] fix targetFPS calculations --- src/core/platforms/web/WebPlatform.ts | 35 ++++++++++++--------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/core/platforms/web/WebPlatform.ts b/src/core/platforms/web/WebPlatform.ts index 53263ce6a..e44757f21 100644 --- a/src/core/platforms/web/WebPlatform.ts +++ b/src/core/platforms/web/WebPlatform.ts @@ -33,15 +33,20 @@ export class WebPlatform extends Platform { const runLoop = (currentTime: number = 0) => { const targetFrameTime = stage.targetFrameTime; - // Check if we should throttle this frame - if ( - targetFrameTime > 0 && - currentTime - lastFrameTime < targetFrameTime - ) { - // Too early for next frame, schedule with setTimeout for precise timing - const delay = targetFrameTime - (currentTime - lastFrameTime); - setTimeout(() => requestAnimationFrame(runLoop), delay); - return; + if (targetFrameTime > 0) { + // Calculate elapsed time since the last frame + const elapsed = currentTime - lastFrameTime; + + // If not enough time has passed, skip this frame + if (elapsed < targetFrameTime) { + requestAnimationFrame(runLoop); + return; + } + + // Adjust lastFrameTime to maintain the target FPS + lastFrameTime = currentTime - (elapsed % targetFrameTime); + } else { + lastFrameTime = currentTime; } stage.updateFrameTime(); @@ -81,17 +86,7 @@ export class WebPlatform extends Platform { stage.flushFrameEvents(); // Schedule next frame - if (targetFrameTime > 0) { - // Use setTimeout + rAF combination for precise FPS control - const nextFrameDelay = Math.max( - 0, - targetFrameTime - (performance.now() - currentTime), - ); - setTimeout(() => requestAnimationFrame(runLoop), nextFrameDelay); - } else { - // Use standard rAF when not throttling - requestAnimationFrame(runLoop); - } + requestAnimationFrame(runLoop); }; requestAnimationFrame(runLoop); } From 48f5fb5a6d1805b8f03960e7cb6a8ee74c478349 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Tue, 13 Jan 2026 15:00:19 -0500 Subject: [PATCH 2/2] update loop from feedback --- src/core/platforms/web/WebPlatform.ts | 43 +++++++++++++++++++-------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/core/platforms/web/WebPlatform.ts b/src/core/platforms/web/WebPlatform.ts index e44757f21..b294c6071 100644 --- a/src/core/platforms/web/WebPlatform.ts +++ b/src/core/platforms/web/WebPlatform.ts @@ -29,6 +29,7 @@ export class WebPlatform extends Platform { override startLoop(stage: Stage): void { let isIdle = false; let lastFrameTime = 0; + const buffer = 4; const runLoop = (currentTime: number = 0) => { const targetFrameTime = stage.targetFrameTime; @@ -39,7 +40,13 @@ export class WebPlatform extends Platform { // If not enough time has passed, skip this frame if (elapsed < targetFrameTime) { - requestAnimationFrame(runLoop); + const wait = targetFrameTime - elapsed; + + if (wait > buffer) { + setTimeout(() => requestAnimationFrame(runLoop), wait - buffer); + } else { + requestAnimationFrame(runLoop); + } return; } @@ -56,16 +63,13 @@ export class WebPlatform extends Platform { // 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); - } + // We use 15ms instead of 16.6ms to provide a safety buffer. + // This ensures we wake up slightly before the next frame to check for updates, + // preventing us from missing a frame due to timer variances. + setTimeout( + () => requestAnimationFrame(runLoop), + Math.max(targetFrameTime, 15), + ); if (isIdle === false) { stage.shManager.cleanup(); @@ -86,7 +90,22 @@ export class WebPlatform extends Platform { stage.flushFrameEvents(); // Schedule next frame - requestAnimationFrame(runLoop); + if (targetFrameTime > 0) { + const nextTarget = lastFrameTime + targetFrameTime; + const now = performance.now(); + const wait = nextTarget - now; + + // If we have a significant wait time, use setTimeout to yield to the browser. + // We subtract a small buffer (4ms) to ensure we wake up BEFORE the next frame. + if (wait > buffer) { + setTimeout(() => requestAnimationFrame(runLoop), wait - buffer); + } else { + requestAnimationFrame(runLoop); + } + } else { + // Use standard rAF when not throttling + requestAnimationFrame(runLoop); + } }; requestAnimationFrame(runLoop); }