Under 150 ms latency and 2% loss the local player moves at once, pushes crates without a jump, and sees the other player move smoothly #31

Merged
agra merged 5 commits from task/3993-under-simulated-150-ms-latency-and-2-los into master 2026-10-31 13:45:49 +02:00
Owner

Tracker: #3993

Step 2 of the multiplayer netcode plan (#3571). It adds exactly-once commands, prediction and reconciliation of the player's own body and of the bodies it pushes, interpolation of everything else, and a network conditioner.

  • Conditioner. --lag <ms> (round trip, half each way), --loss <percent> (each way) and --jitter <ms> condition the app's own link from a seeded generator (src/net/conditioner.sx). Each datagram waits half the lag, give or take the jitter, or is lost, so one may overtake another. tests/net_conditioner.sx drives it on a simulated clock.
  • Exactly-once commands. Each client tick sends its command with every one the server has not acknowledged. The server keeps each player's commands waiting, runs one a tick and guesses none: a starved player stands. Each snapshot names the last command run, how many wait, and the receiver's whole Body. The client nudges its tick rate to keep about two waiting.
  • Prediction and reconciliation. The client runs stepBody for each command as it sends it. Each client tick it reconciles from its snapshot buffer at a base that advances one tick a client tick. The base starts six ticks behind the newest snapshot. Each second it steps forward to three behind when the newest stayed more than six ahead, and back only as far as the newest fell behind it, so it bridges jitter and three snapshots lost in a row. From the base the client re-runs every command the server had not taken.
    • Its own copy of the bodies is built as the server builds them, the world file's and the dummies', jointed. It starts each run from the base's state: each body's pose, velocity, spin, how long it has been still, and the island it sleeps in. It is warmed with the contacts and joint impulses an earlier run found at that tick, and stepped after each re-run command with the predicted capsule pushing as the server's does.
    • A body's contacts match the server's from the tick it wakes. So over open ground, and pushing a body no other player touches, the re-run equals the server's run bit for bit.
    • An awake body too far to reach the capsule within the run lies where the base puts it. A fresh map settling then costs a run 0.4 ms instead of 22 ms.
    • The eye is the body's eye between its last two predicted ticks; nothing eases or offsets it.
  • What is drawn when. The bodies the predicted capsule touches, and the bodies they touch or are jointed to, draw where the prediction puts them, between its last two ticks, until the base holds them asleep. Every other body, and every other player, draws at a render time a tick behind the base, from the snapshots either side: about half a round trip and a tenth of a second in the past. Each snapshot also carries the players of the two ticks before it, so a lost snapshot's players arrive in the next one.
  • Wire. Snapshot values are exact f32. An awake body carries its velocity, spin and stillness; a sleeping one carries its island. The protocol version is 3.
  • The dedicated server from master builds and serves on this protocol.

tests/sim_latency.sx plays two clients on the first map at 150 ms round trip, ±10 ms jitter and 2% loss each way, at 60 frames a second. It checks:

  • (a) the first forward command moves the eye on the tick it is sent;
  • (b) on the stairs, sprinting, jumping and crouching, every server body after a command equals the client's prediction bit for bit, and no command is lost;
  • (c) pushing the lone crate across the clearing, the pusher's eye never moves farther in a frame than the server's body moves in as many ticks, plus 1 mm. Every body after a command of the push also equals the prediction bit for bit;
  • (d) each client draws the other's player every frame within 1 cm of the server's path, never standing while that path moves.

tests/sim_pushed.sx covers drawing and correction:

  • a crate the player pushes, and a second crate the first pushes, draw where the prediction puts them, and draw at the render time again once the base holds them asleep;
  • a sliding crate the player never touches draws at the render time;
  • a dummy's parts draw predicted together;
  • a shove the client cannot predict is corrected from the base.
Tracker: [#3993](https://sak.swipelab.com/tasks/3993) Step 2 of the multiplayer netcode plan ([#3571](https://sak.swipelab.com/tasks/3571)). It adds exactly-once commands, prediction and reconciliation of the player's own body and of the bodies it pushes, interpolation of everything else, and a network conditioner. - **Conditioner.** `--lag <ms>` (round trip, half each way), `--loss <percent>` (each way) and `--jitter <ms>` condition the app's own link from a seeded generator (`src/net/conditioner.sx`). Each datagram waits half the lag, give or take the jitter, or is lost, so one may overtake another. `tests/net_conditioner.sx` drives it on a simulated clock. - **Exactly-once commands.** Each client tick sends its command with every one the server has not acknowledged. The server keeps each player's commands waiting, runs one a tick and guesses none: a starved player stands. Each snapshot names the last command run, how many wait, and the receiver's whole `Body`. The client nudges its tick rate to keep about two waiting. - **Prediction and reconciliation.** The client runs `stepBody` for each command as it sends it. Each client tick it reconciles from its snapshot buffer at a base that advances one tick a client tick. The base starts six ticks behind the newest snapshot. Each second it steps forward to three behind when the newest stayed more than six ahead, and back only as far as the newest fell behind it, so it bridges jitter and three snapshots lost in a row. From the base the client re-runs every command the server had not taken. - Its own copy of the bodies is built as the server builds them, the world file's and the dummies', jointed. It starts each run from the base's state: each body's pose, velocity, spin, how long it has been still, and the island it sleeps in. It is warmed with the contacts and joint impulses an earlier run found at that tick, and stepped after each re-run command with the predicted capsule pushing as the server's does. - A body's contacts match the server's from the tick it wakes. So over open ground, and pushing a body no other player touches, the re-run equals the server's run bit for bit. - An awake body too far to reach the capsule within the run lies where the base puts it. A fresh map settling then costs a run 0.4 ms instead of 22 ms. - The eye is the body's eye between its last two predicted ticks; nothing eases or offsets it. - **What is drawn when.** The bodies the predicted capsule touches, and the bodies they touch or are jointed to, draw where the prediction puts them, between its last two ticks, until the base holds them asleep. Every other body, and every other player, draws at a render time a tick behind the base, from the snapshots either side: about half a round trip and a tenth of a second in the past. Each snapshot also carries the players of the two ticks before it, so a lost snapshot's players arrive in the next one. - **Wire.** Snapshot values are exact f32. An awake body carries its velocity, spin and stillness; a sleeping one carries its island. The protocol version is 3. - **The dedicated server** from master builds and serves on this protocol. `tests/sim_latency.sx` plays two clients on the first map at 150 ms round trip, ±10 ms jitter and 2% loss each way, at 60 frames a second. It checks: - (a) the first forward command moves the eye on the tick it is sent; - (b) on the stairs, sprinting, jumping and crouching, every server body after a command equals the client's prediction bit for bit, and no command is lost; - (c) pushing the lone crate across the clearing, the pusher's eye never moves farther in a frame than the server's body moves in as many ticks, plus 1 mm. Every body after a command of the push also equals the prediction bit for bit; - (d) each client draws the other's player every frame within 1 cm of the server's path, never standing while that path moves. `tests/sim_pushed.sx` covers drawing and correction: - a crate the player pushes, and a second crate the first pushes, draw where the prediction puts them, and draw at the render time again once the base holds them asleep; - a sliding crate the player never touches draws at the render time; - a dummy's parts draw predicted together; - a shove the client cannot predict is corrected from the base.
Each client tick sends its command with every one the server has not
acknowledged. The server keeps each player's commands, runs one a tick and
guesses none, and each snapshot names the last it ran, how many wait and the
receiver's whole body. The client nudges its tick rate to keep about two
waiting.

The client runs stepBody for each command as it sends it. Each tick it
re-runs the commands its base had not taken, from the base's body, against
the map and the bodies where the base puts them. The base advances one tick
a client tick behind the newest snapshot, and a lost snapshot is bridged.
Its eye is its body's between its last two predicted ticks.

Others draw a tick behind the base. Each snapshot carries the players of
the two ticks before it, so a lost snapshot's players are in the next.

The conditioner delays and loses each datagram each way from a seeded
generator. The tests drive it on a simulated clock: tests/sim_latency.sx
plays two clients at 150 ms round trip, ±10 ms jitter and 2% loss.

Pushing a crate in that test, the eye still outruns the server's body
(criterion c): the crate turns, and the prediction slides along the
frozen face.
agra changed title from Under 150 ms of lag and 2% loss, a player's own view answers at once and corrects without a snap, and the other player moves smoothly to Under 150 ms latency and 2% loss the local player moves at once, pushes crates without a jump, and sees the other player move smoothly 2026-10-31 13:16:04 +02:00
Two snapshots lost in a row, about one 60-tick window in ten at 2% loss
of each of a snapshot's two datagrams, left the newest level with the
base and stepped the base back to run its middle margin behind that dip.
The clean margin then sat above the ceiling, and the next window stepped
it forward again: two hitches for one rare dip.

The base now steps back only until the window's least margin is 0, which
still has a kept snapshot and keeps the render time between two ticks
seen, and forward to 3 behind the newest once the least passes 6. Three
snapshots lost in a row are bridged.
Each snapshot carries each body's velocity, spin and how long it has been
still while it is awake, or the island it sleeps in. The protocol version
is 3.

The client builds its copy of the bodies as the server's game does,
dummies and joints included. Each run sets the copy to the latest
snapshot at or before the base, warms it with the contacts and joint
impulses an earlier run's first ticks found at that snapshot's tick, and
steps it after each command with the predicted capsule pushing as the
server's does. A body's contacts match the server's from the tick it
wakes, so a push no other player touches is predicted bit for bit. An
awake body too far to reach the capsule within the run lies where the
snapshot puts it, so a fresh map settling costs a run 0.4 ms, not 22 ms.

The bodies the capsule touches in a run, and those they touch or are
jointed to, draw where the prediction puts them, between its last two
ticks, until the base holds them asleep. Every other body still draws at
the render time.

tests/sim_pushed.sx pins what draws predicted and when, and that a shove
the client cannot predict is corrected from the base. tests/sim_latency.sx
now checks the crate push is predicted bit for bit, beside (c).
agra merged commit 245819a28d into master 2026-10-31 13:45:49 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sx/hulken!31
No description provided.