Vestro.
▪ P—05 · Showcase / live demo

Reconcile.

A deterministic, off-thread demo of client-side prediction and server reconciliation in C++ and WebAssembly. Watch a pawn diverge under network lag — then snap back when the ack lands.

C++20 WebAssembly Emscripten Web Worker Canvas 2D
Jun 2026 Runs in your browser Live demo ↓
01 — What this is

Client-side prediction is one of those problems that looks simple until you try to ship it. The client runs the sim ahead of the server, the server runs the authoritative version, and every so often an acknowledgement — an ack — comes back and the client has to work out how much of its guess was wrong, then fix it without the player ever seeing the seam.

This demo drives a C++ World controller compiled to WebAssembly. It ticks a deterministic fixed-point simulation: the client predicts, the server runs authority, and the channel between them loses packets, reorders them, adds jitter. The cyan pawn you see is the client's best guess; the magenta one is where the server says it actually is. The strip under the arena is the acknowledgement history — which guesses the server has confirmed, and when.

It's modeled on how Unreal Engine handles prediction and reconciliation — the prediction-key and server-reconciliation ideas behind FPredictionKey, FScopedPredictionWindow, and Character Movement's save-and-replay — rebuilt from scratch in C++20 for the browser. Every input the client sends carries a prediction key, a little ticket; the server processes it and sends that key's ack back as confirmation the guess for that moment held. Until the ack returns, the client is only predicting — and how it books those acks is the whole game. Get the bookkeeping wrong and it quietly believes things the server never confirmed.

02 — Try it

Press Run. The cyan dot is the client's prediction; the magenta dot is the server's authority; the sliders set the lag and loss on the channel between them. What every control does — the ack model, the net modes, the timeline, input delay — is broken down in How it works below.

reconcile / world 16.6ms · 60fps
Tick
Divergence now
Divergence avg
Divergence peak
Corrections
Replays
Issued / in-flight
Acked keys
Mis-confirmed
Mode
Configured loss
Ack rate
Ack state
Legend
  • Client — predicted
  • Server — authoritative
  • Command (0-delay) — where your input points now
  • Ghost — reconcile target
  • Channel — inputs up, acks down
  • Issued
  • Inflight
  • Acked
  • Replayed
  • Mis-confirmed
  • Out-of-order ✓ — Range's SACK window
Press Run, then drive with WASD / arrows (or toggle the bot). client prediction · server authority · 60Hz
connecting…

Timeline
Simulation controls
Seed
Mode
Ack model click to cycle

confirms each input by its own key; survives loss, but its state grows with outstanding keys.

RTT ticks
Loss × 10 = %
Jitter ticks
Reorder × 10 = %
Duplicate × 10 = %
Speed ticks/sec
Input delay hold input N ticks · trades lag for lockstep 0
Advanced
Bandwidth bytes/tick
Live freeze or restart the sim
03 — How it works

Three roles, one deterministic tick. The client predicts, the server confirms, and the ack closes the loop.

The C++ World maintains two pawn states in the same tick: a client integrating player inputs immediately (prediction), and a server receiving those inputs over a simulated channel (authority). Every tick the gap between them is the visible effect of network latency.

The channel is everything you set with the top sliders. RTT is the round-trip latency the prediction has to cover; Loss drops a share of packets outright; Jitter scatters delivery times; Reorder and Dup let packets arrive out of order or twice; Bandwidth caps bytes per tick to model a congested uplink; Tick sets the simulation rate. Push any of them and you make the prediction harder — the distance between the two pawns is the result.

The ack model decides how the client books those confirmations, and it's the cycle button on the demo. In FastArray mode each prediction key is tracked independently — a key goes authoritative only when its own ack is received, mirroring Unreal's FReplicatedPredictionKeyMap semantics exactly; correct under loss, but its resident state grows with outstanding keys. In HighWater mode the client keeps only the highest confirmed key and assumes everything below it is safe — cheaper, at a flat 2 bytes, but wrong under packet loss: a dropped ack leaves a gap the high-water mark silently bridges, mis-confirming keys that were never acknowledged (turn Loss up here and the timeline bleeds red). In Range mode the ack carries a cumulative front plus a 32-key selective-ack bitfield — the same idea as TCP SACK and QUIC ack-ranges — so out-of-order confirmations are recorded precisely and a dropped ack self-heals on the next one: correct like FastArray, but at a constant ~7 bytes instead of a per-key list. Out-of-order keys light violet, then collapse into the front when the gap fills. The pawn moves identically in all three — only the bookkeeping, and the Ack state cost, changes.

The net mode swaps the reconciliation policy so you can compare approaches: Real (predict + reconcile), NaiveNoReconcile (predict but never correct — the rubber-band effect), and NaiveNoPredict (wait for the server ack — the laggy effect). All three run the same deterministic fixed-point sim; only the policy changes.

The timeline under the arena is the prediction-key history. Five lanes track every input's fate — issued, acked, replayed, mis-confirmed, out-of-order — stacked over a divergence track that plots the client/server gap tick by tick. Drag across it to scrub back through the buffer and watch a past reconcile frame by frame; drag out a wider selection and every metric below recomputes over just that window, so you can read ack rate and peak divergence on one bad burst instead of the whole run.

Input delay is the last lever. Normally the client applies your input on the tick you press it and the server applies it later — exactly the gap the prediction papers over. Raise the delay and the client holds each input a few ticks before applying it, so both sides act on the same tick and lock together: divergence drops toward zero. The cost is felt lag — a faint zero-delay command ghost marks where an instant-response pawn would be, and the distance back to it is what you traded away. Unreal runs at zero delay and predicts the gap; holding inputs to erase it is the rollback-netcode trick instead.

04 — Notes

Unreal ships client-side prediction and server reconciliation already — in a real game you'd use its CharacterMovementComponent and prediction keys, not hand-roll them. I rebuilt that model from scratch in C++20 to understand it down to the bytes, and to compare ack strategies under packet loss: three acknowledgement models (per-key FastArray, the naive HighWater watermark, a SACK-style Range window), reconciliation by save-and-replay, and a fixed input-delay path that trades a little felt lag for client/server lockstep. The simulation is fixed-point and deterministic, with a CI gate that holds native and WASM bit-identical. WebAssembly, embind, and zero-copy typed-array transfers are just how it runs in your browser, not part of the netcode. If any of this is your kind of problem, the contact link is below.