Gessa Docs
Product · Explanation

Concept

Explanation: Player Browser Runtime

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.
engine v1.0.234since v1Copy for LLM
engine v1.0.232

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 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 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; this page is the client half.

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:

Pseudocode
rendered = simulation + offset

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

offset*=exp(-dtmsτ(magnitude))

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 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.

Was this helpful?Report an issueContact support

On this page