Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ against real hardware.
(ADF / ADZ / ZIP / DMS, read-only SCP), Gayle IDE, A2091 SCSI, and CDTV/CD32
CD.
- **Tooling**: an in-window debugger that can step backwards, an
interactive chip-bus frame analyzer, remote GDB support, deterministic
save states, input recording/replay, and headless screenshot/frame-dump
capture -- the deterministic core makes every replay byte-identical.
interactive chip-bus frame analyzer, a trigger-based VCD waveform export
of the chipset signals for GTKWave (`docs/debugger/waveform.md`), remote
GDB support, deterministic save states, input recording/replay, and
headless screenshot/frame-dump capture -- the deterministic core makes
every replay byte-identical.
- **A browser build**: the same core compiled to WebAssembly with a
canvas/Web Audio frontend, hosted at
[copperline.dev/try](https://copperline.dev/try/) -- boots the bundled
Expand Down
2 changes: 2 additions & 0 deletions docs/debugger/console.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ Inspection and modification:
| `SETREG REG VAL` | Set a CPU register (`SETREG D0 1234`) |
| `TRACE START [PATH]` | Start a runtime instruction trace: one disassembled line per retired instruction with its beam position, no env var or restart needed (capped at a million lines) |
| `TRACE STOP` / `TRACE` | Stop the trace / report its progress |
| `WAVE START [PATH] [TRIGGER] [DURATION] [SIGNALS]` | Arm a trigger-based VCD "logic analyser" capture of chipset signals for GTKWave; the arguments are order-free and all optional (see [](waveform.md)) |
| `WAVE STOP` / `WAVE` | Finish the capture early / report its progress |
| `HELP`, `CLEAR`, `CLOSE` | Console housekeeping |

Memory hunting (a trainer-style delta search over all writable RAM --
Expand Down
111 changes: 111 additions & 0 deletions docs/debugger/waveform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Waveform export (VCD logic analyser)

Copperline can record its internal chipset signals -- beam counters, chip-bus
owner, CPU bus accesses, Copper and blitter state, custom-register writes,
interrupt levels, DMA activity -- into a [VCD](https://en.wikipedia.org/wiki/Value_change_dump)
file that [GTKWave](https://gtkwave.sourceforge.net/) (or any VCD viewer)
displays as a logic-analyser trace. Because the emulator arbitrates the chip
bus per colour clock, the trace shows exactly which device owned every bus
slot and how the Copper, blitter, CPU, and DMA interleave -- the view a logic
analyser probing a real Amiga's bus would give, plus internal state no probe
could reach.

A capture is *trigger-based* and *bounded*: it arms, waits for a trigger (a
CPU PC, a beam position, a register write, an emulated time, or immediately),
records for a fixed window, then finishes the file. This keeps files small
and puts the interesting event at the start of the trace.

## Starting a capture

From the command line (works headless, e.g. together with
`--screenshot-after`):

```sh
copperline --config game.toml --noaudio \
--waveform out.vcd \
--wave-trigger pc=0x00C033C2 \
--wave-duration 20000cck \
--wave-signals cpu,bus,copper,blitter \
--screenshot-after 30 /tmp/shot.png
```

From the debugger console (`Cmd/Alt+K`), with the same order-free arguments:

```
WAVE START out.vcd pc=C033C2 20000cck cpu,bus,copper,blitter
WAVE (status)
WAVE STOP (finish early)
```

From the debugger window (`Cmd/Alt+B`): the **Wave** tab has Arm and Stop
buttons; type the same order-free spec into the entry box (empty means all
defaults) and click Arm.

Everything is optional: the default trigger is `now`, the default duration
one video frame, the default signal set `all`, and an omitted path becomes
`copperline-wave-<timestamp>.vcd` in the working directory.

## Triggers

| Spec | Fires when |
|---|---|
| `now` | immediately on arming (default) |
| `pc=ADDR` | the CPU retires the instruction at hex ADDR |
| `beam=VPOS` / `beam=VPOS:HPOS` | the beam crosses the (decimal) position |
| `reg=OFF` | a custom register is written (hex word offset, e.g. `reg=180` for COLOR00) |
| `time=SECS` | emulated time reaches SECS (fractional ok) |

## Durations

`20000cck` (colour clocks; a bare number means cck), `2f` / `2frames`,
`50ms`, `1.5s`. The default is one frame. A safety cap bounds any capture at
10 emulated seconds, and an emergency stop finishes the file if it passes
512 MB.

## Signal groups

Select with `--wave-signals` (comma list) or the same list as a console/GUI
token. Default: `all`.

| Group | VCD variables |
|---|---|
| `beam` | `vpos[15:0]`, `hpos[7:0]`, `frame[31:0]` |
| `bus` | `owner[3:0]`, `owner_name` (string), `dmacon[15:0]`, `data[15:0]` (the shared chip data bus latch) |
| `cpu` | `addr[23:0]`, `kind` (fetch/read/write/custom), `rw`, `wait_cck[15:0]` -- one record per granted CPU **chip-bus** slot (fast-RAM traffic never touches the chip bus) |
| `copper` | `pc[23:0]`, `state` (run/wait/skip/jump/stop) |
| `blitter` | `busy`, `slot` (the pipeline cycle label: A/B/C/D, line-mode L1..L4, fill FI, ...), `apt/bpt/cpt/dpt[23:0]` |
| `regs` | `off[8:0]`, `value[15:0]`, `source` (cpu/copper), `strobe` (toggles per write so identical back-to-back writes stay visible) |
| `irq` | `ipl[2:0]` (the level presented to the CPU, after INTEN masking), `intreq[15:0]`, `intena[15:0]` |
| `audio` | `channel[1:0]` + `strobe` per audio DMA grant |

`owner` uses the bus-accounting indices (0 refresh, 1 bitplane, 2 sprite,
3 disk, 4 audio, 5 copper, 6 blitter, 7 cpu, 8 idle); `owner_name` carries
the same information as text, which GTKWave shows directly on the wave.

## Time base

One VCD time unit is one colour clock, with timestamps relative to the
trigger. The file declares `$timescale 1 us` -- the closest legal VCD unit --
so cursor deltas in GTKWave read directly as colour clocks (a PAL line is
227, a PAL frame 71,364). Signals are sampled at the chip-bus arbitration
point, so a value change lands on the exact colour clock it took effect.

## Viewing

```sh
gtkwave out.vcd # macOS: brew install gtkwave
vcd2fst out.vcd out.fst # optional: GTKWave's compact FST format
```

In GTKWave, append the signals you care about from the `copperline` scope
tree, set `owner_name`/`state`/`slot` to ASCII display, and use marker
deltas to measure in colour clocks.

## Notes

- A capture observes the machine without disturbing it: no timing changes,
and the hot path costs a single branch while nothing is armed.
- Arming replaces (and finishes) any previous capture; captures do not
survive save-state loads.
- The `pc=` trigger matches retired instructions, so it works with every
CPU model including prefetch/cache effects.
9 changes: 9 additions & 0 deletions docs/debugger/window.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,15 @@ The Break tab with a PC breakpoint, a memory watchpoint, and a
chipset-register watch armed.
```

**Wave** arms and monitors the VCD "logic analyser" export
([](waveform.md)): **Arm** starts a capture from the order-free spec in the
`$` box -- any of an output path, a trigger (`PC=C033C2`, `BEAM=100:64`,
`REG=180`, `TIME=2.5`, `NOW`), a duration (`20000CCK`, `2F`, `50MS`), and a
signal-group list (`CPU,BUS,COPPER`); an empty box means all defaults.
**Stop** finishes the file early. The tab shows the capture's state
(armed / capturing with progress / done), its trigger, and where the file
is going. The console `WAVE` command drives the same capture.

## Breakpoints and watchpoints

On the Break tab, type an address into the `$` box and toggle any of:
Expand Down
19 changes: 19 additions & 0 deletions docs/debugger/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ symbols, while source lines, `next`, and `print` need an
ELF-with-DWARF sibling of the binary. The walkthrough lives in the
[GDB chapter](gdb), "Source-Level Debugging of Amiga Programs".

## See the bus like a logic analyser

When the question is *interleaving* -- who owned which DMA slot, when the
blitter stalled the CPU, where the Copper woke relative to the display
fetch -- text traces are the wrong shape. Capture a waveform instead:

```
WAVE START glitch.vcd beam=100 2f
```

arms a capture that triggers when the beam reaches line 100 and records two
frames of every signal group (bus owner per colour clock, Copper PC/state,
blitter pipeline slots and pointers, register writes, IPL/INTREQ/INTENA,
CPU chip-bus accesses). Open `glitch.vcd` in GTKWave and read the bus
arbitration directly off the screen; marker deltas measure in colour
clocks. Headless runs arm the same capture with
`--waveform glitch.vcd --wave-trigger beam=100 --wave-duration 2f`.
[](waveform.md) has the full trigger/duration/signal reference.

## Making it reproducible

Any of the above is dramatically easier when the failure replays
Expand Down
5 changes: 5 additions & 0 deletions docs/guide/headless.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ COPPERLINE_DBG_BREAK=C033C2 COPPERLINE_DBG_DUMP=C09580:4 \
--screenshot-after 30 /tmp/out.png
```

For chip-bus timing questions, the [waveform export](../debugger/waveform)
records a trigger-based VCD trace of the internal chipset signals during
the same kind of run (`--waveform out.vcd --wave-trigger pc=0xC033C2
--wave-duration 20000cck`) for viewing in GTKWave.

## The vAmigaTS compatibility suite

An ignored integration test runs ADFs from a local
Expand Down
2 changes: 2 additions & 0 deletions docs/myst.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ project:
- file: debugger/console.md
- file: debugger/headless.md
- file: debugger/reverse.md
- file: debugger/waveform.md
- file: debugger/gdb.md
- file: debugger/workflows.md
- title: Internals
Expand Down Expand Up @@ -57,6 +58,7 @@ project:
- debugger/console.md
- debugger/headless.md
- debugger/reverse.md
- debugger/waveform.md
- debugger/gdb.md
- debugger/workflows.md
- internals/architecture.md
Expand Down
17 changes: 17 additions & 0 deletions src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,16 @@ pub struct Bus {
current_frame_bus_trace: FrameBusTrace,
#[serde(skip)]
last_frame_bus_trace: Option<FrameBusTrace>,
/// Waveform (VCD) capture: `wave_on` is the single hot-path gate for
/// every sampling tap (true while armed or capturing), `wave_pc_trigger`
/// the per-instruction gate for the `pc=` trigger. Host-side observer
/// state, never serialized.
#[serde(skip)]
wave_on: bool,
#[serde(skip)]
pub(crate) wave_pc_trigger: bool,
#[serde(skip)]
wave: Option<Box<crate::waveform::WaveCapture>>,
/// Debugger-window custom-register watch offsets ($000-$1FE, word
/// aligned), mirrored from the CPU machine's InteractiveBreaks, and
/// the first pending hit since the debugger last polled. Recorded in
Expand Down Expand Up @@ -2216,6 +2226,9 @@ impl Bus {
frame_analyzer_enabled: false,
current_frame_bus_trace: FrameBusTrace::default(),
last_frame_bus_trace: None,
wave_on: false,
wave_pc_trigger: false,
wave: None,
ui_reg_watches: Vec::new(),
ui_reg_hit: None,
ui_beam_traps: Vec::new(),
Expand Down Expand Up @@ -3597,6 +3610,9 @@ impl Bus {
// A long-word access stores its second (low-word) slot.
self.cpu_custom_access_slot = Some(grant_slot);
}
if self.wave_on {
self.wave_note_cpu_access(addr, kind, wait_cck);
}
let (cck, tick) = self.advance_one_chip_bus_quantum(Some(ChipBusOwner::Cpu));
self.note_cpu_granted_chip_bus_cycle();
self.record_slice_bus_advance(cck, tick);
Expand Down Expand Up @@ -7446,6 +7462,7 @@ mod custom_regs;
mod ddf_line;
mod dma_slots;
mod frame_capture;
mod wave;

#[cfg(test)]
mod tests;
3 changes: 3 additions & 0 deletions src/bus/custom_regs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ impl Bus {
hpos: self.agnus.hpos as u16,
});
}
if self.wave_on {
self.wave_note_reg_write(off, val, source);
}
if is_audio_timing_custom_write(off) {
self.flush_audio();
}
Expand Down
6 changes: 6 additions & 0 deletions src/bus/dma_slots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ impl Bus {
self.blitter.busy,
);
}
if self.wave_on {
self.wave_tap_quantum(owner);
}
// The Copper was already stepped above (or is held without fetching at
// the end-of-line lockout); only drive the other owners here.
if !matches!(owner, ChipBusOwner::Copper) {
Expand Down Expand Up @@ -282,6 +285,9 @@ impl Bus {
if !self.ui_beam_traps.is_empty() {
self.check_ui_beam_traps((old_vpos, old_hpos), old_frame_lines, tick.new_frames);
}
if self.wave_on {
self.wave_note_beam((old_vpos, old_hpos), old_frame_lines, tick.new_frames);
}
if tick.new_frames == 0 && tick.new_lines == 0 {
self.capture_sprite_dma_words_if_due(
old_vpos,
Expand Down
Loading
Loading