Skip to content

TypeScript: dynamic-state snapshot for rollback netcode (takeSnapshot is ~19x larger than a rollback needs) #991

Description

@jounih

Motivation

For rollback netcode, takeSnapshot() is called every simulated frame — the state has to be captured before anyone knows whether a correction will arrive. That makes its size and cost the dominant fixed per-frame charge, and it is currently serializing far more than a rollback needs.

Measured with @dimforge/rapier2d-deterministic-compat 0.20.0 on a contact-rich scene:

bodies takeSnapshot() bytes/body time
50 50.2 KB ~1.0 KB 0.035 ms
200 219.9 KB ~1.1 KB 0.079 ms
800 988.7 KB ~1.2 KB 0.404 ms

At 800 bodies that is ~1 MB serialized every frame, and it dominates allocation churn (~1.07 MB/tick, ~96% of it snapshot copies — around 64 MB/s of GC traffic at 60 Hz).

The dynamic state a rollback actually needs is small: position, rotation, linear and angular velocity, and sleep state — roughly 64 bytes/body, or ~19× less than the current whole-world encoding. Colliders, shapes, mass properties and the broad-phase structure are unchanged between two frames of the same world and are re-serialized every time.

The obstacle, as I understand it

Restoring only positions and velocities is not sound today, and I want to be explicit that I understand why rather than asking for something unsafe. The TGS solver warm-starts from cached contact impulses; a world restored without them re-simulates along a slightly different trajectory than the original pass, which in a lockstep/rollback setting is a divergence between the peer that rolled back and the peer that did not. That is presumably exactly why the snapshot is whole-world.

So the request is really one of:

  1. A dynamic-state snapshot that includes the warm-start cache — positions, velocities, sleep state, and the contact impulse cache — but omits the static topology (shapes, colliders, mass properties, broad-phase tree) that a caller can guarantee is unchanged. Presumably with a validation handle so a mismatched restore fails loudly rather than corrupting.
  2. Or a documented way to disable warm starting, so callers who accept the solver-quality cost can use a positions-and-velocities snapshot safely. IntegrationParameters currently exposes numSolverIterations, lengthUnit and predictionDistance through the TS bindings, but nothing for warm starting, so this cannot be traded off from JS today.
  3. Or a snapshot API that writes into a caller-provided buffer, which would at least remove the per-frame allocation even if the size stayed the same.

Any of the three would help; (1) is the one that would make rollback cheap.

Prior art

Jolt exposes SaveState/RestoreState with a StateRecorderFilter so applications can omit state they know is unchanged — static or inactive bodies in particular — explicitly for network rollback. Something in that spirit for Rapier would fit rollback netcode very well.

I appreciate that whole-world snapshots are the conservative and obviously-correct design; this is a request for an opt-in fast path where the caller can uphold the extra invariants, not a change to the default. Happy to help test or benchmark.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions