Volker Schwaberow

Contributing to Copperline, a Cycle-Driven Amiga Emulator in Rust

Contributing to Copperline, a Cycle-Driven Amiga Emulator in Rust

I spent the past few weeks contributing to Copperline, a cycle-driven Commodore Amiga emulator written in Rust.

Copperline interests me as another implementation alongside FS-UAE and vAmiga. I do not follow “rewrite it in Rust” as a rule.

FS-UAE and vAmiga solve the same problem in C and C++, and their code has gone through years of development and use.

Working on Copperline lets me explore a different design without dismissing the work that already exists.

The work ranged from a looping WebAssembly board to the missing G in the Chambers of Shaolin intro. Along the way, I touched six parts of the emulator.

WasmBoard fault isolation and transactional DMA

Copperline runs Zorro expansion boards as WebAssembly modules. A looping guest exhausted its fuel and trapped, but Copperline called it again every 50 Hz vertical blank.

In one test, this cycle consumed 2.5 billion host CPU instructions per second. Emulation fell from 50 FPS to about 1 FPS.

Pull request 451 turns a trap into a permanent fault state. Copperline then removes the board’s mappings, timers, and interrupt request lines.

I also staged guest DMA writes in a temporary buffer. Copperline commits them after a successful execution slice and discards them after a trap.

Savestates preserve the fault state. The host stays at 50 FPS, and an interrupted DMA transfer leaves Amiga memory unchanged.

Two-level case-insensitive LRU path cache

AmigaDOS ignores letter case in paths. resolve_path() handled different host spelling by scanning directories with read_dir().

Deep WHDLoad collections made it scan the same directories repeatedly. In pull request 448, I added two LRU caches.

One cache resolves individual components. The other caches complete paths. Both reuse allocated slots, so a hit does not allocate on the heap.

For 6,000 lookups, directory scans needed 15.31 ms. The caches needed 4.73 ms, which gave a 3.23× speedup on a warm host disk cache.

Video raster centering for non-standard display windows

Issue 465 showed the Chambers of Shaolin intro shifted 89 pixels left. I traced that offset to present_h_shift.

In pull request 467, I replaced the dynamic calculation with the calibrated 24-pixel shift for standard frames.

The frame moved back into place, but part of the logo remained missing. Maintainer Andrew Hutchings pointed this out during review.

Andrew reopened the issue and wrote pull request 468. The remaining fault came from Denise’s horizontal display-window flip-flop.

Its beam counter wraps after $1C7, but the intro sets a stop value of $1D8. The comparator never fires, so the window stays open.

Copperline modeled that state in scan_h_window_line, but its painter still began at DIWSTRT.

PR 468 extends the paint start on carried-open rows and subtracts that extension from native_x_offset. The fetched data keeps its beam position.

Andrew compared the result with A500 photos, vAmiga, and FS-UAE. The missing G in GRANDSLAM returned, and issue 465 closed.

Sprite clipping still uses the register-derived window. Copperline records that separate gap in docs/internals/video.md.

I had started with a centering bug. The second half turned out to be a timing bug.

Debugger address masking and copper list alignment

The debugger only helps when it follows the machine that it observes. Pull request 437 fixed three places where it did not.

last_writer now applies the 24-bit or 32-bit bus mask for the selected Amiga. This removes phantom writes caused by address mirroring.

Copper dumps now start on a 16-bit word boundary. The command parser also accepts the $ hexadecimal prefix and fp register alias.

Latin1 volume label encoding

The volume-label fault came down to bytes. AmigaDOS stores a label as a BCPL string and expects Latin1, while host tools supplied UTF-8.

Pull request 438 converts the label before writing the root block. Workbench now displays German umlauts and accented letters correctly.

M68000 disassembler refactoring

The M68000 disassembler repeated operand formatting across its integer ALU paths. Pull request 454 moved that logic into shared code.

The same code now handles <ea>, Dn and Dn, <ea>. Regression tests cover every addressing mode, so the refactor cannot silently change debugger output.

Pull requests

The table lists my six contributions and Andrew Hutchings’ follow-up to the video fix.

PR Area Change
#437 Debugger Bus masking, Copper alignment, and parser forms
#438 Disk images Latin1 encoding for BCPL volume labels
#448 Filesystem Two-level LRU path cache
#451 WasmBoard Fault isolation and transactional DMA
#454 Disassembler Shared ALU formatting and regression tests
#467 Video Fixed centering for standard frames
#468 Video Andrew Hutchings’ carried-open DIW fix