TypeScript: dynamic-state snapshot for rollback netcode (takeSnapshot is ~19x larger than a rollback needs)
- Dominant language
- Rust
- Stars
- 5.7k
- Forks
- 387
- Avg merge
- 4d 23h
- Merged PRs (30d)
- 6
Description
### 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.
Contributor guide
Research direction
Start with the takeSnapshot() entry point and the TypeScript bindings around IntegrationParameters. Trace which dynamic state and warm-start data the current snapshot includes, then compare the three proposed approaches: a validated dynamic snapshot, a warm-starting toggle, or caller-provided storage. Done means an opt-in rollback-safe API or documented tradeoff, with benchmarks showing reduced allocation or snapshot size.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, typescript
- Domain
- game-dev, networking, performance
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 32/100