Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 63 additions & 52 deletions src/core/platforms/web/WebPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,24 @@ export class WebPlatform extends Platform {
override startLoop(stage: Stage): void {
let isIdle = false;
let lastFrameTime = 0;
// Set by `tick` once it has queued the next frame; read by `runLoop`'s catch
// so a throw after scheduling never double-schedules (which would compound
// into runaway frames if it threw every frame). Reset at the top of each frame.
let scheduled = false;
const buffer = 4;

const runLoop = (currentTime: number = 0) => {
const requestLoop = () => requestAnimationFrame(runLoop);

// All per-frame work lives in `tick`, deliberately split out of `runLoop`.
// `runLoop` carries the try/catch that keeps the loop alive, and some
// optimizing compilers — notably Chrome 38's Crankshaft, our language floor —
// refuse to optimize any function that contains a try/catch. Keeping that
// guard in a near-empty shell confines the de-opt to the shell: every hot
// operation (the `stage.*` calls below, run every frame) stays in fully
// optimizable functions. `tick` is also where all client-supplied code runs
// (synchronous event subscribers, the events drained by `flushFrameEvents`,
// and animation steps) — i.e. everything that can actually throw.
const tick = (currentTime: number) => {
// The GL context is lost and the engine does not rebuild it in place.
// Stop the loop entirely (no reschedule) so we issue no GL calls against
// a dead context. Recovery is via app reload (see the `contextLost` event).
Expand All @@ -55,6 +70,7 @@ export class WebPlatform extends Platform {
} else {
requestAnimationFrame(runLoop);
}
scheduled = true;
return;
}

Expand All @@ -64,65 +80,62 @@ export class WebPlatform extends Platform {
lastFrameTime = currentTime;
}

// From here on the frame runs client-supplied code: synchronous event
// subscribers (`frameTick`, `idle`, and the queued events drained by
// `flushFrameEvents`) and animation steps. A throw in any of these would
// otherwise propagate out of the rAF callback and permanently stop the
// render loop — the whole app freezes until reload. Guard the body and
// always keep the loop alive: hand the error to `handleLoopError` (a
// no-op by default; the app can override it to log/report) and reschedule.
// `scheduled` tracks whether the next tick was already queued before the
// throw so we never double-schedule (which would compound into runaway
// frames if it threw every frame).
let scheduled = false;
try {
stage.updateFrameTime();
const hasActiveAnimations = stage.updateAnimations();

if (!stage.hasSceneUpdates()) {
// We still need to calculate the fps else it looks like the app is frozen
stage.calculateFps();
stage.updateFrameTime();
const hasActiveAnimations = stage.updateAnimations();

// 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(requestLoop, Math.max(targetFrameTime, 15));
scheduled = true;
if (!stage.hasSceneUpdates()) {
// We still need to calculate the fps else it looks like the app is frozen
stage.calculateFps();

if (isIdle === false) {
// The render burst has settled. Probe for a GPU out-of-memory now
// rather than every frame: GL errors accumulate and persist until
// drained, so a single check here still catches any OOM raised during
// the active frames, without paying the getError() CPU/GPU sync on
// every frame. Queues the `outOfMemory` event, flushed below.
if (stage.renderer.checkForOutOfMemory() === true) {
stage.txMemManager.handleOutOfMemory();
}
stage.shManager.cleanup();
stage.cleanupTextRenderers();
stage.eventBus.emit('idle');
isIdle = true;
}
// 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(requestLoop, Math.max(targetFrameTime, 15));
scheduled = true;

if (stage.txMemManager.checkCleanup() === true) {
stage.txMemManager.cleanup();
if (isIdle === false) {
// The render burst has settled. Probe for a GPU out-of-memory now
// rather than every frame: GL errors accumulate and persist until
// drained, so a single check here still catches any OOM raised during
// the active frames, without paying the getError() CPU/GPU sync on
// every frame. Queues the `outOfMemory` event, flushed below.
if (stage.renderer.checkForOutOfMemory() === true) {
stage.txMemManager.handleOutOfMemory();
}
stage.shManager.cleanup();
stage.cleanupTextRenderers();
stage.eventBus.emit('idle');
isIdle = true;
}

stage.flushFrameEvents();
return;
if (stage.txMemManager.checkCleanup() === true) {
stage.txMemManager.cleanup();
}

isIdle = false;
stage.drawFrame(hasActiveAnimations);
stage.flushFrameEvents();
return;
}

// Schedule next frame
requestAnimationFrame(runLoop);
scheduled = true;
isIdle = false;
stage.drawFrame(hasActiveAnimations);
stage.flushFrameEvents();

// Schedule next frame
requestAnimationFrame(runLoop);
scheduled = true;
};

const runLoop = (currentTime: number = 0) => {
// `tick` runs client-supplied code every frame; a throw there would
// otherwise propagate out of the rAF callback and permanently stop the
// render loop — the whole app freezes until reload. Guard it and always
// keep the loop alive: hand the error to `handleLoopError` (a no-op by
// default; the app can override it to log/report) and reschedule unless
// `tick` already queued the next frame before throwing.
scheduled = false;
try {
tick(currentTime);
} catch (error: unknown) {
// Report the error (default handler is a no-op), then keep the loop
// alive — a single bad frame must never freeze the app. Skip the
// reschedule if this frame already queued the next tick before throwing.
const handleLoopError = stage.options.handleLoopError;
if (handleLoopError !== undefined) {
handleLoopError(error);
Expand All @@ -133,8 +146,6 @@ export class WebPlatform extends Platform {
}
};

const requestLoop = () => requestAnimationFrame(runLoop);

requestAnimationFrame(runLoop);
}

Expand Down
Loading