This chapter shows common debugging workflows that combine the debugger window, Debugger console, Frame Analyzer, reverse execution, headless options, GDB remote stub, and the A/B divergence finder.
Diagnosing sprite rendering issues¶
If an on-screen sprite disappears, flickers, or displays incorrect graphics:
Pause before the failure point: Step backward if necessary (
RFRAMEin the console steps back one frame).Inspect the Video tab: It decodes each sprite’s position (
SPRxPOS,SPRxCTL) and DMA line count, with a thumbnail of the fetched graphics.If a sprite is armed but fetches zero DMA lines, check whether sprite DMA is disabled in
DMACON.If the sprite contains valid graphics data but renders at the wrong coordinates, inspect the Copper list positioning instructions.
Trace register writes: In the console, run
RWATCH DMACONto find when and where sprite DMA was changed.Isolate layers: On the Video tab, toggle individual sprites or bitplanes to see which one draws a given part of the screen.
Investigating Copper list corruption and visual artifacts¶
If the display shows raster splits on the wrong scanlines or corrupted palettes:
Open the Frame Analyzer: Press
Uto draw the rendered picture beneath the chip-bus slot heatmap.Inspect scanline writes: Select a slot on the affected scanline to decode the custom register writes (
COLORxx,BPLxPTH) made near that beam position.Set a beam trap: Set a trap at the problem scanline (e.g.
BTRAP 145in the console, or select the slot and use Run to beam in the Frame Analyzer). Execution halts when the beam reaches that line.Single-step the Copper: Switch to the Copper tab and use CStep (
C) to execute Copper instructions one at a time acrossWAITboundaries.Find a memory change:
WRITER ADDRreplays retained snapshots and moves back to the last observed change of that word. Its PC identifies the CPU step around the change; useWATCH ADDR CPUorWATCH ADDR BLITTERto distinguish the writer on a forward run.
Identifying memory corruption¶
When tracking down overwritten data buffers or corrupted OS structures:
Set a memory watchpoint: In the console, run
WATCH ADDR(orWATCH ADDR BLITTERto catch only blitter writes).Reverse lookup: If memory has already been corrupted, run
WRITER ADDRto replay the snapshot ring and find the instruction responsible for the write.Bisection with save states: Use
--save-state-afterand--load-stateto narrow down when the corruption happened.
Diagnosing Guru Meditation crashes and unhandled exceptions¶
Catch system alerts: In the console, enter
CATCHALERT. Emulation halts whenexec.library/Alert()is called, before the alert screen is drawn.Decode alert codes: Run
GURUto translate the alert code in registerD7into a description.Inspect the call stack: Use
STACKandHISTORYin the console to inspect recent subroutine calls and retired program counters. Step backward withRSTEPto inspect the state before the crash.Inspect Exec tasks: Run
TASKSto view the ready and waiting task queues, orTASK <name>to inspect a task’s stack pointers and signal allocations.
Locating in-game variables (Memory search / Trainer workflow)¶
Start a search: In the console, enter
HUNT START(orHUNT START Bfor a byte search).Filter by value: If searching for a lives counter starting at 3, run
HUNT EQ 3.Update and narrow: Change the in-game value (e.g. lose a life) and run
HUNT EQ 2.Review candidates: Run
HUNT LISTto view the matching memory addresses.Set watchpoints or modify: Attach a watchpoint (
WATCH ADDR) or change the value (POKE ADDR 9writes a word; usePOKE.B ADDR 9after a byte search).
Logic analyzer waveform capture¶
To investigate fine-grained DMA and bus arbitration timing:
WAVE START glitch.vcd beam=100 2fThis arms a capture that triggers at scanline 100 and records two frames of
chip-bus activity. Open the resulting .vcd file in GTKWave to inspect the exact
cycle-by-cycle interleaving of the CPU, Copper, blitter, and DMA channels.
Bisecting an emulator regression between two builds¶
When a title that worked in one build misbehaves in another, find the first emulated state that differs before reading any code:
Pin the scene: Save a state just before the problem with the build that still works (
--save-state-after SECS before.clstate) and record the input that reaches it (--record-input inputs.clscript), so both sides start from the same snapshot and see the same input.Run both builds in lockstep:
copperline-ctl diverge --a ./good/copperline --b ./bad/copperline \ --until 130 --memory all --screenshots /tmp/shots -- \ --factory --config game.toml --noaudio \ --load-state before.clstate --script inputs.clscriptThe report names the first differing frame, then the first instruction inside it at which the registers, the colour clock or RAM differ, with the beam position and, for memory, the first differing byte. A DMA-only verdict (the picture differs while the CPU state never does) points at the chipset’s fetch, Copper, blitter or output path rather than the CPU core; a timing verdict (same instruction, different colour clock) points at bus arbitration or instruction timing.
Inspect the point on either side: Open the same state in the debugger window or over the control protocol,
run_until {"frame": F}to the reported frame, thenstepto the reported instruction and usecustom.writer,frame.slotsor the Frame Analyzer to see what the two builds did differently there.Bisect the change: Repeat with intermediate builds (
git bisect runaround the command; exit status 1 means diverged) until the commit that introduced the difference is found. The same command with--config-a/--config-binstead of two binaries checks whether a configuration knob changes behaviour it should not.