Skip to main content

Runtime Frame Loop

GameRuntime is the host-owned frame loop for Hosanna games. It is intentionally small: it ticks engine services, drains input, gives one controller a chance to update, asks that controller to render, draws optional debug overlays, and presents the screen.

Game authors normally do not subclass or replace GameRuntime. Put game flow in an Hs2dGame, put screens in scenes, and put gameplay in Hs2dLevelScene. The runtime is the boundary between the platform host and the game controller.

Frame Order

Every accepted frame runs in this order:

GameRuntime frame flow from raw service ticks through throttling, input, Hs2d update hooks, rendering, presentation, and gated telemetryGameRuntime frame flow from raw service ticks through throttling, input, Hs2d update hooks, rendering, presentation, and gated telemetry

Services use every host delta, including a tick skipped by the 30 FPS gameplay throttle. Input, controller update, render, presentation, and runtime performance sampling use only the accepted frameDeltaMs.

The update phase always happens before render. Game logic should mutate state during update, then render should compose the state that already exists.

Runtime Responsibilities

GameRuntime owns these concerns:

ConcernRuntime behavior
Frame acceptanceRuns every 60 FPS tick by default, or accumulates deltas when setTargetFps(30) is active.
ServicesTicks system, timer, and audio services before gameplay input/update.
InputTicks the input manager, drains normalized GameInput objects, and handles runtime debug shortcuts first.
Controller dispatchCalls controller.update(deltaMs, inputs) and then controller.render(screen).
PresentationCalls screen.Present() when available, otherwise SwapBuffers().
Debug overlayIn a __DEV__ build, renders collision debug colliders after the controller unless the current controller handles debug rendering itself.
StatsIn a __TELEMETRY__ build, tracks frame distribution and update, render, and present timings.
Controller lifetimeDisposes the previous controller when setController() installs a replacement controller.

Game code owns scene selection, world state, entity logic, and drawing policy. The runtime only coordinates the frame.

Controller Wiring

The host creates the runtime once, then installs a game controller. The controller is usually an Hs2dGame subclass.

export class MyGameHost {
private readonly runtime: GameRuntime;

constructor(screen: GameScreen, inputManager: GameInputAdapterManager, context: MyGameContext) {
this.runtime = new GameRuntime({
screen,
inputManager,
timerService: context.timerService,
audioManager: context.audioManager,
});
this.runtime.setController(new MyGame(context));
}

tick(deltaMs: number): void {
this.runtime.tick(deltaMs);
}
}

Keep this host layer thin. If a menu opens a level, use Hs2dGame.replaceScene(). If a pause panel opens, use Hs2dGame.pushScene(). Do not swap the runtime controller for ordinary game screens.

Target FPS

The runtime defaults to 60 FPS. Calling setTargetFps(30) makes the runtime accumulate incoming deltas until the 30 FPS interval is reached. A skipped gameplay tick still advances system, timer, and audio services; it returns before input, update, render, or presentation.

class GraphicsOptionsScene extends Hs2dScene {
constructor(private readonly runtime: GameRuntime) {
super();
}

setPerformanceMode(isLowPower: boolean): void {
if (isLowPower) {
this.runtime.setTargetFps(30);
return;
}
this.runtime.setTargetFps(60);
}
}

Use 30 FPS as a deliberate product choice, not as a way to hide an expensive frame loop. New gameplay should still follow the performance covenant: fixed pools, cached surfaces, one camera, and no steady-state allocation.

Runtime Input Handling

In a __DEV__ build, the runtime sees input before the game controller and uses the options button to toggle collision debug rendering when the input is neither a release nor a held repeat. That shortcut is compiled out of non-development builds. If runtime or host input handles the frame, controller update is skipped for that frame.

Hosts may also install a runtime input handler:

this.runtime.setInputHandler((deltaMs, inputs) => {
void deltaMs;
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
if (input.release || input.held) continue;
if (input.isButton('back')) {
this.navigateToShellMenu();
return true;
}
}
return false;
});

Use this for host-level concerns that sit above the game. Use scene handleInputs() for gameplay and menu actions.

Controller Changes During A Frame

GameRuntime.tick() captures the controller before update, then reads the controller again before render. This lets an update replace the controller and have the new controller render the same frame.

That behavior is useful for host-level transitions, but normal game flow should stay inside Hs2dGame:

  • replaceScene(scene) for menu to level, level to game over, or back to title.
  • pushScene(scene) for pause, controller pairing, diagnostics, or modal overlays.
  • popScene() for returning to the scene underneath.

If a scene transition happens during Hs2dGame.update(), the scene stack rules decide what renders and what updates. See Game And Scene Stack.

Present Phase

The controller renders into the supplied GameScreen. After controller render:

  1. In a __DEV__ build, collision debug colliders render unless the controller reports isCollisionDebugRenderingHandled().
  2. Present() is called when the screen exposes it.
  3. Otherwise, SwapBuffers() is called.
  4. In a __TELEMETRY__ build, runtime stats are updated with measured update, render, and present timing.

Do not call Present() or SwapBuffers() from a scene. Scenes draw; the runtime presents.

Performance Stats

getPerformanceStats() returns one retained object. Timing and distribution fields are populated only when __TELEMETRY__ is enabled.

Field groupMeaning
fpsLast completed one-second FPS window.
frameCountAccepted frames accumulated in the current, not-yet-published measurement window. It resets after each window.
targetFps, targetFrameMsActive target and its frame budget.
windowFrameCount, windowElapsedMsSize of the last published measurement window.
lastFrameMs, lastUpdateMs, lastRenderMs, lastPresentMsMost recent accepted-frame delta and measured phases.
frameDeltaP50Ms, frameDeltaP95Ms, frameDeltaP99Ms, frameDeltaMaxMsFrame-delta distribution for the last published window.
overBudgetFrameCount, missedFrameBudgetCount, slowFrameCountBudget pressure in the last window.
consecutiveSlowFrameCount, maxConsecutiveSlowFramesCurrent and maximum slow-frame streaks.
shortFrameCount, longFrameCount, bunchedFrameCount, shortLongFrameCountDelivery-shape diagnostics for short/long and alternating frames.
averageUpdateMs, maxUpdateMs, averageRenderMs, maxRenderMs, averagePresentMs, maxPresentMsPhase averages and maxima for the last published window.

These stats describe runtime phases. Use Hs2dTelemetry inside level scenes when you need finer breakdowns such as projectile update, sprite sync, particle update, or world render.

Do And Don't

Do:

  • Let GameRuntime own frame presentation.
  • Install one Hs2dGame controller for normal game flow.
  • Keep platform services ticking before gameplay update.
  • Treat deltaMs as the single source of frame time.
  • Read runtime stats when diagnosing frame budget issues.

Don't:

  • Call scene update() or render() directly from host code.
  • Present the screen from a scene or layer.
  • Create a new runtime to open a pause menu, level, or modal.
  • Put game-specific input shortcuts in the runtime unless they are truly host-level.
  • Rely on 30 FPS throttling to compensate for avoidable per-frame allocation or drawing.

Source References

SourceRuntime facts to verify
../games/hosanna-ui/src/hosanna-game/runtime/GameRuntime.tsTick order, FPS throttling, input drain, controller dispatch, debug collision render, present/swap, performance stats, controller disposal.
../games/hosanna-ui/src/hosanna-game/hosanna2d/game/Hs2dGame.tsNormal controller implementation used by Hs2d games.
../games/hosanna-ui/src/hosanna-game/hosanna2d/game/Hs2dScene.tsScene update/render surface consumed by Hs2dGame.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterGame.tsMinimal sample controller installed by a host.
Talk to us