Skip to content

deliver requested replay speeds when frames are slower than ticks - #105

Open
JBoggsy wants to merge 1 commit into
replay-controlfrom
replay-control-speed
Open

JBoggsy wants to merge 1 commit into
replay-controlfrom
replay-control-speed

Conversation

@JBoggsy

@JBoggsy JBoggsy commented Oct 11, 2026

Copy link
Copy Markdown

Deliver requested replay speeds when frames are slower than ticks

Depends on #102 (stacked on replay-control).

What and why

Player.startFrame capped the realtime backlog at 0.25 s of game time, so one frame could advance at most a quarter second of match. At 16× that needs 64 frames per second; real viewers draw 40–60, so 16× actually played at about 10× in headless Chromium and about 4× inside a heavier host page (PlayerADE). Speeds 1–4× were unaffected.

  • The cap is now PlaybackLagSeconds (0.25 s) of wall time, scaled by speed: 1× is unchanged (0.25 s), 16× may carry 4 s of match time.
  • Realtime ticks now also yield after catchUpSeconds, the same per-frame budget seeks use, so a machine that can't keep up keeps drawing frames and drops time at the cap instead of stalling.

Measurements

Embedded GotA viewer, 23,642-tick replay, headless Chromium with Metal on an M-series Mac, 5 s of play from tick 6000 and from tick 18000:

Requested Before (6000 / 18000) After (6000 / 18000)
2× 2.00× / 2.00× 2.00× / 2.00×
4× 3.99× / 4.00× 4.00× / 4.00×
16× 9.83× / 10.41× 16.00× / 15.99×

Frame rate at 16× went from 47–50 to 38–45 fps (more ticks per frame). In PlayerADE's headed workbench 16× measured 15.95–16.05× (was about 4×).

What to look at

  • src/polyworld/player.nim: startFrame cap and the budget check in the realtime branch of shouldTick. This is the shared transport, so LvD/CTA/Crewrift get the same behaviour; at 1–4× nothing changes unless a frame's simulation exceeds catchUpSeconds.
  • tests/test_player.nim: the existing realtime case now passes a real frame start (epochTime()) instead of 0, like Add a replay control API for embedded browser viewers #102 did for seeks; with 0 the new budget is always exhausted.

Tests

  • nim r tests/test_player.nim: pass, with new cases for the scaled backlog (38 ticks from one 0.1 s frame at 16×; cap 4 s at 16×, 0.25 s at 1×) and the frame budget.
  • nim r tests/tests.nim: all tests passed (with the sibling PRs merged in).
  • test_browser_with_playwright.py --control-only --gpu: pass; the speed check now plays 4× and 16× for 3 s each and with --gpu asserts at least 85% of the requested speed (measured 3.99–4.00× and 15.97–16.05×).

Docs: docs/replay-control.md (setSpeed, verification).

🤖 Generated with Claude Code

startFrame capped the realtime backlog at 0.25 s of simulated time, so a
frame could advance at most a quarter second of game time. At 16x that
needs 64 frames a second; real viewers drew 40-60 and the embedded GotA
viewer played at about 10x (4x in a heavier host page).

- player.nim: cap the backlog at PlaybackLagSeconds (0.25 s) of wall time,
  scaled by speed, so 1x is unchanged and 16x may carry 4 s. Realtime ticks
  now also yield after catchUpSeconds, so a slow machine keeps drawing
  frames and drops time at the cap instead of stalling.
- Tests: test_player cases for the scaled backlog and the frame budget
  (realtime shouldTick now takes a real frame start); browser check that 4x
  and 16x reach 85% of the requested speed with --gpu.
- Docs: docs/replay-control.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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