metronome/docs/rust-port.md
Me Here f201892c9c docs: staged Rust-port plan (codec crate first, gated by the golden vectors)
Grounds the native-Rust direction in what now exists: port inside-out, lowest risk
first, with tests/fixtures/track-format.json as the acceptance gate. Stage 1 (the
track-format crate as a third conformance adapter) is the concrete next PR -
host-testable in a container, no hardware. Toolchain goes in a container per the
develop-in-container rule, not the host.

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

3.8 KiB
Raw Blame History

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 34 until Stage 12 are green and you've decided the live-push tradeoff is acceptable.