Skip to content

Restore lockstep frame semantics#118

Merged
gschup merged 1 commit into
mainfrom
lockstep-helper
Jun 25, 2026
Merged

Restore lockstep frame semantics#118
gschup merged 1 commit into
mainfrom
lockstep-helper

Conversation

@gschup

@gschup gschup commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

This restores conservative lockstep semantics for max_prediction = 0.

In lockstep mode, GGRS now keeps current_frame() as the public game frame, does not emit rollback save/load requests, and only advances when the current frame has confirmed inputs from all players. Stalled lockstep frames return Ok(vec![]) without consuming a game frame.

Using this with the recommended game loops for rollback will result in phaselocked clients that require at least RTT input delay to operate smoothly. By changing the gameloop, users should be able to only require single-way delay for smooth operation.

let frame_duration = Duration::from_secs_f64(1.0 / fps as f64);
accumulator += delta;

session.poll_remote_clients();

while accumulator >= frame_duration {
    session.add_local_input(local_handle, input)?;

    let deadline = Instant::now() + frame_duration;
    let mut requests = session.advance_frame()?;

    while requests.is_empty()
        && session.in_lockstep_mode()
        && Instant::now() < deadline
    {
        session.poll_remote_clients();
        requests = session.advance_frame()?;
    }

    if requests.is_empty() && session.in_lockstep_mode() {
        // Still no game frame emitted. Keep accumulated time and retry later.
        break;
    }

    accumulator -= frame_duration;

    for request in requests {
        handle_ggrs_request(request);
    }
}

This PR also adds optional bounded wait helpers:

  • advance_frame_with_wait(): waits up to roughly one configured frame duration.
  • advance_frame_with_wait_timeout(timeout): same behavior with an explicit timeout.

These helpers reduce poll-phase stalls in lockstep mode without requiring users to hand-roll a busy-wait loop. In rollback mode, they behave like advance_frame().

let frame_duration = Duration::from_secs_f64(1.0 / fps as f64);
accumulator += delta;

session.poll_remote_clients();

while accumulator >= frame_duration {
    session.add_local_input(local_handle, input)?;

    let requests = session.advance_frame_with_wait()?;
    // Or, with an explicit bound:
    // let requests = session.advance_frame_with_wait_timeout(Duration::from_millis(2))?;

    if requests.is_empty() && session.in_lockstep_mode() {
        // No game frame was emitted. Keep the accumulated time and retry
        // after more network polling instead of dropping a simulation tick.
        break;
    }

    accumulator -= frame_duration;

    for request in requests {
        handle_ggrs_request(request);
    }
}

@gschup gschup merged commit aa61f9a into main Jun 25, 2026
2 checks passed
@gschup gschup deleted the lockstep-helper branch June 25, 2026 06:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant