diff --git a/docs/rust-port.md b/docs/rust-port.md new file mode 100644 index 0000000..c2784f5 --- /dev/null +++ b/docs/rust-port.md @@ -0,0 +1,70 @@ +# 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 ← 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 +Port the look-ahead step scheduler. `engine.js` already marks these primitives +`PORTS TO FIRMWARE`, and `app.py`'s `tick()` is the same model. Host-test it by asserting click +*timings* for known patches (e.g. swing ratios, polymeter bar lengths) — still no hardware. + +### 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 3–4 until Stage 1–2 are green and you've decided the live-push +tradeoff is acceptable.