metronome/docs/rust-port.md
Me Here c1601d9e46 Rust port Stage 2: scheduler timing + no_std (builds for RP2350)
- schedule.rs: ports the firmware's durs/timeline math (app.py tick/_prepare_next).
  render(track, bars) yields the deterministic click timeline; tests/schedule.rs
  asserts quarter-note spacing, subdivisions, swing 2/3:1/3, polymeter 5:4,
  accents/ghosts, mute, and multi-bar looping. All green on the host.
- The crate is now #![no_std] + alloc and builds for thumbv8m.main-none-eabihf,
  so the codec + scheduler are firmware-ready (verified:
  cargo build --lib --target thumbv8m.main-none-eabihf).

./rust/run.sh -> 9 tests pass (2 conformance + 7 schedule). docs/rust-port.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:34:02 -05:00

81 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Rust port — staged plan
This is the plan for the native-Rust direction first discussed alongside the A/B-bootloader
idea. **What changed since then:** the track format is now formally specced
(`docs/track-format.md`) with a golden-vector conformance suite (`tests/run.mjs`). That suite is
the thing that makes a port *safe* — any Rust engine has a precise, executable definition of
"correct" to validate against, the same one `engine.js` and `app.py` already pass.
## The core idea
Port from the **inside out**, lowest-risk first. The pure logic (track codec, then the
scheduler) is host-testable with zero hardware and is gated by the existing golden vectors. Only
once that's proven do we touch drivers, A/B, and the actual firmware. We do **not** rip out
CircuitPython until the Rust engine passes the vectors *and* the drivers are proven on hardware.
## Stages
### Stage 0 — toolchain in a container
Add a Rust toolchain image (mirroring `hardware/eda/`): a `Containerfile` with `rustup`, the
`thumbv8m.main-none-eabihf` target (RP2350 is Cortex-M33), `flip-link`, `probe-rs`, `elf2uf2`.
Driven by a `run.sh` like the EDA one. **Never on the host.**
### Stage 1 — `track-format` crate ✅ DONE (`rust/track-format/`)
Implemented and **passing**: `./rust/run.sh` builds the container and runs `cargo test`, which
validates the crate against `tests/fixtures/track-format.json` (conformance + idempotency). The
Rust codec agrees with `engine.js` and `app.py` on every vector — and carries `vol`/`cd`, so it's
the most spec-complete of the three. Original scope below.
#### (original) Stage 1 — `track-format` crate ← the concrete first PR
A pure, `no_std`-compatible crate: `parse(&str) -> Track` and `serialize(&Track) -> String`,
plus a `normalize()` that emits the neutral structure from `docs/track-format.md` §5. Then a
`cargo test` that reads `tests/fixtures/track-format.json` and asserts each case's `norm` and
round-trip — i.e. a **third adapter alongside `js_adapter.mjs` / `py_adapter.py`**. When this is
green, the Rust engine provably agrees with web + device on every groove, euclid, swing, ghost,
polymeter, and the playback-flow tokens. No hardware, fully testable in the container.
This is the highest-value slice: small, gated by work already done, and it proves the toolchain.
### Stage 2 — scheduler/engine ✅ DONE (`rust/track-format/src/schedule.rs`)
Ported the look-ahead step scheduler (the `durs` math from `app.py` `tick`/`_prepare_next`).
`render(track, bars)` produces the deterministic click timeline; `tests/schedule.rs` asserts the
timings — quarter-note spacing, subdivisions, swing 2/3:1/3, polymeter 5:4, accents/ghosts, mute,
multi-bar looping. All green on the host, no hardware. The real-time firmware loop will just play
this timeline against the wall clock.
**Also done:** the crate is now `#![no_std]` + `alloc` and **builds for the RP2350 target**
(`cargo build --lib --target thumbv8m.main-none-eabihf`) — the codec + scheduler are firmware-ready.
### Stage 3 — drivers (hardware)
On `embassy` / `rp-hal`:
- ST7789 240×320 display → `mipidsi` + `embedded-graphics` (mature; the parts are well-supported).
- I²S to the PCM5102A → RP2350 PIO.
- WS2812 → `ws2812-pio`. USB-MIDI → `usbd-midi` / `embassy-usb`.
- GT911 touch (Kit) over I²C.
### Stage 4 — native A/B + secure boot
Replace the `.mpy`-level A/B hack (`code.py` loads `app.mpy`, rolls back to `app.bak`) with the
**RP2350 bootrom's native** partition-table A/B + signed boot, configured via `picotool` (the
chip already provides this — see the earlier hardware discussion). The Rust app is the image in
the slot; rollback and version selection move into silicon.
## What you keep / lose
- **Gain:** memory safety, native A/B + secure boot, performance headroom, one typed model instead
of three hand-written parsers.
- **Lose:** the live one-click `.mpy` push (Rust is compile→flash→reboot). The editor's *data*
live-sync (tempo/pattern/setlist mirroring) still works — it's a data protocol. Only live
*logic* edits go away, and an embedded `wasm3`/script module could buy those back if wanted.
## Acceptance gate
Every codec/engine change must pass `tests/fixtures/track-format.json`. The Rust crate joins
`js`/`py` as a runner adapter, so "same groove on web, device, and the Rust build" is enforced,
not hoped for.
## Recommendation
Do **Stage 1 in a container next** — it's small, testable today (given a toolchain), reuses the
suite, and produces a real artifact to judge the Rust path on before committing to drivers or a
firmware rewrite. Defer Stages 34 until Stage 12 are green and you've decided the live-push
tradeoff is acceptable.