Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Architecture overview

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 disk

The 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:

  1. 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).

  2. 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).

  3. When the CPU sits in STOP, the loop fast-forwards device time to the next event (timer underflow, raised interrupt) instead of spinning.

  4. 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).

  5. In the default path window.rs sends RenderInput to the copperline-render worker 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.

  6. 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.

  7. 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:

RangeContents
$000000 - chip topChip RAM (512K-2M; ROM overlaid at $0 after reset until CIA-A releases /OVL)
$200000 -Zorro II space (fast RAM autoconfig boards)
$A00000 / $BFxxxxCIA-A (odd bytes, /LDS) and CIA-B (even bytes, /UDS)
$B80000Akiko (CD32 only)
$C00000Slow (“ranger”) RAM, up to 512K
$DA0000 / $DE1000Gayle IDE task file / Gayle ID and status (A600/A1200)
$DC0000Battery RTC (MSM6242 view)
$DFF000Custom-chip register window
$E80000Zorro autoconfig window (then CDTV DMAC first on CDTV)
$E00000 / $F00000Extended ROM (CD32 / CDTV)
$F80000Kickstart 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: