Skip to content
12 changes: 5 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,15 @@ In this document, all notable changes are listed, including bug fixes, breaking
- breaking: `NetworkStats::kbps_sent` has been removed; `network_stats()` now reports queue length, RTT, and frame-advantage data only

### Bug fixes
- fix: lockstep mode (`max_prediction = 0`) now requires only one-way latency worth of input delay instead of round-trip; `advance_frame` now unconditionally advances the input queue (keeping packets flowing) while the game frame only steps forward when all players have confirmed inputs, so a single network one-way trip is sufficient to confirm and advance each game frame (fixes [#116](https://github.com/gschup/ggrs/issues/116))
- fix: desync detection no longer panics with sparse saving when the configured checksum interval frame was not saved exactly (fixes [#107](https://github.com/gschup/ggrs/issues/107))
- fix: malformed input packets are now discarded with warnings instead of panicking when connection status lengths, start frames, compressed payloads, or decoded player input shapes are invalid
- fix: input packets with a start frame ahead of the last received frame are no longer rejected; the gap check was incorrectly blocking the first packet from a peer using input delay, freezing the session
- fix: increasing input delay mid-session now sends generated gap-fill inputs to remote peers, preventing sessions from freezing after the delay change
- fix: input delay changes now work when multiple local players share one endpoint; outgoing local inputs are queued by effective frame until every local player has input for that frame
- fix: increasing input delay mid-session now sends generated gap-fill inputs to remote peers, and the first outgoing packet from a peer using input delay is no longer incorrectly rejected; both issues would freeze the session
- fix: input delay changes now work when multiple local players share one endpoint; outgoing local inputs are queued by effective frame until every local player has input for that frame, and the queue correctly finds the first complete frame rather than assuming the earliest buffered frame is complete
- fix: `SessionBuilder::with_num_players()` now revalidates already-registered handles, so changing the player count after adding players cannot leave invalid local, remote, or spectator handles in the builder
- fix: `SessionBuilder::start_p2p_session()` now rejects `DesyncDetection::On { interval: 0 }`, which cannot produce a valid checksum reporting cadence
- fix: spectator catch-up no longer attempts to read past `SPECTATOR_BUFFER_SIZE` frames when the spectator is very far behind; `frames_to_advance` is now also bounded by the buffer capacity
- fix: spectator catch-up now caps the number of frames advanced to the number of confirmed frames available from the host, so `catchup_speed` can safely exceed `max_frames_behind`
- fix: spectator catch-up no longer attempts to read past `SPECTATOR_BUFFER_SIZE` frames when the spectator is very far behind, and now caps the number of frames advanced to the number of confirmed frames available from the host so `catchup_speed` can safely exceed `max_frames_behind`
- fix: `P2PSession` no longer leaks memory in all-local-player sessions; outgoing inputs were queued into an unbounded buffer that was never drained when there were no remote peers
- fix: `P2PSession` with multiple local players at different input delays no longer stalls on the first frame; the outgoing queue now scans for the first complete frame rather than assuming the earliest buffered frame has inputs from all players
- fix: desync detection no longer panics with sparse saving when the configured checksum interval frame was not saved exactly (fixes [#107](https://github.com/gschup/ggrs/issues/107))

### Improvements
- feat: `P2PSession::set_input_delay()` allows changing the input delay for a local player mid-session (closes [#106](https://github.com/gschup/ggrs/issues/106))
Expand Down
5 changes: 4 additions & 1 deletion docs/main-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,12 @@ while time_since_last_frame >= fps_delta {
for request in requests {
handle_ggrs_request(request);
}
// In lockstep mode, requests may be empty — remote inputs have not
// arrived yet. This is not an error; the session will advance on the
// next tick once poll_remote_clients delivers the missing inputs.
}
Err(GgrsError::PredictionThreshold) => {
// remote peer is too far behind; skip this frame
// remote peer is too far behind; skip this frame (rollback mode only)
}
Err(e) => return Err(e),
}
Expand Down
4 changes: 2 additions & 2 deletions docs/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ let mut session = SessionBuilder::<GgrsConfig>::new()
|---|---|---|
| `with_num_players(n)` | 2 | Total number of players (not counting spectators). Must be at least 1. Revalidates already-added handles. |
| `with_fps(fps)` | 60 | Expected update frequency. Used for frame synchronization heuristics. |
| `with_input_delay(n)` | 0 | Frames of artificial delay applied to local input. Reduces rollbacks at the cost of added latency. |
| `with_max_prediction_window(n)` | 8 | Maximum frames GGRS will predict ahead. Set to `0` for lockstep mode (no rollbacks, no prediction). |
| `with_input_delay(n)` | 0 | Frames of artificial delay applied to local input. Reduces rollbacks in rollback mode. In lockstep mode, set this to at least `ceil(one_way_latency_in_frames)` so remote inputs arrive before they are needed. |
| `with_max_prediction_window(n)` | 8 | Maximum frames GGRS will predict ahead. Set to `0` for lockstep mode: no prediction, no rollbacks, game stalls until all remote inputs are confirmed. |
| `with_sparse_saving_mode(bool)` | false | Only save state at the last confirmed frame. See [Sparse Saving](sparse-saving.md). |
| `with_desync_detection_mode(mode)` | Off | Enable checksum-based desync detection. `DesyncDetection::On` requires an interval higher than 0. See [`DesyncDetection`](https://docs.rs/ggrs/latest/ggrs/enum.DesyncDetection.html). |
| `with_disconnect_timeout(duration)` | 2s | How long without packets before a remote peer is disconnected. |
Expand Down
4 changes: 4 additions & 0 deletions src/input_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ impl<T: Config> InputQueue<T> {
self.first_incorrect_frame
}

pub(crate) fn frame_delay(&self) -> usize {
self.frame_delay
}

/// Changes the frame delay and returns any fill inputs that were implicitly added to bridge the
/// gap. The caller is responsible for sending these to remote peers so they see consecutive
/// frame numbers.
Expand Down
27 changes: 14 additions & 13 deletions src/sessions/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,27 +144,28 @@ impl<T: Config> SessionBuilder<T> {
///
/// ## Lockstep mode
///
/// As a special case, if you set this to 0, GGRS will run in lockstep mode:
/// * ggrs will only request that you advance the gamestate if the current frame has inputs
/// confirmed from all other clients.
/// * ggrs will never request you to save or roll back the gamestate.
/// Setting this to 0 enables lockstep mode:
/// - `AdvanceFrame` is only emitted once all remote inputs for the current frame are confirmed.
/// - `SaveGameState` and `LoadGameState` are never emitted — no rollback occurs.
///
/// Lockstep mode can significantly reduce the (GGRS) framerate of your game, but may be
/// appropriate for games where a GGRS frame does not correspond to a rendered frame, such as a
/// game where GGRS frames are only advanced once a second; with input delay set to zero, the
/// framerate impact is approximately equivalent to taking the highest latency client and adding
/// its latency to the current time to tick a frame.
/// In lockstep mode the game stalls until remote inputs arrive, so smooth playback requires
/// enough input delay to cover the one-way network trip. Set
/// `with_input_delay(ceil(one_way_latency_in_frames))` — roughly half the round-trip time.
/// The semantics match rollback mode: input pressed on frame `F` takes effect on frame `F + D`.
pub fn with_max_prediction_window(mut self, window: usize) -> Self {
self.max_prediction = window;
self
}

/// Change the amount of frames GGRS will delay the inputs for local players. Default is 0.
///
/// Adding a small amount of input delay (typically 2–4 frames) reduces the number of rollbacks
/// by giving remote inputs time to arrive before they are needed. The trade-off is a small
/// but constant increase in perceived input latency. This is usually preferable to frequent
/// rollbacks at higher network latencies.
/// In rollback mode, input delay (typically 2–4 frames) reduces rollbacks by letting remote
/// inputs arrive before they are needed, at the cost of constant perceived latency.
///
/// In lockstep mode, input delay determines how far ahead the input queue runs relative to
/// the confirmed game frame. Set this to at least `ceil(one_way_latency_in_frames)` —
/// roughly half the round-trip time — so remote inputs arrive before the game needs them
/// and the session can advance without stalling.
///
/// There is no enforced upper bound, but values above ~8 frames will produce noticeable
/// input lag. Setting this higher than `max_prediction_window` is not recommended —
Expand Down
Loading
Loading