This chapter is the map; the following chapters zoom into the timing model, the chipset modules, the video pipeline, the CPU integration, the Peripherals and expansion, and the save-state format.
Source layout¶
src/
main.rs # thin CLI binary: arg parse, config load, boot
lib.rs # the copperline library crate all of src/ lives in
config.rs # TOML config + validation + machine profiles
envcfg.rs # cached COPPERLINE_* environment-variable snapshot
emulator.rs # frame loop driving CPU, chipset, and host I/O
debugger.rs # env-driven headless debugger
disasm.rs # 68000 + Copper-list disassemblers
cpu.rs # m68k core wrapper and CPU-visible bus adapter
cache.rs # 68020/030/040 on-chip instruction/data cache model
bus.rs # shared RAM, ROM, chipset, CIA, RTC, and I/O state
bus/ # size-split impl Bus continuations + the bus test suite
custom_regs.rs # custom-chip register read/write dispatch
dma_slots.rs # chip-bus slot arbitration + DMA scheduling
collisions.rs # live (beam-timed) collision accumulation
frame_capture.rs# per-frame sprite/bitplane DMA + render capture
memory.rs # chip/slow RAM, ROM, extended ROM containers
romsearch.rs # locate the bundled AROS default boot ROM
zorro.rs # Zorro II/III autoconfig chain and boards
floppy.rs # disk images + timed disk DMA controller
dms.rs # DMS archive decompression
drive_sounds.rs # synthesized floppy-drive sound effects
gayle.rs # A600/A1200 Gayle gate array + IDE
a2091.rs # A2091 SCSI controller board (DMAC + boot ROM)
scsi.rs # WD33C93A SBIC + SCSI-2 disk targets
harddrive.rs # shared hard-drive image backend (IDE + SCSI)
dirfs.rs # host directory -> in-memory FFS partition image
cdrom.rs # CD image (BIN/CUE) parsing
cdtv.rs # CDTV DMAC + Matshita drive model
akiko.rs # CD32 Akiko (C2P, NVRAM, Chinon drive)
rtc.rs # MSM6242-compatible battery RTC
serial.rs # Paula serial sink (stdout)
audio.rs # AudioSink trait + cpal/WAV/null outputs
priority.rs # opt-in realtime-like thread scheduling (pacer + audio)
gamepad.rs # gilrs input + guided calibration
screenshot.rs # PNG export helpers
recorder.rs # video+audio capture (ZMBV/PCM AVI writer)
inputrec.rs # live-input recording to the scripted-input format
inputsched.rs # deterministic input replay for reverse debugging
savestate.rs # whole-machine snapshot/restore (versioned file format)
timetravel.rs # reverse-debugging snapshot ring + replay
timestamp.rs # compact wall-clock stamps for generated filenames
chipset/
agnus.rs # beam counters, DMACON, display fetch, arbitration data
copper.rs # Copper decode + cycle-stepped execution
blitter.rs # scheduled per-DMA-slot blitter engine
paula.rs # interrupts, audio DMA, serial, disk regs
denise.rs # palette + bitplane/sprite control registers
cia.rs # 8520 CIA model (CIA-A and CIA-B)
keyboard.rs # bit-timed 6500/1 keyboard MCU into CIA-A
video/
beam.rs # beam-position event index for the renderer
bitplane.rs # event replay + planar->RGBA renderer
bitplane/ # size-split renderer submodules + its test suite
# (sprite.rs, fetch.rs, output.rs, diag.rs, tests.rs)
deinterlace.rs # motion-adaptive deinterlacer
window.rs # winit ApplicationHandler + render worker + main/tool pixels surfaces + status bar
window/ # size-split window submodules + its test suite
# (statusbar.rs, present.rs, host_input.rs, tests.rs)
ui.rs # pop-up menu, overlay panels, and debugger/analyzer panel drawing
font.rs # 8x8 overlay font
crates/m68k/ # vendored m68k CPU core
tests/ # ignored integration tests (need local ROM assets)
timing-test/ # bootable cross-emulator timing-measurement diskThe big picture¶
Copperline’s emulated machine runs synchronously on the main thread inside
winit’s event loop. There is no emulation thread and no locking between CPU
and chipset: each turn of the loop (Emulator::step_real,
src/emulator.rs) advances the deterministic core a frame’s worth of
emulated time, cycle-stepping the CPU and the chipset together. By default,
the completed-frame renderer runs one frame behind on a worker thread; set
COPPERLINE_THREADED_RENDER=0 to use the synchronous renderer for
comparison.
The flow of a frame:
The frame loop hands the CPU an instruction budget. The CPU executes one instruction at a time through the vendored m68k core; every memory access the instruction makes is routed through the bus adapter and billed in colour clocks (CCK, 3.546895 MHz -- the chip bus clock).
Advancing the clock for a CPU access also advances everything else: Agnus beam counters, Copper fetches, blitter slots, Paula audio and disk DMA, CIA timers. The chip bus is arbitrated per colour clock, so a CPU chip-RAM access that loses arbitration genuinely waits (The timing model).
When the CPU sits in
STOP, the loop fast-forwards device time to the next event (timer underflow, raised interrupt) instead of spinning.Render-relevant register writes (by Copper or CPU) are recorded as beam-position events. At the frame boundary, the bus turns the completed frame’s events, chip-RAM snapshot, display geometry, and Agnus blanking latches into an owned
RenderInput; the renderer replays that snapshot, never the live chipset state (The video pipeline).In the default path
window.rssendsRenderInputto thecopperline-renderworker while the main thread advances the next frame. The worker paints into a CPU framebuffer, owns the deinterlacer history, and returns a presentation buffer tagged with the emulated frame. GPU upload and winit/window operations stay on the main thread.For screenshots, frame dumps, video recording, debugger stepping, and run-to-PC commands, the window code waits for the worker result for the exact emulated frame being captured or inspected. For normal interactive display, one frame of presentation latency is allowed.
For the interactive window the loop sleeps to pace emulated time to wall-clock; for headless captures it does not. The emulated result is identical either way -- pacing only schedules host work.
So an interactive run uses three host threads: the main thread (event
loop, core, and pacer), the copperline-render worker, and the
cpal audio callback that cpal owns. Only the last two cross a thread
boundary with the main thread, and both do so through owned data (a
RenderInput/presentation buffer over a channel, and a lock-free sample ring
buffer) rather than shared mutable state. The pacer and the audio callback are
latency-critical and can optionally be given above-normal scheduling priority
([emulation] realtime_priority, src/priority.rs); see
The timing model for what that does per platform.
The Bus¶
Bus (src/bus.rs) owns all shared machine state: chip/slow/fast RAM and
ROM (Memory), both CIAs, the RTC, all custom-chip state (Agnus, Copper,
blitter, Paula, Denise), the floppy controller, the Zorro chain, and the
optional Gayle, Akiko, and CDTV subsystems. Everything routes through it;
there is exactly one owner for every register.
The CPU-visible memory map:
| Range | Contents |
|---|---|
$000000 - chip top | Chip RAM (512K-2M; ROM overlaid at $0 after reset until CIA-A releases /OVL) |
$200000 - | Zorro II space (fast RAM autoconfig boards) |
$A00000 / $BFxxxx | CIA-A (odd bytes, /LDS) and CIA-B (even bytes, /UDS) |
$B80000 | Akiko (CD32 only) |
$C00000 | Slow (“ranger”) RAM, up to 512K |
$DA0000 / $DE1000 | Gayle IDE task file / Gayle ID and status (A600/A1200) |
$DC0000 | Battery RTC (MSM6242 view) |
$DFF000 | Custom-chip register window |
$E80000 | Zorro autoconfig window (then CDTV DMAC first on CDTV) |
$E00000 / $F00000 | Extended ROM (CD32 / CDTV) |
$F80000 | Kickstart ROM (512 KiB) |
$40000000+ | Zorro III space (32-bit CPUs) |
MMIO stubs cover every “gap” region DiagROM probes during fast-RAM detection, so probing software sees bus-like behaviour everywhere.
A CPU read of an address no region claims floats to the last value the
chip data bus carried (Bus.data_bus, fed by the live display and audio
DMA fetches), as on real Agnus-arbitrated hardware -- not a fixed all-ones
pattern; on a blank screen that value is 0. The constant matters: software
that chases a pointer off into unmapped space (e.g. a filesystem walking a
corrupted buffer-cache chain) can loop forever on a fixed value, where the
ever-changing floating value lets the chase wander to a zero terminator as
on silicon. Device windows that decode their own floating bus keep their
own values -- e.g. the A2091 board’s unpopulated XT-interface bytes read
$FF from its own model, not the chip data bus.
Determinism and the host boundary¶
The core’s only inputs are the config, the loaded images, and the
timestamped input events (host keyboard/mouse/gamepad in windowed runs;
--press-after-style scripted events in headless runs). Audio is rendered
in emulated time and resampled at the host boundary; wall-clock affects
scheduling only. This is what makes --screenshot-after runs exactly
reproducible, lets the headless debugger replay a failure
deterministically, and makes save states exact: a restored
run is byte-identical to one that was never interrupted.
The one host-clock value that reaches emulated state is the battery RTC: a
guest that reads it ($DC0000) sees the host date and time, so RTC reads
are not reproducible across wall-clock runs. COPPERLINE_RTC_FIXED_SECS=
unix-seconds pins the clock to a fixed value, which is what makes
differential traces against another emulator line up.
Input scripting and recording (inputrec.rs)¶
Scripted input (--press-after and friends, or a --script file) fires
at the first frame boundary at-or-after each event’s emulated timestamp.
The input recorder (window shortcut / --record-input) produces those
scripts from a live session by combining two capture styles: direct
hooks where event identity matters (the keyboard choke point
handle_amiga_key_event, floppy inserts) and a once-per-quantum diff of
the live InputState for port-1 mouse buttons, mouse motion (wrapped
quadrature-counter deltas become mouse-after directives), and the
port-2 joystick/CD32 pad. Two details keep record-then-replay
byte-identical: recorded timestamps are the emulated times the events
were applied (frame boundaries, not the wall-clock moment the host
delivered them), and times/holds are floored -- never rounded -- to
milliseconds, because rounding a boundary time up would push the
replayed event one frame late. The end-to-end gate is the same as for
save states: record a scripted run, replay the recording, cmp the
screenshots. User-facing usage is in
Input recording and script files.
envcfg: environment variables are start-up settings¶
All COPPERLINE_* knobs are read through src/envcfg.rs, which snapshots
the entire environment once into a static map on first access. Hot paths
(per-instruction, per-cycle) consult these knobs; a live std::env::var
call there would take the process-wide environment lock millions of times a
second and starve the audio thread of the same lock.
Three consequences for contributors:
Never call
std::env::var*for aCOPPERLINE_*knob -- useenvcfg::flag/envcfg::var.Values cannot change at runtime; every knob is a start-up setting.
On genuinely hot paths (per pixel, per colour clock, per device tick), even
envcfg::flag/envcfg::varare too expensive: each call hashes the variable name to probe the snapshot map. Cache the value once through aOnceLockhelper next to the call site -- seedbg_cia_on()andno_disk_stall()insrc/bus.rsorclamp_planes_setting()insrc/video/bitplane.rsfor the pattern. (A per-pixelenvcfg::varcall in the playfield decoder once cost ~20% of total host CPU.)