- Benches: cpu_bench.c and mem_bench.c ported as plain std::time
cargo benches (harness = false, no criterion), same output formats
so C and Rust numbers compare directly. The mem addressing helpers
in ins.rs become pub, mirroring mem.h. Numbers recorded in
REWRITE.md: parity or better on most paths (Rust ADC 2x faster,
JSR 1.7x faster; ind* modes ~2.5x slower from dyn Bus dispatch —
all orders of magnitude beyond the 1.023 MHz requirement).
- --trace wired end to end: Cpu grows a trace sink written before
each step via the fmt.rs formatters ('PC: disassembly state');
both frontends open it from --trace[=file]. In C the write path
was dead code — recorded as a documented divergence. Unit-tested
against exact expected lines, and smoke-tested (767k lines from a
3s replica1 run, matching the Woz monitor reset sequence).
- --strict audited: accepted but inert at runtime in both C and Rust
(the only runtime caller of irq/nmi discards the error) — recorded
as quirk #7.
- REWRITE.md: flag-by-flag and feature-by-feature parity checklist,
a new 'Documented divergences' section (exact --flag parsing, no
PC in unexpected-I/O messages, frame-sampled joystick, working
--trace, Rust-only --screenshot, the skipped dsk.c OOB write,
no Lua), bench numbers, Phase 8 -> Done.
- README: 'Building the Rust version' section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ewm two and ewm boo run windowed; the frontend is feature-complete
against the C binary (minus Lua, by decision):
- ewm/src/scr.rs: scr.c — TEXT with interleaved rows and flashing,
LGR 16-color blocks, HGR in green monochrome or color (including
the fringing fix from #187: adjacent-pixel white detection and
column-parity coloring), mixed mode, page 2. Rendering is pure
pixels (no SDL) so the golden screenshot gate runs headless in CI;
the loop uploads the buffer as a streaming texture. Includes a
dependency-free BMP encoder for --screenshot and the golden test.
- ewm/src/snd.rs: snd.c (#188) — the core's cycle-stamped $C030
toggles replay into a square-wave sample buffer queued to SDL,
with the same 100ms backpressure check.
- ewm/src/two.rs: the SDL loop of two.c — fixed 40 fps step with
1023000/fps cycle bursts, --drive1/--drive2/--color/--fps/--memory/
--trace/--strict/--debug, controller buttons and joystick paddles,
the key mapping (with two.c's TAB fallthrough and alt-keyup-only
button clearing), Cmd-Esc reset, Cmd-Return fullscreen, Cmd-P pause
with the tty overlay, Cmd-I status bar with the fake ~1.023 MHz
display (quirk #3) and drive lights, and the hidden
--screenshot=<path> debug flag (BMP after 120 frames, then exit).
- ewm/src/boo.rs: boo.c — the bootloader menu; main.rs dispatches
one/two/boo with no args running boo, as ewm.c does.
- Golden gate: rust/ewm/golden/two-boot.bmp is the System Master
boot screen rendered after a fixed 100M cycles; the test
boot_screen_matches_golden_bmp re-renders and compares byte for
byte (regenerate with EWM_WRITE_GOLDEN=1).
- Core: Two::ram() read access for the renderers, extra --memory
regions (dispatched after base RAM like the C fast path), and
Dsk::active_drive() for the drive lights.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Phase 6: Disk II — DOS 3.3 boots headless
Ports dsk.c:
- dsk.rs: the 16-sector slot 6 controller with two drives. Disk
images (.dsk/.do/.po/.nib) are nibblized to GCR 6-and-2 tracks on
load: interleave tables, 4-and-4 address fields with volume/track/
sector/checksum, the 6-and-2 data field with the Beneath Apple DOS
write table, and per-track gap layout. The embedded 256-byte boot
ROM lands at $C600; $C0E0-$C0EF handles stepper phases (the
dsk_phase_delta half-track table, clamped), drive select/on/off,
and the read/write latch — including dsk.c's skip counter that
makes every fourth read return 0. Writes only touch the in-memory
nibble stream and never persist (quirk #2).
- One deliberate divergence, documented in the code: dsk_convert_
sector in C writes 2 bytes past its 342-byte nibble buffer for
idx6 = 0x100/0x101 (the apple2js source it derives from discards
those writes into a JS typed array). Rust bounds checks caught it;
the store is skipped, matching the effective C/JS behavior.
- Two: bus dispatch for the boot ROM and IOM, plus load_disk()
(ewm_two_load_disk).
- Tests: nibblizer unit tests (4-and-4 round trip, address/data
field layout and checksum, track interleave order, the every-
fourth-read-is-zero quirk, stepper clamping) and tests/two_dos.rs —
boots DOS33-SystemMaster.dsk to the DOS banner, types CATALOG,
asserts DISK VOLUME 254 and HELLO.
- The interactive example takes a disk image now:
cargo run -p ewm-core --example two -- ../disks/DOS33-SystemMaster.dsk
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix two_boot for the always-present Disk II
Since Phase 6 the machine always has the Disk II controller, as in
ewm_two_init — so booting without a disk hangs in the slot 6 boot
ROM polling the empty drive, exactly like the C build and the real
machine. The Phase 5 boot test now boots the System Master and
evaluates PRINT 2+2 at the DOS-loaded AppleSoft prompt.
(The local gate had masked this: the test failure disappeared behind
a pipe. Verified with unmasked exit codes now — 31 tests green.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Phase 5: headless Apple ][+ machine
Ports the machine half of two.c and the language card:
- two.rs: Two implements Bus with the ewm_two_init memory map — 48K
RAM, the six AppleSoft/Autostart ROMs at $D000-$FFFF (embedded),
and the full $C000-$C07F soft-switch dispatch: keyboard latch and
strobe, TEXT/GRAPHICS, LGR/HGR, FULL/MIXED, PAGE1/PAGE2, speaker
toggle recording at $C030, annunciators, buttons $C060-$C063,
and the paddle trigger/timers at $C070/$C064-$C065 (including
two.c's PADL1 never clearing its timer). apple2 and apple2e return
an error, per quirk #4. Speaker and paddle timestamps come from a
cycles field the run loop mirrors from cpu.counter, since the C
handlers read cpu->counter and the Bus trait has no CPU access.
- two.rs: Two::text_screen() decodes the interleaved $0400 text page
— the workhorse assertion helper for all later headless gates.
- alc.rs: the language card from alc.c as plain state consulted by
Two's bus dispatch: bank select on bit 3, write-enable only after
two consecutive switch reads (writes reset the count and the
$C081/$C083 read cases preserve a previously enabled write flag),
reads falling through to ROM when read-disabled, and writes
swallowed when write-disabled.
- tests/two_boot.rs: boots to the APPLE ][ banner and ] prompt with
no disk card, types PRINT 2+2 through the key latch and asserts 4;
plus language-card tests for initial state, double-read write
enable, count reset on switch write, bank separation and shared
$E000 RAM, cycle-stamped speaker toggles, and the apple2/apple2e
error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add an interactive BASIC console example for the headless Two
cargo run -p ewm-core --example two boots to the AppleSoft prompt and
redraws the 40x24 text screen after each entered line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ewm one now runs windowed:
- ewm-core/src/chr.rs: the bitmap half of chr.c — the 2716 character
ROM (3410036.bin, embedded) decodes to 7x8 glyphs indexed by Apple ][
screen code, with the same table layout as ewm_chr_init (normal
$A0-$DF, inverse $00-$3F, flashing-as-inverse $40-$7F). Unit
tests pin the 'A' glyph, inverse glyphs, and unmapped slots.
- ewm/src/tty.rs: tty.c — 24x40 screen buffer, scrolling, blinking
cursor, rendered into a 280x192 pixel buffer.
- ewm/src/sdl.rs: sdl.c — pixel-format pick and green phosphor color.
- ewm/src/one.rs: the SDL loop of one.c — 40 fps cycle bursts
(1023000/40), the same key mapping (including one.c's TAB
fallthrough that also sends DEL, and ctrl-a mapping to 0x00),
Cmd-Esc reset, Cmd-Return fullscreen, and --model/--memory/
--trace/--strict flags. Extra --memory regions are supported by
new One::add_ram/add_rom (dispatched LIFO like the C mem list).
- ewm/src/main.rs: subcommand dispatch; two/boo report not ported.
The sdl2 crate now uses the use-pkgconfig feature: linking the real
SDL symbols needs the Homebrew/pkg-config library path.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Phase 3: headless Apple 1 / Replica 1
Ports the machine layer for the One:
- pia.rs: the 6820 PIA from pia.c. The C output callback becomes a
return value — Pia::write yields (ddr, byte) when an output register
is written — and reads keep the IRQA1-clear side effect.
- one.rs: One implements Bus with the C memory map (Apple 1: 8K RAM +
Woz monitor ROM at $FF00 on a 6502; Replica 1: 32K RAM + Krusader
ROM at $E000 on a 65C02; PIA at $D010). ROMs are embedded with
include_bytes! so nothing needs to run from src/. Display bytes are
collected in a drainable buffer, with the Apple 1's 7-bit masking
applied exactly where ewm_one_pia_callback did. One::key latches
key | 0x80 into the PIA and raises IRQA1, per ewm_one_keydown.
- tests/one_boot.rs: both machines boot to the Woz monitor prompt;
FF00.FF0F (Apple 1) and E000.E00F (Replica 1) dumps are verified
against the actual ROM bytes; typed characters echo back.
utl.c is not ported: ewm_utl_endswith is str::ends_with, and the
clock_gettime shim is obsolete.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add an interactive console example for the headless One
cargo run -p ewm-core --example one -- [apple1|replica1] gives a
line-based Woz monitor console, so the machine can be tried by hand
before the SDL frontend lands in Phase 4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Make the example's demo program print HI!
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Completes the CPU core port:
- ins.rs: the 65C02 override handlers ((zp) modes, BRA, STZ, PHX/PHY/
PLX/PLY, TRB/TSB, RMB/SMB, BBR/BBS, INC/DEC A, BIT imm/zpgx/absx,
JMP (abs,X)) and the override table transcribed verbatim from
instructions_65C02 — including its 1-cycle NOPs and the stray 0x03
opcode fields. The table is overlaid onto the 6502 table once, the
Rust version of cpu_initialize()'s back-fill. C's arity-cast NOPs
become explicit nop_byte/nop_word wrappers.
- fmt.rs: cpu_format_state, cpu_format_stack, and the disassembler
from fmt.c, bugs preserved (0x9c prints JMP; BBR/BBS targets are
computed from pc+2).
- tests/dormann.rs: dormann_65c02 passes at $24a8 after 65,094,140
cycles — exactly the C emulator's count.
- Golden-trace harness: scripts/gen-golden-trace.{c,sh} builds a
capture program against the unmodified C sources and records the
first 100k instructions of the functional test as normalized
'PC A X Y SP P' lines; checked in gzipped. trace_compare.rs replays
it on the Rust core and reports the first divergent instruction
(currently: zero divergence). flate2 is a dev-dependency only.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Ports cpu.c, mem.c, and the 6502 half of ins.c to rust/ewm-core:
- bus.rs: the Bus trait (replacing the mem_t linked list) and a flat
64K TestBus for the CPU test harness
- cpu.rs: registers, separate raw-valued flag fields packed/unpacked
only in status()/set_status(), stack ops with u8 sp wraparound,
reset/irq/nmi (keeping the pc+1 interrupt push quirk the Dormann
tests depend on), strict-mode stack overflow checks, and step()
with pc advanced before handler dispatch
- ins.rs: the 3-variant Handler arity enum, addressing-mode and RMW
helpers ported expression-for-expression from mem.c (including the
indx zero-page wrap and the indy non-wrap), ~200 handlers in ins.c
source order, and the 256-entry table transcribed verbatim
- tests/dormann.rs: the cpu_test.c harness (success PC $3399,
branch-to-self deadlock detection) plus unit tests for status
round-trip, stack wraparound, and the indexed-indirect quirks
The Rust core executes 88,957,316 cycles for the functional test —
exactly the C emulator's count.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Phase 0: Rust workspace scaffolding + CI
Adds the parallel Rust workspace under rust/ per REWRITE.md:
- ewm-core (headless lib, zero dependencies) and ewm (binary,
declares sdl2 now so CI installs SDL2 headers from day one;
prints the same usage line as the C binary)
- rust-toolchain.toml pinning 1.91.1 with rustfmt + clippy
- GitHub Actions CI: C build job (ubuntu + libsdl2-dev) and Rust
job (fmt --check, clippy -D warnings, test)
- Anchor the 'ewm' gitignore pattern to src/ so rust/ewm is tracked;
ignore rust/target/
- README note pointing at REWRITE.md
The C tree in src/ is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* CI: build the C tree on macOS
gcc on ubuntu fails -Werror with -Wimplicit-fallthrough in two.c;
src/ is frozen until Phase 9, so build C where it has always built
(macOS, as Travis did).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* CI: point clang at the Homebrew prefix via CPATH/LIBRARY_PATH
The committed Makefile does not search /opt/homebrew, so SDL2
headers are not found on the arm64 macOS runner. src/ is frozen,
so pass the paths through the environment instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add sound subsystem that emulates the Apple II speaker by:
- Tracking speaker state toggles when $C030 is accessed
- Generating audio samples based on CPU cycle timing
- Outputting 44.1 kHz audio via SDL
New files: snd.h, snd.c
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace the 2-bit pairing approach with per-pixel rendering to eliminate
color artifacts at edges. The new implementation processes each pixel
individually, uses each byte's own high bit for palette selection, and
properly detects adjacent pixels for white rendering.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>