Skip to content

feat(wasm): ui_set_tick_rate export — declared-rate realms reach the sim - #276

Draft
NathanWalker wants to merge 1 commit into
pocket-stack:mainfrom
NathanWalker:feat/wasm-tick-rate
Draft

feat(wasm): ui_set_tick_rate export — declared-rate realms reach the sim#276
NathanWalker wants to merge 1 commit into
pocket-stack:mainfrom
NathanWalker:feat/wasm-tick-rate

Conversation

@NathanWalker

Copy link
Copy Markdown
Contributor

The coverage deferral from #257: engine/wasm exported no ui_set_tick_rate, so sim/goldens/tapes could not reach a non-60 realm at all. This closes it end to end.

What changed

  • engine/wasm/src/lib.rs: ui_set_tick_rate(hz) -> i32, delegating to Ui::set_tick_rate — the same declare-before-first-tick lifecycle and MAX_TICK_HZ gate the native surfaces settled on in feat(core): per-realm tick rate — fixed-step at a declared hz #257 (returns 0 once ui_tick has run).
  • hosts/web/wasm-ops.js: feature-detected ops.setTickRate (stale binaries keep booting at the spec 60, the ui_set_viewport convention). An applied rate is published as ops.__tickHz — the same mount fact the native surfaces publish — and init()'s core reset retracts it, so a runner that resets must redeclare. The init(initialDensity) boot call moved below the ops literal to make that retraction possible; nothing in between calls into the wasm.
  • hosts/sim/sim.ts: Scenario.tickHz / SimBootOptions.tickHz (default 60). bootWorld declares the rate before eval — the order the native surfaces enforce, since mount-time animate()/spring() convert ms at the rate — refuses an hz that does not divide tickHz, and drives tickHz/hz core ticks per virtual frame. SimWorld.tickHz is exposed so rate-aware companions pair with it (createSimAudioSink(world.tickHz) once fix(audio): audioFramesForTick counts frames at the realm's declared rate #275 lands). touchGlide takes the finest grid as a parameter instead of a hardcoded 60 (the sim-side hardcode called out in review).
  • tests/tick-rate-sim.test.ts + a tick-rate sim gate stage (prep rebuilds the wasm and bakes a --hz=120 café into dist/tick-rate-120/).

Coverage

  • ABI lifecycle: declare-then-__tickHz; 0 / 241 / post-tick rejected with nothing published; init() resets to 60, retracts __tickHz, and a fresh declaration applies again.
  • A 120-baked café runs the full #sim.test.ts journey at tickHz: 120: deterministic across runs, "ORDERS PLACED 1" in the tree, and the subsampling theorem holds at the declared rate — 60 Hz and 30 Hz presentations strictly subsample the 120 Hz trajectory (sub[m] == full[k(m+1)-1] for every m), byte-equal final frames, effects at identical virtual seconds.
  • hz not dividing tickHz refuses loudly.

One finding worth recording: the first draft of the subsample test used the 120-baked hero, and it diverges at frame 1 — hero's mount spring is guest-side per-frame state, which DETERMINISM.md's theorem statement explicitly scopes out ("JS state changes only on events and virtual time"). The café is the theorem's reference app and subsamples exactly. The theorem's scope condition is doing real work at non-60 rates, not just at 60.

Out of scope

hosts/web/engine.js's browser pacing (let simHz = 60, 60 / simHz) — driving a non-60 realm in the browser host needs a rAF pacing decision, not just plumbing; flagged for a follow-up.

Verification

bun run test 12/12 stages green (47.4 s) including the new stage; bunx tsc --noEmit clean.

🤖 Generated with Claude Code

The wasm core takes the same declare-before-first-tick lifecycle as the
native surfaces; wasm-ops publishes an applied rate as ops.__tickHz and
retracts it on init's core reset. The sim declares Scenario.tickHz before
eval (mount-time conversions run at the rate), drives tickHz/hz core ticks
per virtual frame, and touchGlide's finest-grid emission takes the rate
instead of a literal 60.

Co-Authored-By: Claude Fable 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