Combines 8a (self-test + doc reconciliation) and 8b (ProDOS boot + parity checklist) into one PR. Self-test gate (ewm/tests/two_e_selftest.rs): holding Solid-Apple (button 1) across cpu.reset() runs the ROM diagnostic, which tests all 128K + ROM checksums + the MMU/aux routing and prints "System OK" (asserted via text_screen, ~100M cycles) -- the strongest end-to-end burn-in for the aux plumbing. ProDOS gate (ewm/tests/two_e_prodos.rs): boots ProDOS_2_4_3.po and asserts the Bitsy Bye launcher lists PRODOS.2.4.3 / BITSY.BOOT. ProDOS keeps its global page, buffers and /RAM disk in aux, so this is a real-OS aux integration burn-in. Bitsy Bye comes up in 40 columns here (its 80-col path wants a machine-ID/firmware signature we do not fully present); the 80-column render path is already gated by two_e_80col (5b) and the PR#3 test. Doc reconciliation: - REWRITE.md quirk #4: only plain `apple2` errors now; the Enhanced //e is supported (two --model 2e). - TOTAL_RECALL_WRITE_WARNINGS.md: the $C00x/$C05x addresses are real, implemented switches on the //e path (IouE) -- the warnings are ][+-only; the --debug gating it wished for already landed (#215). - APPLE_IIE_ENHANCED.md: added the switch/mode parity checklist (every capability -> its gate) and marked 8a/8b done. Outstanding before the master promotion (deferred polish): the AN3 falling-edge double-res latch and the DHGR colour revisit. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
29 KiB
EWM C-to-Rust Rewrite Plan
This is a working document for an iterative, agent-driven rewrite of EWM from C to Rust. Re-read this file at the start of every session. Update the phase checklist as work completes. The tree must build and pass all verification gates after every phase.
Status
| Phase | Description | Size | Status |
|---|---|---|---|
| 0 | Workspace scaffolding + CI | S | Done |
| 1 | 6502 core (Dormann functional test) | L | Done |
| 2 | 65C02 + disassembler + golden traces | M | Done |
| 3 | Apple 1 / Replica 1, headless | M | Done |
| 4 | Apple 1 SDL frontend | M | Done (manual checklist below) |
| 5 | Apple ][+ machine, headless, no disk | L | Done |
| 6 | Disk II | L | Done |
| 7 | Apple ][+ SDL frontend + boo menu | L | Done (manual checklist below) |
| 8 | Parity sweep, benches, docs | M | Done |
| 9 | Remove C, promote Rust to root | M | Done |
Ground rules (apply to every phase)
- Parallel rewrite. All Rust code lives under
rust/. C code insrc/is never modified until Phase 9.cd src && makemust keep working through Phase 8. - Each phase is one PR-sized unit: independently completable, verifiable with the commands listed in its gate, and leaves both C and Rust builds green.
- Port behavior, not just structure — including documented quirks (see "Quirks to preserve" below). When the C code and the 6502 spec disagree, match the C.
- No new emulation features during the port. Feature ideas go in "Future work" at the bottom of this file.
- If a phase gate fails at session end, revert to the last green commit rather than leaving a broken tree.
Top-level decisions (decided; do not relitigate per phase)
| Decision | Choice | Rationale |
|---|---|---|
| Workspace location | Self-contained Cargo workspace in rust/ (rust/Cargo.toml with members ewm-core, ewm) |
Zero interference with the C build during the parallel period; Phase 9 is a mechanical git mv to the root. CI uses --manifest-path rust/Cargo.toml. |
| Crate structure | Two crates: ewm-core (no SDL dependency: CPU, bus, machines, devices, headless logic) and ewm (binary: SDL2 windowing, renderers, audio, input, menu, CLI) |
The Dormann tests and machine integration tests run headless in CI; the crate boundary enforces the core/frontend seam instead of relying on discipline. |
Memory system (C: mem_t linked list + fn pointers + void *obj) |
trait Bus { fn read(&mut self, addr: u16) -> u8; fn write(&mut self, addr: u16, b: u8); }. Cpu holds no memory — step, reset, irq, nmi take &mut dyn Bus. Each machine struct (One, Two, TestBus) implements Bus, owns its RAM/ROM/devices as plain fields, and dispatches with a match on address ranges. Language-card switching becomes state fields consulted in Two::read/write instead of toggled region flags. |
The void *obj seam becomes the trait object; the linked-list walk becomes a match (faster and clearer); and this dissolves the RAM-aliasing problem (next row). |
RAM aliasing (C: renderers scan cpu->ram directly) |
Machine owns ram: Vec<u8>; the CPU never holds a RAM pointer. Renderers take &Two (immutable) and are called between step batches in the frame loop — exactly the C sequencing, now borrow-checker-legal by construction. No Rc<RefCell>, no unsafe. |
|
| Instruction dispatch | Port the C design 1:1: a 256-entry [Instruction; 256] table per model. Instruction { name, bytes, cycles, handler: Handler } where Handler is a 3-variant enum — Implied(fn(&mut Cpu, &mut dyn Bus)), Byte(fn(.., u8)), Word(fn(.., u16)) — the type-safe version of C's arity-cast void *. 65C02 table = copy of the 6502 table with overrides overlaid at construction (mirrors the C back-fill), built once. |
A mechanical 1:1 port of ~200 handlers is the lowest-risk path; a giant match can be a later refactor. dyn Bus dispatch is plenty fast for a 1 MHz target (Phase 8 benches confirm). |
| CPU flags | Keep the C layout: separate fields n v b d i z c, packed/unpacked only in status() / set_status() |
Exact behavioral match (B-flag handling) and 1:1 handler porting. |
| ROM paths | include_bytes! for all machine/character ROMs (they are checked into the repo; the Disk II boot ROM is already a C array). During Phases 0–8, core references ../../src/rom/... relative to the crate; Phase 9 moves rom/ to the repo root and fixes the paths in one edit. Only user-supplied files (disk images, --memory files, trace paths) are runtime paths. Dormann test bins load at runtime via env!("CARGO_MANIFEST_DIR")-relative paths (test inputs, not assets). |
Kills the "must run from src/" quirk; the Rust binary works from any cwd with no data files to install. |
| Trace comparison vs C | Golden-file based, not live lockstep. One-time procedure (documented script, run manually, not in CI): build C with tracing, capture the first ~100k instructions of the 6502 functional test as a normalized text trace (PC A X Y SP P per step), check in gzipped under rust/ewm-core/tests/golden/. A Rust test replays and diffs, reporting the first divergent instruction. |
Golden files keep CI free of the C toolchain and survive C removal in Phase 9. The Dormann tests are the pass gate; the trace diff exists to localize failures (a Dormann failure alone gives a deadlock PC, not the culprit instruction). |
| CLI | Hand-rolled arg parsing in main.rs mirroring the existing getopt_long flags (ewm one --model apple1|replica1 --memory ... --trace ..., ewm two --color --fps N --drive1 ..., ewm boo) — no clap. |
Flag-for-flag parity is the parity contract; avoids a dependency and behavior drift. |
| Lua | Dropped. lua.c, scripts/*.lua, and all EWM_LUA paths are not ported. Listed under Future work (mlua). |
Optional feature, threaded through cpu/two/dsk behind #ifdef; dropping it greatly simplifies the CPU dispatch port. |
Module naming map
| C | Rust | Crate |
|---|---|---|
cpu.c/h |
src/cpu.rs (registers, step, stack, vectors, reset/irq/nmi) |
ewm-core |
mem.c/h |
src/bus.rs (Bus trait, TestBus flat 64K); addressing-mode/RMW helpers move into src/ins.rs alongside their only callers |
ewm-core |
ins.c/h |
src/ins.rs (tables + handlers) |
ewm-core |
fmt.c/h |
src/fmt.rs (disassembler, trace-line formatter) |
ewm-core |
pia.c/h |
src/pia.rs |
ewm-core |
one.c (machine half) |
src/one.rs (One struct + Bus impl) |
ewm-core |
two.c (machine half) |
src/two.rs (Two struct, soft switches, key latch, speaker/paddle state) |
ewm-core |
alc.c/h |
src/alc.rs (language-card state, driven from Two's bus) |
ewm-core |
dsk.c/h |
src/dsk.rs (nibblization, boot ROM, stepper, IOM) |
ewm-core |
chr.c (bitmap half) |
src/chr.rs (char ROM → glyph bitmaps) |
ewm-core |
utl.c/h |
src/util.rs (file loading, misc) |
ewm-core |
one.c (SDL loop) |
src/one.rs |
ewm (bin) |
two.c (SDL loop) |
src/two.rs |
ewm (bin) |
tty.c/h |
src/tty.rs |
ewm |
scr.c/h |
src/scr.rs |
ewm |
snd.c/h |
src/snd.rs (toggle events → samples → queue_audio) |
ewm |
chr.c (texture half) |
src/chr.rs (glyph bitmaps → SDL textures) |
ewm |
sdl.c/h |
src/sdl.rs (init/joystick helpers) |
ewm |
boo.c/h |
src/boo.rs |
ewm |
ewm.c |
src/main.rs (subcommand dispatch) |
ewm |
cpu_test.c |
ewm-core/tests/dormann.rs |
ewm-core |
cpu_bench.c / mem_bench.c |
ewm-core/benches/ |
ewm-core |
scr_test.c / tty_test.c |
manual SDL viewers — dropped; equivalent coverage via headless gates + Phase 7 screenshots | — |
lua.c/h |
not ported | — |
Quirks to preserve (verify per phase, do not "fix")
- IRQ/NMI push
pc + 1, not the spec'spc + 2(cpu.ccomments this; the Dormann tests depend on it). - Disk II writes are no-ops (write support stubbed in
dsk.c). - The MHz display is fake (always ≈1.023 MHz, not measured).
apple2(non-plus) returns an error, as in C. (No longer applies toapple2e: the Enhanced //e is now a supported machine —two --model 2e, seenotes/APPLE_IIE_ENHANCED.md. Only the plain NMOSapple2still errors.)- Cycle budget per frame =
1023000 / fps, default 40 fps fixed step (EWM_TWO_SPEED,EWM_TWO_FPS_DEFAULTintwo.h). - CPU test success detection = branch-to-self deadlock check; start
$0400, success PC$3399(6502) and$24a8(65C02), percpu_test.c. - Strict mode is accepted but inert at runtime: the stack checks live only in irq/nmi, whose sole runtime caller (BRK) discards the error — in C and in Rust.
--fps 0divides by zero (C: SIGFPE, Rust: panic) — crash parity, not fixed.
Documented divergences (Rust vs C, deliberate)
- Flag parsing requires exact
--flag; C'sgetopt_long_onlyalso accepted single-dash (-model) and unambiguous abbreviations. - Unexpected-I/O messages omit the PC (the Bus trait has no CPU access).
- Joystick axes are sampled once per frame, not live at the
$C070trigger (≤ 1 frame of input latency). --traceactually writes traces in Rust (PC: disassembly stateper step, via the fmt.rs formatters); in C the write path was dead code.--screenshot=<path>onewm twois a Rust-only hidden debug flag (dumps a BMP after 120 frames, then exits) — it powers the Phase 7 graphics gate.- The
dsk.cnibblizer's 2-byte out-of-bounds buffer write is not reproduced; the store is skipped (the effective C/JS behavior, see dsk.rs). - Lua (
--script) is not ported (top-level decision; see Future work).
Phase 0 — Workspace scaffolding + CI (S)
Goal: An empty but building Rust workspace beside the untouched C tree, with CI guarding both.
Scope:
rust/Cargo.toml(workspace),rust/ewm-core(lib with empty modules),rust/ewm(bin that prints usage),rust/rust-toolchain.toml,rust/rustfmt.toml..github/workflows/ci.yml(new — the repo has no CI;.travis.ymlis defunct):- Job 1: install
libsdl2-dev, build the C tree (cd src && make). - Job 2:
cargo fmt --check,cargo clippy -- -D warnings,cargo test, all with--manifest-path rust/Cargo.toml.
- Job 1: install
- Short "Rust rewrite in progress, see REWRITE.md" note in README.
Key decisions: ewm-core has zero non-dev dependencies; ewm declares the sdl2
crate now so CI installs SDL2 headers from day one.
Gate: CI green on both jobs; cargo run --manifest-path rust/Cargo.toml -p ewm
prints usage; cd src && make still builds.
Phase 1 — 6502 core, gated on the Dormann functional test (L)
Goal: A complete NMOS 6502 with the Bus trait, passing the Klaus Dormann functional test.
Scope:
- Port
cpu.c→cpu.rs,mem.c→bus.rs(+ helpers intoins.rs), and the 6502 half ofins.c(table + all handlers) →ins.rs. ewm-core/tests/dormann.rsports thecpu_test.charness: loadsrc/rom/6502_functional_test.bininto a flat-64KTestBusat$0000, set PC=$0400, step until PC==$3399(pass) or PC repeats (fail — print the PC).
Key decisions: All the top-level decisions land here: Bus trait, Handler arity
enum, separate flag fields, the pc+1 interrupt quirk. Port handlers in the same order
as ins.c's table so diffs against C are positional. Port strict mode (stack
over/underflow checks) as a Cpu flag.
Gate: cargo test -p ewm-core passes dormann_6502 (PC reaches $3399). Unit
tests for status pack/unpack round-trip and stack wraparound.
Phase 2 — 65C02, disassembler, golden-trace harness (M)
Goal: 65C02 mode passes the extended-opcodes test, and any divergence from C is mechanically localizable.
Scope:
- 65C02 override handlers + table overlay in
ins.rs(BRA, STZ, PHX/PLX/PHY/PLY, SMB/RMB/BBR/BBS, TRB/TSB, decimal-corrected ADC/SBC,(zp)modes). fmt.c→fmt.rs(disassembly + trace-line formatting);Cputrace output in the same format as C.scripts/gen-golden-trace.sh(documented one-time C-side capture) + checked-in gzipped ~100k-instruction golden trace +ewm-core/tests/trace_compare.rs.
Key decisions: Golden files over live lockstep (see decisions table). Normalize the trace format (uppercase hex, fixed columns) in the capture script, not in the emulators.
Gate: cargo test -p ewm-core passes dormann_65c02 (PC reaches $24a8) and
trace_compare (zero divergence over the golden window).
Phase 3 — Apple 1 / Replica 1, headless (M)
Goal: A bootable headless Apple 1: Woz monitor prompt reachable and interactive through the PIA.
Scope:
pia.c→pia.rs: 6820 PIA at$D010; the C output callback becomes an output byte sink the frontend (or test) drains.one.cmachine half →ewm-core/src/one.rs:One::new(model)wires RAM + ROMs (apple1.rom,krusader.romviainclude_bytes!) and implementsBus.utl.c→util.rs.
Key decisions: Keyboard input = One::key(u8) pushing into the PIA (with the
7-bit masking one.c's callback applies); display output = PIA output bytes appended
to a buffer that the test (later: the tty renderer) drains — the same callback seam
as C, minus SDL.
Gate: ewm-core/tests/one_boot.rs: create an Apple 1, reset, step ~1M cycles,
feed E000.E00F\r to the Woz monitor, assert the drained output contains the
expected hex dump of that ROM region. A second test echoes a typed character
(mirrors tests/apple1/echo.s intent).
Phase 4 — Apple 1 SDL frontend (M)
Goal: ewm one runs windowed with keyboard and terminal display, matching the C
binary side by side.
Scope:
chr.cbitmap half →ewm-core/src/chr.rs(ROM3410036.bin→ glyph bitmaps); texture creation →ewm/src/chr.rs.tty.c→ewm/src/tty.rs;one.cSDL loop →ewm/src/one.rs;sdl.c→ewm/src/sdl.rs.- CLI subcommand
onewith--model/--memory/--traceinmain.rs.
Key decisions: Glyph decoding is core (unit-testable, no SDL); textures are frontend. The frame loop copies the C loop structure (event pump → CPU cycle budget → tty render) rather than inventing a new timing scheme.
Gate: Unit test: a known glyph ('A') decodes to the expected bitmap. Manual
checklist (record results in this file): cargo run -p ewm -- one --model replica1
shows the prompt; typing echoes; Krusader assembles a two-line program; behavior
matches src/ewm one run alongside.
Manual checklist results:
- Unit tests: 'A' glyph bitmap, inverse glyphs, unmapped codes (
chr.rs) cargo run -p ewm -- one --model replica1launches, renders, runs, and exits cleanly (smoke-tested 4s)- Prompt shows and typing echoes (verify by hand)
- Krusader assembles a two-line program
- Behavior matches
src/ewm onerun alongside
Phase 5 — Apple ][+ machine, headless, no disk (L)
Goal: A headless Apple ][+ that boots the ROMs into AppleSoft and evaluates BASIC, verified by text-page inspection.
Scope:
two.cmachine half →ewm-core/src/two.rs: RAM$0000–$BFFF, ROMs at$D000–$FFFF, full$C000–$C07Fsoft-switch dispatch (keyboard latch/strobe, TEXT/MIXED/PAGE2/HIRES, speaker toggle recording at$C030, annunciators, buttons$C061–$C063, paddle trigger/read$C070/$C064–$C067as settable fields), screen-mode state.alc.c→alc.rs: language-card bank state consulted byTwo::read/writefor$D000–$FFFFand the$C080–$C08Fswitch reads.
Key decisions: Speaker = a vector of cycle-stamped toggle events recorded on
$C030 access, drained by the frontend later (keeps sound out of core). A text-page
scraping helper Two::text_screen() -> String (decode the $0400 page using the
interleaved row offsets and charset) lives in core — it is the workhorse for every
headless gate from here on.
Gate: ewm-core/tests/two_boot.rs: boot, step until text_screen() contains the
] prompt (with a cycle cap); type PRINT 2+2\r via the key latch; assert 4
appears. Language-card tests: exercise $C08x sequences and assert read/write
banking matches the C semantics (including the double-read write-enable behavior).
Phase 6 — Disk II (L)
Goal: Boot DOS 3.3 from a .dsk image, fully headless.
Scope:
dsk.c→ewm-core/src/dsk.rs: nibblization of.dsk/.do/.po/.nibimages to GCR 6-and-2 on load (sector interleave tables, address/data field encoding), the embedded 256-byte boot ROM at$C600(const array),$C0E0–$C0EFIOM handling inTwo's bus dispatch, stepper phase→track math (dsk_phase_delta).- Writes remain no-ops (quirk #2).
Key decisions: The nibblizer gets its own unit tests (interleave tables, prologue/checksum bytes) since it is the most algorithmic, least observable code in the repo. Drive selection/mode flags mirror C exactly.
Gate: Unit tests: nibblize a known sector and assert the address-field prologue,
volume/track/sector, and checksum bytes. Integration ewm-core/tests/two_dos.rs:
insert disks/DOS33-SystemMaster.dsk, boot, step until text_screen() shows the DOS
banner and ]; type CATALOG\r; assert known filenames (e.g. HELLO) appear.
Phase 7 — Apple ][+ SDL frontend + boo menu (L)
Goal: Full windowed ewm two (and ewm boo) at feature parity with the C binary.
Scope:
scr.c→ewm/src/scr.rs: 280×192 renderer, TEXT (interleaved row offsets), LGR (16-color), HGR (including the color-fringing fix from #187), mixed mode, page 2, flashing text via the frame-phase counter,--color/green/white schemes.snd.c→ewm/src/snd.rs: drain the core's speaker toggle events → sample buffer → SDLqueue_audio(port of #188).two.cSDL loop →ewm/src/two.rs: 40 fps fixed step,1023000/fpscycle budget, joystick→paddles, key mapping, Cmd-Esc reset / Cmd-Return fullscreen / Cmd-P pause / Cmd-I status bar, status-bar tty overlay.boo.c→ewm/src/boo.rs; completemain.rsdispatch (one/two/boo, no args → boo; fake MHz display preserved).
Key decisions: The renderer reads &Two between step batches (the aliasing
decision realized). Add a hidden --screenshot <path> debug flag (dump the render
surface as BMP after N frames) so graphics gates are automatable and comparable
against the C build.
Gate: Automated: --screenshot after booting System Master matches a checked-in
golden BMP of the text screen. Manual checklist recorded here: boot System Master to
], CATALOG, RUN a program from DOS33-SamplePrograms.dsk exercising LGR and
HGR, speaker beeps on boot, paddles respond if a joystick is present.
Manual checklist results:
- Automated golden screenshot: headless System Master boot render matches
rust/ewm/golden/two-boot.bmpbyte for byte (testboot_screen_matches_golden_bmp) ewm two --color --drive1 DOS33-SystemMaster.dskopens, renders, and boots (smoke-tested via--screenshot);ewm(no args) shows the boo menu- Boot System Master to
]andCATALOGby hand RUNan LGR and an HGR program from DOS33-SamplePrograms.dsk- Speaker beeps on boot
- Paddles respond if a joystick is present
Phase 8 — Parity sweep, benches, docs (M)
Goal: Nothing left that the C binary does and the Rust one doesn't (except Lua).
Scope:
- Port
cpu_bench/mem_benchasewm-core/benches/(plainstd::timeharness, no criterion dependency, matching the C ops/sec output). - CLI flag audit against the
getopt_longtables inone.c/two.c/ewm.c;--trace/--strictwired end to end. - README gains a "Rust build" section; this file gains a filled-in parity checklist (flag by flag, feature by feature).
Key decisions: Benches are informational, not gated; record C-vs-Rust numbers in this file. Any deliberate divergence found in the audit gets a line added to "Quirks to preserve" — parity is documented, not assumed.
Gate: Every row of the parity checklist marked done or explicitly waived; all prior automated gates still green; bench numbers recorded.
Parity checklist (flags)
| Flag | C source | Rust | Status |
|---|---|---|---|
ewm --help/-h, one, two, boo, no args → boo |
ewm.c |
main.rs |
Done |
one --help |
one.c |
one.rs |
Done |
one --model apple1|replica1 (default replica1) |
one.c |
one.rs |
Done |
one --memory ram|rom:addr:path |
one.c |
one.rs |
Done |
one --trace[=file] (bare = /dev/stderr) |
one.c |
one.rs |
Done (writes traces; dead in C) |
one --strict |
one.c |
one.rs |
Done (inert, quirk #7) |
two --help |
two.c |
two.rs |
Done (usage says "default: 30", actual 40, as C) |
two --drive1/--drive2 <path> |
two.c |
two.rs |
Done |
two --color |
two.c |
two.rs |
Done |
two --fps <n> |
two.c |
two.rs |
Done (0 crashes, quirk #8) |
two --memory |
two.c |
two.rs |
Done |
two --trace[=file] |
two.c |
two.rs |
Done |
two --strict |
two.c |
two.rs |
Done (inert) |
two --debug |
two.c |
two.rs |
Done |
two --script (Lua) |
two.c |
— | Waived (Lua dropped) |
two --screenshot=<path> |
— | two.rs |
Rust-only debug flag (divergence #5) |
Parity checklist (features)
| Feature | Verified by |
|---|---|
| 6502 + 65C02 cores, all quirks | Dormann ×2, trace_compare, cycle counts equal to C |
| Apple 1 / Replica 1 machine + PIA | one_boot tests, --example one |
| Apple 1 SDL frontend (tty, keys, fullscreen, reset) | Phase 4 checklist (manual items open) |
| Apple ][+ machine, soft switches, key latch | two_boot tests, --example two |
| Language card banking | two_boot ALC tests |
| Disk II (nibblizer, boot ROM, stepper; writes no-op) | dsk unit tests, two_dos (DOS boots + CATALOG) |
| TEXT/LGR/HGR/mixed/page2 rendering, color schemes | golden BMP gate + Phase 7 checklist |
| Speaker sound (#188 port) | cycle-stamp test; manual beep check open |
| Paddles/buttons (controller) | ported; manual check open |
| Pause / status bar (fake MHz, quirk #3) / boo menu | Phase 7 smoke tests + manual checklist |
Bench numbers (2026-07-05, Apple Silicon; C at -O3, Rust bench profile)
mem_bench, ms per 100M ops (C → Rust): push_byte 185→199, pull_byte 162→165,
push_word 319→200, pull_word 171→196; get_byte 82→67, get abs/zp family
~70-84→~90; get ind/indx/indy 71-96→~195; set_byte family ~68-75→~90-97,
set ind* ~72-75→~195. The ind* gap is the dyn Bus dispatch replacing C's
direct cpu->ram reads; at 1.023 MHz the machine needs <0.5% of this.
cpu_bench, ms per 10M handler calls, avg of 3 (C → Rust): LDA imm 6→6,
LDA zpg 10→11, LDA (zp),Y 9→23, STA zpg 7→9, ADC imm 44→22, JSR 32→19,
RTS 17→19, SMB0 13→17. Full run: ./src/cpu_bench vs
cargo bench -p ewm-core --bench cpu_bench (optionally filtered by mnemonic).
Phase 9 — Remove C, promote Rust to root (M)
Goal: A single-language repo: Rust is EWM.
Scope:
- Delete
src/*.c,src/*.h,src/Makefile,src/CMakeLists.txt,.travis.yml,scripts/*.lua. git mvthe workspace to the root (rootCargo.toml);git mv src/rom romand fix theinclude_bytes!/test paths.- Keep
disks/andtests/(cc65 asm sources; mark their harnesses as manual). - CI drops the C job. README rewritten: build =
cargo build --release, run =cargo run --release -- two --color --drive1 disks/DOS33-SamplePrograms.dsk.
Key decisions: Do the move as pure git mv + path fixes in one commit and any
remaining edits in separate commits — this makes review of the destructive phase
trivial.
Gate: From a fresh clone: cargo test green (Dormann ×2, trace_compare,
one_boot, two_boot, two_dos), cargo run -- boo boots both machines, System Master
boots to ] and runs CATALOG; no .c/.h files remain; README instructions
verified by following them literally.
Completion notes: scripts/gen-golden-trace.* was deleted with the C tree —
the checked-in golden trace is frozen; regenerating it requires checking out the
C emulator from git history (tag: the Phase 8 merge). The rewrite is complete.
Post-rewrite refactors
Memory/Device abstraction restored (2026-07-05)
The port originally inverted the C ownership: the CPU held no memory, machines
implemented a Bus trait and pattern-matched addresses themselves. This was
refactored back to the C architecture: machines own the Cpu, the Cpu owns a
Memory (ewm-core/src/mem.rs, the mem.c region table: base-RAM fast path +
RAM/ROM/Device regions walked newest-first), and peripherals implement
Device — Pia, Alc (which now owns the machine ROM it shadows), Dsk, and
the new TwoIo (the $C000-$C07F soft switches, formerly inline in Two).
Instruction handlers dropped their bus parameter and match the C typedefs
(fn(&mut Cpu) etc.); Cpu::step stamps mem.cycles from counter, which
removed the Two.cycles mirror the run loops used to maintain. All golden
gates passed unchanged. Two notes:
Onepreviously dispatched--memoryextras before base RAM; the region table aligns it with the Caddr < ram_sizefast path (base RAM wins), observable only if an extra region overlaps base RAM.- The microbenches moved a few ms per 10M calls in places (the flat
TestBusarray became the region-walkingMemory;TestBusitself was replaced by a 64KMemory, ascpu_test.cused) — still orders of magnitude beyond the 1.023 MHz budget.
Crate split realigned (2026-07-06)
The workspace crates originally split headless-vs-SDL; they now split
kernel-vs-machine. ewm-core is the generic 6502 system kernel only —
cpu, mem, ins, fmt — with the CPU-level tests (Dormann, golden
trace) and benches. Everything Apple-specific moved to the ewm crate,
which gained a library target beside its binary: the devices (pia, dsk,
alc, chr) and the machines, where one.rs and two.rs now each hold
the machine and its SDL loop — the original one.c/two.c file layout.
The machine tests (boots, DOS 3.3, language card) and the headless example
consoles moved to ewm with them, so they now build in an SDL-linking
crate (CI already installs libsdl2-dev; the consoles run as
cargo run -p ewm --example …). Pure code motion; all gates unchanged.
Speaker synthesis diverges from snd.c (2026-07-06)
snd.c held the output at a DC rail forever after the last $C030 toggle,
so queue underruns (late frames during game loading) snapped the output to
silence and back — audible clicks real hardware never makes. snd.rs now
models the AC-coupled speaker: the level decays to zero (~4 ms time
constant), the queue is primed at startup, and frames carrying signal are
never dropped. Deliberate behavior divergence, with unit tests on the
waveform.
Reset rebound from Cmd-Esc to Cmd-R (2026-07-06)
The C key map used Cmd-Esc for machine reset, but on macOS AppKit claims
Cmd-Esc as a cancel key equivalent (cancelOperation:) before SDL ever
sees the event — confirmed with --debug key logging: Cmd-Return arrives,
Cmd-Esc never does, in the C as much as in the Rust. Reset is now Cmd-R in
both frontends (Cmd-letter combos are reliably delivered; Cmd-I and Cmd-P
already depend on that). Divergence from the C key map. The --debug flag
now also logs KeyDown events, which is how this was diagnosed.
Cycle accounting corrected to real hardware (2026-07-06)
The C's instruction table undercounted cycles — all memory-mode SBCs at 2
(real 3-6), BRK at 2 (real 7), DEC abs at 3 (real 6), ORA zp/zp,X off by
one — and charged no penalties for taken branches (+1, +2 cross-page) or
page-crossing indexed reads (+1), so the machine ran ~15-25% fast despite
correct wall-clock frame pacing. All counts now match the 6502 datasheet,
penalties accrue via Cpu::extra_cycles, and the ROM WAIT $FCA8 routine
measures cycle-exact against its documented (26+27A+5A²)/2 formula
(ewm/tests/two_timing.rs, the permanent gate). A deliberate divergence
from the C's counts: Dormann/golden-trace gates compare architectural
state, not cycles, and are unaffected; the golden screenshot proved
byte-identical (the boot settles either way). Quirk #3's "fake ≈1.023 MHz"
display is now a true 1.023 MHz. The frontends also advance their frame
deadline instead of re-reading the clock after render, so frame time no
longer stretches the period. 65C02-specific timing (decimal +1, fixed
page-cross reads) remains out of scope.
Sequencing notes
- Phases are strictly ordered 0→9. Phase 4 may be deferred until after 5/6 if headless momentum is preferred; nothing else reorders.
- The golden-trace harness (Phase 2) is the debugging tool of first resort for any CPU-level regression discovered in later phases.
Future work (out of scope for the port)
- Lua scripting via
mlua(re-expose the cpu/machine hookslua.chad). - Disk II write support;
.wozimage support. - Apple II (non-plus) and Apple IIe machine models.
- Real measured MHz display; configurable speed throttling.
- A debugger (listed in README goals, never implemented in C).