Skip to content

Commit ed4ac79

Browse files
committed
docs: defer wasm frame-pacing work to later
Captures the speed mismatch between basic-gfx (native, locked 60 Hz via SetTargetFPS) and basic-wasm-raylib (browser, RAF at monitor refresh rate). Lists root causes (RAF binding, Asyncify cost, interpreter overhead) and three concrete fix options — fixed-step sim, per-axis delta scaling, capped main loop. Recommends fixed-step when we revisit. Not blocking: RPG and shooter MVPs aren't timing-sensitive, no real user complaints yet. Pick this up when a tight-reaction game ships or 120 Hz panels report 'everything too fast'.
1 parent 6163d1a commit ed4ac79

1 file changed

Lines changed: 99 additions & 0 deletions

File tree

docs/wasm-frame-pacing-todo.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# WASM frame pacing — deferred
2+
3+
Status: **TODO, not blocking.** Browser builds run at the host monitor's
4+
refresh rate via `requestAnimationFrame`; native locks to 60 Hz via
5+
raylib's `SetTargetFPS(60)`. Game logic ticks once per frame, so a
6+
120 Hz monitor runs RPG / shooter twice as fast. Fine for the MVPs;
7+
revisit when the difference bothers a real user or we ship a game
8+
that's timing-sensitive.
9+
10+
## Symptoms today
11+
12+
- Same `.bas` feels markedly snappier in `basic-gfx` (native, 60 Hz)
13+
than in `basic-wasm-raylib` (browser, RAF-bound).
14+
- Tab in background → RAF throttled to 1 Hz → game crawls.
15+
- 120 Hz / 144 Hz monitors → game runs ~2× / 2.4× speed.
16+
- Heavy frames overshoot one VSYNC tick; light frames undershoot.
17+
Net drift between input and motion.
18+
19+
## Causes
20+
21+
1. **RAF binding.** `emscripten_set_main_loop` (and raylib's emscripten
22+
shim) drives the loop from `requestAnimationFrame`. RAF fires at
23+
the monitor's refresh rate, not 60 Hz.
24+
2. **Asyncify cost.** Every `VSYNC` save/restores the BASIC stack via
25+
Asyncify so JS can yield. Adds a few ms per frame at our program
26+
sizes; not catastrophic but noticeable on slower devices.
27+
3. **Interpreter overhead.** Each BASIC opcode dispatches through the
28+
interpreter loop; native + raylib batches to GPU faster than WASM
29+
per-statement work.
30+
31+
## Fix options (pick when revisiting)
32+
33+
### A. Fixed-step sim, render every RAF (recommended)
34+
35+
Run game logic at 60 sim ticks/sec independent of RAF rate:
36+
37+
```basic
38+
SIM_DT_MS = 1000 \ 60
39+
ACC_MS = 0
40+
LAST_MS = TICKMS
41+
DO
42+
NOW = TICKMS
43+
ACC_MS = ACC_MS + (NOW - LAST_MS)
44+
LAST_MS = NOW
45+
WHILE ACC_MS >= SIM_DT_MS
46+
Tick() ' input + move + collide
47+
ACC_MS = ACC_MS - SIM_DT_MS
48+
WEND
49+
RenderFrame()
50+
VSYNC
51+
LOOP
52+
```
53+
54+
- Collision logic stays integer-step.
55+
- 120 Hz browser still runs 60 sim/sec — game speed identical to
56+
native.
57+
- Tab-unfreeze: clamp `ACC_MS` so we don't run 1000 ticks at once.
58+
59+
### B. Per-axis delta-time scaling
60+
61+
Multiply movement by ms-since-last-frame. Simpler, but variable step
62+
risks AABB tunneling through walls if `dt > tile_size / speed`. Cap
63+
step size to `MAP_TILE_W - 1`. Fractional accumulator needed for
64+
sub-pixel speeds.
65+
66+
### C. Cap RAF to 60 Hz at the interpreter
67+
68+
`emscripten_set_main_loop_timing(EM_TIMING_SETTIMEOUT, 16)` instead
69+
of RAF. Loses VSYNC alignment (visible tearing) and ignores the
70+
host refresh; users on 60 Hz monitors gain nothing.
71+
72+
### D. Compile interpreter hotspot to WAT/intrinsics
73+
74+
Profile-guided. Tilemap composite, sprite cell sort, BASIC main
75+
dispatch. Defer until profiling shows where time actually goes —
76+
WebGL2 raylib's GPU path may already be the bottleneck-free part.
77+
78+
## Pre-work to consider
79+
80+
- Add `TIME$` / `TICKMS` examples to docs so users discover the
81+
primitive without reading basic.c.
82+
- Wire a debug HUD overlay (already have OVERLAY plane) showing
83+
`fps = 1000 / DT_MS` and sim ticks per RAF — tells us where time
84+
goes without external tools.
85+
- Audit Asyncify imports list to confirm only the real awaits are
86+
flagged. Each entry costs Asyncify save/restore.
87+
88+
## Why deferring is fine
89+
90+
MVP-1 (shooter) is auto-scroll, MVP-2 (RPG) is exploration — neither
91+
exercises tight reaction timing. The map editor doesn't care. Real
92+
users haven't complained yet. Pick this up when:
93+
94+
- A timing-sensitive game ships (rhythm, action platformer, parry
95+
combat).
96+
- Telemetry shows users on 120 Hz / 144 Hz panels reporting
97+
"everything moves too fast".
98+
- We add network multiplayer or replays where lockstep becomes
99+
load-bearing.

0 commit comments

Comments
 (0)