---
title: "Explanation: Player Browser Runtime"
description: "How the browser play client admits a deployment, mounts, predicts the local player's owned kinematic movement, reconciles against server authority, interpolates remote entities, and smooths the rendered pose."
engineVersion: v1.0.234
date: 2026-09-28
license: "(c) Gessa, proprietary. Cite with attribution to https://gessa.ai/docs/. Terms: https://gessa.ai/terms/."
canonical: https://gessa.ai/docs/explanation/player-browser-runtime/
---

# Explanation: Player Browser Runtime

{% version engineVersion="v1.0.232" coordinate="docs-public.v0" /%}

_Last verified 2026-09-03 against engine v1.0.232._

This page awaits re-extraction under decision D26 of the docs program; treat every number on it as provisional until its dossier receipt is stamped.

When a player opens a published world, a browser client admits the deployment, mounts the runtime, connects, and starts rendering a projection of the server's authoritative room. In a few tightly bounded cases it predicts ahead of that authority so the local player feels no round-trip latency. This page explains that client: how it admits and mounts, what it is allowed to predict, how it keeps a predicted pose honest against authority, how it shows remote players, and what it deliberately does not do. It frames the **why**. The [Runtime Playability reference](../reference/runtime-playability.md) defines the surrounding proof contract. Detailed source evidence remains in the repository's `docs/spec/dossiers/player-browser-runtime/dossier.yaml`; a dedicated public reference has not been published.

Two framings from [Backend Authority](backend-authority.md) carry through everything here: the server owns the world, and the client is a non-authoritative view of it. The player runtime is where that view is rendered, and prediction is the narrow, auditable exception to it. The server side of the same discipline (the room clock, authority lease, replication, and load ladders) is the [Netcode Model](netcode-model.md); this page is the client half.

{% warning severity="info" title="Two subsystems share the name browser runtime" %}
`server/src/modules/browser-runtime` is a server-side capability boundary that lets only an image carrying a Playwright and Chromium payload run headless-browser workloads (asset preview capture, renderer proof). It is not the player's browser and touches no play, prediction, or netcode path. This page is about the **player** browser runtime; the server boundary appears here only so the two are not confused. See `loadDedicatedPlaywright`.
{% /warning %}

## Admission before the heavy runtime

The dependency-light entry `admitPlayerEntry` runs first. It resolves the deployment id from the injected play context or the URL, and it takes one of two paths. When the server injected a runtime config that matches the deployment, it starts the runtime immediately. Otherwise it fetches the deployment runtime-config endpoint, confirms the returned deployment id matches, and only then continues. If the deployment id is missing, or the fetch returns an unauthorized, forbidden, or not-found status, or the network drops, `playerAdmissionFailure` classifies the error and renders a typed, retryable failure surface instead of a broken canvas.

Only after admission proves a deployment exists does `startRuntime` dynamically import the heavy runtime entry and warm the runtime shell chunk, then call `mountPlayerRuntime`. So a bad link never pays the cost of loading the engine.

`mountPlayerRuntime` pins the play palette dark for every visitor (a played game looks identical regardless of the viewer's studio appearance), installs the desktop window chrome, and mounts `PlayerRuntimeHost` under a crash boundary. The host lazy-loads `PlayerRuntimeShell` beneath it.

## Prediction is a generic deterministic loop

The client does not run gameplay-specific prediction code. It runs `Predictor`, a generic loop over a deterministic `step` function that the client and the server supply **identically**. Because both sides advance the same `step` over the same command, a predicted state and its authoritative counterpart are bit-identical by construction rather than by careful matching. `OwnedCharacterPredictor` is the live specialization: its `step` is the shared `stepCharacter` kinematic-character kernel, the same one the server tick runs.

A tick of prediction records the command on an input tape and advances the simulation once. When authority arrives, `reconcile` restores the authoritative baseline and replays the unacknowledged tail of the tape through the same `step`, so the local player keeps its lead instead of snapping back to a stale server position. The replay is trimmed in the **server tick domain**, not the input-sequence domain, because a held key tapes one command per tick under a single sequence number, and trimming by sequence would erase exactly the lead prediction exists to hold. That trim lives in `InputTimeline.trimToAuthorityReceipt`.

When authority hands the client an incompatible tape (a controller reconfiguration, say), `rebaseHistory` retires it without a visible snap, a continuous handoff distinct from the hard `reset` that a declared discontinuity triggers.

## Simulation truth and what you see are different things

The simulation position snaps to authority. The **rendered** pose does not; it trails the simulation by a smoothed offset so a correction is absorbed over a few frames instead of popping in one. The rendered pose is the simulation position plus that offset:

```math
rendered = simulation + offset
```

The offset decays every frame by a time-normalized exponential law:

```math
offset \mathrel{*}= \exp\!\left(-\,\frac{dt_{ms}}{\tau(\text{magnitude})}\right)
```

The subtle part is that `tau` is graded on the offset magnitude **fixed at the moment of the last correction**, held constant between corrections, not on the live shrinking magnitude. That is what makes the smoothing frame-rate-independent: the product of the per-frame decay factors over a wall-time window is `exp(-totalMs/tau)` no matter how many frames subdivide the window, so a player at a high refresh rate and a player at a low one see the same settle time. Grading on the live magnitude would let `tau` drift as the offset shrank and reintroduce frame-rate dependence. This is INV-11, and it is enforced by `advancePresentation`.

The offset has no arbitrary setter. The only path that snaps it to zero is a **teleport**-severity correction, and a teleport is not a matter of distance alone. `classifyCorrection` returns teleport only when the correction carries **both** a declared discontinuity bit **and** a distance at or above the threshold. A server-authoritative launch can legitimately be metres ahead by the time its first pose arrives; treating that as a teleport would manufacture the one-frame pop the smoother exists to prevent.

## Three cadences, one owned predictor

Input, simulation, and presentation run on separate clocks. The input dispatcher latches controller state as the player presses keys. A fixed clock samples the latest latched state once per due step and advances the predictor exactly one tick. Rendering interpolates between the previous and current fixed states and then adds the decaying offset. `OwnedEntityPredictor` bridges these cadences to the shared core, and the whole owned predictor is built from the accepted netcode contract and the replicated collision snapshot by `buildOwnedPredictorConfig`, per entity from its replicated components.

Because the simulation must not stall when rendering does, the client steps the predictor on **wall time**, decoupled from `requestAnimationFrame`. `SimClockDriver` re-arms against an absolute target, so a late or coalesced animation-frame wake self-corrects instead of dropping simulation steps and manufacturing a large reconcile. The render path only samples the predicted pose; it never steps the simulation. A backgrounded tab keeps neither clock on real time, so the sim clock declares a discontinuity once it exceeds its hidden budget rather than sprinting through a backlog on return.

The owned predictor is rebuilt when the entity id **or** its config revision changes. A movement-component edit (gravity, jump, capsule, max speed) or a contract swap changes the revision and forces a rebuild through the discontinuity path, so the client never keeps predicting with stale parameters behind an id-only cache. That is INV-13, in `ClientPredictionState.ensureOwnedPredictor`.

## Remote players are interpolated, never guessed

Entities the client does not own are rendered behind an interpolation delay in `InterpolationBuffer`, which derives each stream's velocity by finite difference. When a stream stalls, the buffer bridges the gap by extrapolating along the last authoritative velocity, but only up to a bounded cap the contract supplies, and then it **halts** at the newest sample rather than inventing unbounded motion. A contract cap of zero disables extrapolation entirely, so a profile can choose to hug the newest sample and never overshoot.

## What the client is not allowed to do

The client predicts kinematic character movement and nothing else, by design.

- **No dynamic-rigidbody prediction.** A physics-owned body is server-authoritative; predicting it would need a rollback physics contract the engine does not ship, so a body owned by physics is interpolated from authority instead.
- **No client-side scripted-effect prediction.** Client-side script prediction is removed. An owned command carries movement intent only, and scripted effects arrive server-authoritatively through reconcile or interpolation. The `OwnedCommand` type is the live truth here even though an older comment above the predictor still describes a predicted-op lane; the comment is stale and the type is not.
- **No client script executor or op applicator.** The prediction and input graph cannot import a script executor at all.

These are not conventions; they are static gates that fail the build if the prediction path grows an import it should not have. The repository dossier records those gates and their source evidence; the [Runtime Playability reference](../reference/runtime-playability.md) explains the required proof classes.

## A second runtime, built but not yet live

A parallel refactor of the play client exists in the tree: a dependency-DAG rebuild with a leaf contracts layer at the root, `createPlayerCore` as the single stateless composition root, and `PlayerRuntimeShell` as the one shell that instantiates it over the existing surface. The shell composes the core through port adapters that **wrap** the live surface rather than starting a second transport, world, or connection, so composing it changes no runtime output today. The bounded lane transport and canonical world store that belong to that foundation are isolated and gated, and they are **not** the live transport or store; the live pipeline is still the gateway hook plus `ClientPrediction`. The repository dossier distinguishes these staged foundation surfaces from the live path.

## How this connects to playability

A world is playable only when runtime authority, not a rendered scene, demonstrates the claimed behavior under a live session. The player runtime is the client half of that demonstration: it admits, mounts, connects, predicts the owned player, and renders authority for everyone else. For what formally counts as playable and how it is proven, see [Runtime Playability](../reference/runtime-playability.md).
