|
| 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