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
1 change: 1 addition & 0 deletions docs/debugger/headless.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ authoritative list. The most useful ones:
| `COPPERLINE_DIAG_PCSAMPLE` | Top-50 executed-PC histogram every 50 frames |
| `COPPERLINE_DIAG_PCHIST` | Full PC history (with `COPPERLINE_DIAG_PCHIST_START=SECS`) |
| `COPPERLINE_DIAG_COPLEN` | Copper list length (optionally at a given emulated time) |
| `COPPERLINE_DIAG_COP_WRITES` | Every Copper MOVE's landing colour clock (beam position, register, value), for cross-emulator write-landing comparison against vAmiga's `VAMIGA_COP_PROBE` trace |
| `COPPERLINE_DIAG_DISPLAY` | Display-register change log |
| `COPPERLINE_DIAG_CAPROW` | `=all`, `=V`, or `=START:END`: per-line bitplane capture state at DDF start, including DMACON, current and DDF-anchor BPLCON0, FMODE/DIW/DDF, effective fetch window, unit/period/quantum, words/row, modulos, and all BPLxPTs -- separates wrong-pointer from wrong-decode display bugs |
| `COPPERLINE_DIAG_PALETTE_ROW` | `=all`, `=V`, or `=START:END`: log beam-timed COLOR writes for selected beam lines, including source, framebuffer x, palette entry, LOCT, value, and BPLCON3; the setting is cached after first use |
Expand Down
34 changes: 30 additions & 4 deletions docs/internals/timing.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,24 @@ clocks and idles on the odd ones (`Copper::hpos_is_access_cycle`). So:
the alternate clocks free for the blitter/CPU. Modelling the Copper as
owning every clock while running would starve a chip-bus-bound CPU
during dense Copper effects such as horizontal colour gradients.
- WAIT and SKIP span **six** colour clocks, spending a dummy-plus-compare
- WAIT and SKIP span **eight** colour clocks, spending a two-Copper-cycle
tail after their two word fetches (the Minimig
FETCH1/FETCH2/WAITSKIP1/WAITSKIP2 sequence).
FETCH1/FETCH2/WAITSKIP1/WAITSKIP2 sequence; vAmiga COP_WAIT1/COP_WAIT2
at fetch2+2 and fetch2+4): an immediately-true WAIT or a SKIP resumes
fetching at fetch2+6.
- The custom-register side effect occurs on the second word fetch, i.e.
the third of the four colour clocks: three back-to-back MOVEs starting
at beam `hpos` write at `hpos + 2`, `hpos + 6`, and `hpos + 10`.
- The WAIT comparator's horizontal input runs **two colour clocks ahead**
of the beam (`CopperWait::comparator_is_satisfied`), wrapping through 0
over the last three clocks of a line. A sleeping WAIT therefore wakes at
target-2 -- the match colour clock is the bus-free wake-up cycle -- and
the next instruction's first fetch lands exactly on the masked target,
its write two clocks later. Cross-verified against vAmiga with the
two-sided landing probes (`COPPERLINE_DIAG_COP_WRITES` /
`VAMIGA_COP_PROBE`): a `WAIT $4721` + `MOVE SPR0CTL` lands the write at
hpos $22 in both emulators (vAmigaTS spritedma/interfere2, matching the
real-A500 photo).

Anchoring the cadence to the beam rather than to a carried-over flip-flop
is what makes a back-to-back colour MOVE list land its writes at the
Expand All @@ -178,7 +190,16 @@ check is applied by `Copper::step_eligible_slot`, the single primitive
shared by the live bus path and the blitter-deadline predictor's cloned
simulation, so prediction and execution cannot drift apart.

For the low-res renderer, a same-line `COLORxx` write at beam `hpos`
Denise applies a register write to its pixel pipeline about four colour
clocks after the chip-bus cycle (vAmiga: a one-DMA-cycle register-change
delay plus pixel-domain application offsets). Render events are recorded
at that Denise-effective position: CPU-sourced writes already carry the
offset in their landing (the known CPU write-landing class), so only
Copper-sourced writes -- whose bus landings are cycle-exact against
vAmiga -- record the `DENISE_WRITE_EFFECT_DELAY_CCK` explicitly
(`Bus::record_render_write`).

For the low-res renderer, a same-line `COLORxx` event recorded at `hpos`
starts affecting pixels at `(hpos - $35) * 4` (`COLOR_WRITE_HPOS_FB0` in
`src/video/bitplane.rs`); beam-timed placement is anchored at
`COPPER_WAIT_HPOS_FB0` ($28), and bitplane-control writes add the
Expand Down Expand Up @@ -234,7 +255,12 @@ wait for the blitter to go idle.
gets DMA slots to fetch it.
- A Copper MOVE can update COP1LC/COP2LC; a later COPJMP strobe branches
through the *current* value. A Copper MOVE to COPJMP1/COPJMP2 spends its
second word fetch on the strobe.
second word fetch on the strobe, then two more bus-free Copper cycles
(vAmiga COP_JMP1/COP_JMP2); the program counter reloads on the second of
those, so the first fetch from the new list lands three Copper cycles
after the strobe (verified against the vAmiga copper trace: a COPJMP2
MOVE at hpos $04 fetches the target list's first MOVE at $0A, its write
landing at $0C).
- The automatic frame reload latches the current COP1LC at end of frame
and restarts the Copper at the top of the next frame (vpos 0) through
the vertical-blank lines -- it branches through a Copper-programmed
Expand Down
20 changes: 18 additions & 2 deletions src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,13 @@ const DMACON_BLTEN: u16 = 1 << 6;
const DMACON_BPLEN: u16 = 1 << 8;
const DMACON_BLTPRI: u16 = 1 << 10;
const BLTCON1_DOFF: u16 = 1 << 7;
const COPPER_BUS_LOCKOUT_HPOS: u32 = 0x00E1;
// The Copper cannot take (or hold) a bus slot on the last-but-two color
// clock of the line: $E0 on short lines, $E1 on NTSC long lines (vAmiga
// busIsFree<COPPER>, keyed on the long-line flag). A fetch chain that hits
// the lockout resumes past the line wrap, which is what a line-end SKIP's
// deferred decision and the cycleE0 vAmigaTS case observe.
const COPPER_BUS_LOCKOUT_HPOS_SHORT_LINE: u32 = 0x00E0;
const COPPER_BUS_LOCKOUT_HPOS_LONG_LINE: u32 = 0x00E1;
const COPER_CPU_IRQ_DELAY_CCK: u32 = 2;
const RENDER_VISIBLE_START_VPOS: u32 = 0x2C;
const RENDER_MIN_OVERSCAN_START_VPOS: u32 = 0x1C;
Expand Down Expand Up @@ -271,6 +277,9 @@ const RENDER_DIW_HSTART_FETCH_REFERENCE_HIRES: i32 = 0x84;
const RENDER_COPPER_WAIT_HPOS_FB0: u32 = 0x28;
// Agnus DMA scheduling runs four color clocks ahead of Denise's pixel counter.
const DENISE_HPOS_LAG_CCK: u32 = 4;
// Denise applies a register write to its pixel pipeline about four colour
// clocks after the chip-bus cycle (see `record_render_write`).
const DENISE_WRITE_EFFECT_DELAY_CCK: u32 = 4;
const BPLCON0_ECSENA: u16 = 1 << 0;
const BPLCON0_SHRES: u16 = 1 << 6;
const BPLCON3_BRDSPRT: u16 = 1 << 1;
Expand Down Expand Up @@ -5095,7 +5104,14 @@ fn chip_dma_addr_mask(chip_ram_len: usize) -> u32 {
(bytes - 1) as u32
}

pub(crate) const COPPER_FRAME_START_HPOS: u32 = 6;
// The vertical-blank COP1LC strobe wakes the Copper early on the restart
// line: its first instruction-word fetch lands on the hpos $02 access cycle
// and a leading MOVE's write on $04, matching the vAmiga copper trace
// (jumpbpu image-list upload; first MOVE write at v=0 h=$04). The value was
// 6 while copper WAIT releases ran four colour clocks late; it moved in
// lockstep with the WAIT comparator lookahead fix so un-waited frame-start
// streams keep their calibrated screen positions.
pub(crate) const COPPER_FRAME_START_HPOS: u32 = 2;

fn copper_frame_start_vpos(_video_standard: VideoStandard) -> u32 {
// The Copper is restarted (COP1LC reloaded into the Copper PC) at the very
Expand Down
53 changes: 41 additions & 12 deletions src/bus/dma_slots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@

use super::*;

/// One-shot env flag for the Copper write-landing trace
/// (`COPPERLINE_DIAG_COP_WRITES=1`): logs every Copper MOVE's landing color
/// clock (beam position, register, value) to stderr for cross-emulator
/// comparison against vAmiga's `VAMIGA_COP_PROBE` trace.
fn diag_cop_writes_on() -> bool {
use std::sync::OnceLock;
static V: OnceLock<bool> = OnceLock::new();
*V.get_or_init(|| crate::envcfg::flag("COPPERLINE_DIAG_COP_WRITES"))
}

impl Bus {
pub(super) fn advance_one_chip_bus_quantum(
&mut self,
Expand Down Expand Up @@ -199,6 +209,12 @@ impl Bus {
CopperSlotAction::Idle => false,
CopperSlotAction::BusUsed => true,
CopperSlotAction::Move { register, value } => {
if diag_cop_writes_on() {
eprintln!(
"COPPROBE MOVE v={:03x} h={:02x} reg={:03x} val={:04x}",
vpos, hpos, register, value
);
}
if self.copper_can_write_custom(register) {
let _ = self.write_custom_word_from(register, value, BeamWriteSource::Copper);
} else {
Expand Down Expand Up @@ -503,7 +519,7 @@ impl Bus {
if quantum >= CHIP_BUS_SLOT_CCK
&& pending_copper_frame_start.is_none()
&& self.copper_dma_enabled()
&& hpos != COPPER_BUS_LOCKOUT_HPOS
&& !self.copper_bus_lockout_active_at(hpos)
{
let _ = copper.step_eligible_slot(
&self.mem.chip_ram,
Expand All @@ -521,7 +537,7 @@ impl Bus {
true
} else if !self.copper_dma_enabled() {
false
} else if hpos == COPPER_BUS_LOCKOUT_HPOS {
} else if self.copper_bus_lockout_active_at(hpos) {
copper.is_running()
} else {
!matches!(
Expand Down Expand Up @@ -967,14 +983,22 @@ impl Bus {
}

pub(super) fn copper_bus_lockout_active_at(&self, hpos: u32) -> bool {
hpos == COPPER_BUS_LOCKOUT_HPOS
hpos == self.copper_bus_lockout_hpos()
}

pub(super) fn copper_bus_lockout_hpos(&self) -> u32 {
if self.agnus.lol {
COPPER_BUS_LOCKOUT_HPOS_LONG_LINE
} else {
COPPER_BUS_LOCKOUT_HPOS_SHORT_LINE
}
}

pub(super) fn cck_until_copper_wait_position(&self, wait: CopperWait) -> Option<u32> {
if wait.is_end_of_list() {
return None;
}
if wait.is_satisfied(self.agnus.vpos, self.agnus.hpos) {
if wait.comparator_is_satisfied(self.agnus.vpos, self.agnus.hpos) {
return Some(0);
}

Expand All @@ -996,15 +1020,20 @@ impl Bus {
vpos = 0;
}
}
if wait.is_satisfied(vpos, hpos) {
if wait.comparator_is_satisfied(vpos, hpos) {
return Some(delta);
}
}
None
}

pub(super) fn cck_until_full_mask_copper_wait(&self, wait: CopperWait) -> Option<u32> {
// The comparator's horizontal input runs two color clocks ahead of
// the beam, so a sleeping full-mask wait releases two color clocks
// before its masked horizontal target (see
// `CopperWait::comparator_is_satisfied`).
let target_h = (wait.position_bits() & 0x00FE) as u32;
let release_h = target_h.saturating_sub(2);
let frame_lines = self.agnus.current_frame_lines();

for line_delta in 0..=frame_lines {
Expand All @@ -1017,16 +1046,16 @@ impl Bus {
let target_line_cck = self.line_cck_after_lines(line_delta);

if line_delta == 0 {
if target_h < target_line_cck
&& self.agnus.hpos <= target_h
&& wait.is_satisfied(vpos, target_h)
if release_h < target_line_cck
&& self.agnus.hpos <= release_h
&& wait.comparator_is_satisfied(vpos, release_h)
{
return Some(target_h - self.agnus.hpos);
return Some(release_h - self.agnus.hpos);
}
} else if wait.is_satisfied(vpos, 0) {
} else if wait.comparator_is_satisfied(vpos, 0) {
return Some(line_start_delta);
} else if target_h < target_line_cck && wait.is_satisfied(vpos, target_h) {
return Some(line_start_delta + target_h);
} else if release_h < target_line_cck && wait.comparator_is_satisfied(vpos, release_h) {
return Some(line_start_delta + release_h);
}
}

Expand Down
26 changes: 25 additions & 1 deletion src/bus/frame_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1301,7 +1301,31 @@ impl Bus {
}

pub(super) fn record_render_write(&mut self, offset: u16, value: u16, source: BeamWriteSource) {
let (vpos, hpos) = (self.agnus.vpos, self.agnus.hpos);
let (mut vpos, mut hpos) = (self.agnus.vpos, self.agnus.hpos);
// Denise applies a register write to its pixel pipeline about four
// colour clocks after the chip-bus cycle (vAmiga models this as a
// one-DMA-cycle register-change delay plus pixel-domain application
// offsets inside Denise). The render-side anchors
// (COLOR_WRITE_HPOS_FB0, COPPER_WAIT_HPOS_FB0, the sprite write
// pipeline) are photo-calibrated against events recorded at that
// effective position. CPU-sourced writes already carry the offset in
// their landing (the known CPU write-landing class), so only
// Copper-sourced writes -- whose bus landings are cycle-exact against
// vAmiga since the WAIT comparator lookahead fix -- record the delay
// explicitly.
// TODO: model the CPU write landing exactly and make this effect
// delay source-independent.
if matches!(source, BeamWriteSource::Copper) {
hpos += DENISE_WRITE_EFFECT_DELAY_CCK;
let line_cck = self.agnus.current_line_cck();
if hpos >= line_cck {
hpos -= line_cck;
vpos += 1;
if vpos >= self.agnus.current_frame_lines() {
vpos = 0;
}
}
}
let event = BeamRegisterWrite {
vpos,
hpos,
Expand Down
Loading
Loading