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
34 changes: 26 additions & 8 deletions docs/internals/timing.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,32 @@ 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.

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`).
Register writes take effect a fixed number of colour clocks after the
chip-bus slot that carried them, and the delay is a property of the
register pipeline, not of the bus master. Denise-boundary registers apply
to the pixel pipeline about four colour clocks after the slot
(`DENISE_WRITE_EFFECT_DELAY_CCK`; vAmiga: a register-change delay plus
pixel-domain application offsets). Agnus's two-cycle register class
(DMACON, BPLxPT, BPLxMOD, SPRxPT; vAmiga `recordRegisterChange(DMA_CYCLES(2))`)
applies two colour clocks after the slot (`AGNUS_WRITE_EFFECT_DELAY_CCK`);
the bitplane/sprite DMA-gating replay is calibrated against events
recorded at that position. Render events are recorded at the effective
position for *every* writer: a Copper MOVE executes at its bus slot, so
its event records at the current beam position plus the delay; a CPU
write is applied once its whole bus cycle has been billed (the beam sits
past the granted slot by then), so its event is referenced from the
granted slot itself (`Bus::cpu_custom_access_slot`) plus the same delay
(`Bus::record_render_write`). Copper-sourced events currently record the
Denise delay for the Agnus two-cycle class as well -- the copper-driven
DMA-gating replay was calibrated with that offset when the copper
landings became bus-exact (a documented TODO). The Denise delay was
verified two-sided with the `COPPERLINE_DIAG_CPU_WRITES` /
`VAMIGA_CPU_PROBE` landing traces on the vAmigaTS DMACON sprena dense CPU
`COLOR00` stream: with landings matched line-for-line, the rendered rows
sat exactly two colour clocks left of vAmiga until the CPU side carried
the same slot-referenced delay; the Agnus delay is pinned by the DMACON
bplon bitplane-gating bars, which sit 8 px right of vAmiga when those
events carry the Denise delay instead.

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
Expand Down
65 changes: 64 additions & 1 deletion src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,13 @@ 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`).
// clocks after the chip-bus slot that carried it, regardless of whether the
// Copper or the CPU drove the bus (see `record_render_write`).
const DENISE_WRITE_EFFECT_DELAY_CCK: u32 = 4;
// Agnus applies its two-cycle register class (DMACON, BPLxPT, BPLxMOD,
// SPRxPT; vAmiga `DMA_CYCLES(2)` in `recordRegisterChange`) two colour
// clocks after the chip-bus slot that carried the write.
const AGNUS_WRITE_EFFECT_DELAY_CCK: u32 = 2;
const BPLCON0_ECSENA: u16 = 1 << 0;
const BPLCON0_SHRES: u16 = 1 << 6;
const BPLCON3_BRDSPRT: u16 = 1 << 1;
Expand Down Expand Up @@ -871,6 +876,17 @@ pub struct Bus {
/// here so the fractional cck are not lost.
#[serde(skip)]
cpu_bus_tail_carry: u32,
/// Chip-bus slot of the most recently granted CPU access to custom
/// register space. A CPU register write is applied to chip state once
/// its whole bus cycle has been billed (the beam sits past the granted
/// slot by then), but its Denise/Agnus-effective position is referenced
/// from the slot that carried the write: `record_render_write` places
/// CPU-sourced render events at slot + `DENISE_WRITE_EFFECT_DELAY_CCK`,
/// the same write-to-effect pipeline the Copper's slot-exact writes
/// take. Also feeds the `COPPERLINE_DIAG_CPU_WRITES` landing trace.
/// Transient bus-cycle state, rebuilt every access; not serialized.
#[serde(skip)]
cpu_custom_access_slot: Option<(u32, u32)>,
dbg_bpl_cck: Vec<u32>,
dbg_slotmap: Vec<Vec<u8>>,
dbg_slotmap_on: bool,
Expand Down Expand Up @@ -1027,6 +1043,14 @@ pub enum BeamWriteSource {
Copper,
}

/// One-shot env flag for the CPU write-landing trace
/// (`COPPERLINE_DIAG_CPU_WRITES=1`); see [`Bus::diag_cpu_write`].
fn diag_cpu_writes_on() -> bool {
use std::sync::OnceLock;
static V: OnceLock<bool> = OnceLock::new();
*V.get_or_init(|| crate::envcfg::flag("COPPERLINE_DIAG_CPU_WRITES"))
}

fn beam_write_source_name(source: BeamWriteSource) -> &'static str {
match source {
BeamWriteSource::Cpu => "cpu",
Expand Down Expand Up @@ -2053,6 +2077,7 @@ impl Bus {
ext_clock_carry_x100: 0,
cpu_short_bus_cycle: false,
cpu_bus_tail_carry: 0,
cpu_custom_access_slot: None,
cpu_granted_chip_slots: 0,
cpu_missed_chip_slots: 0,
dbg_bpl_cck: vec![0; 340],
Expand Down Expand Up @@ -3381,6 +3406,14 @@ impl Bus {
self.record_slice_bus_advance(cck, tick);
self.flush_audio_before_audio_dma_slot();
}
if matches!(kind, CpuBusAccessKind::Custom) {
// Remember the slot that carries a custom-register access:
// a register write's Denise/Agnus-effective position is
// referenced from this slot (see `record_render_write`),
// not from the beam position after the bus cycle's tail.
// A long-word access stores its second (low-word) slot.
self.cpu_custom_access_slot = Some((self.agnus.vpos, self.agnus.hpos));
}
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 @@ -4349,6 +4382,26 @@ impl Bus {
}
}

/// One-shot env flag for the CPU write-landing trace
/// (`COPPERLINE_DIAG_CPU_WRITES=1`): logs every CPU custom-register
/// write's granted chip-bus slot (and the beam position the write's
/// effect applies at) to stderr, for cross-emulator comparison against
/// vAmiga's `VAMIGA_CPU_PROBE` trace.
fn diag_cpu_write(&self, off: u16, word: u16) {
let (v, h) = self
.cpu_custom_access_slot
.unwrap_or((self.agnus.vpos, self.agnus.hpos));
eprintln!(
"CPUPROBE POKE v={:03x} h={:02x} reg={:03x} val={:04x} ev={:03x} eh={:02x}",
v,
h,
off & 0x1FE,
word,
self.agnus.vpos,
self.agnus.hpos
);
}

/// Returns true if the write set a new INTREQ bit and the caller
/// should preempt the slice so the freshly-asserted IRQ can be
/// delivered before agnus has a chance to OR in VERTB.
Expand All @@ -4374,6 +4427,9 @@ impl Bus {
let b = (val & 0xFF) as u16;
let word = (b << 8) | b;
trace!("custom W8 off={:03X} val={:02X}", off, b);
if diag_cpu_writes_on() {
self.diag_cpu_write(off & 0xFFE, word);
}
self.write_custom_word_from(off & 0xFFE, word, BeamWriteSource::Cpu)
}
4 => {
Expand All @@ -4384,13 +4440,20 @@ impl Bus {
let hi = ((val >> 16) & 0xFFFF) as u16;
let lo = (val & 0xFFFF) as u16;
trace!("custom W32 off={:03X} val={:08X}", off, val);
if diag_cpu_writes_on() {
self.diag_cpu_write(off, hi);
self.diag_cpu_write(off.wrapping_add(2), lo);
}
let p1 = self.write_custom_word_from(off, hi, BeamWriteSource::Cpu);
let p2 = self.write_custom_word_from(off.wrapping_add(2), lo, BeamWriteSource::Cpu);
p1 || p2
}
_ => {
let word = (val & 0xFFFF) as u16;
trace!("custom W16 off={:03X} val={:04X}", off, word);
if diag_cpu_writes_on() {
self.diag_cpu_write(off, word);
}
self.write_custom_word_from(off, word, BeamWriteSource::Cpu)
}
}
Expand Down
86 changes: 63 additions & 23 deletions src/bus/frame_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1301,29 +1301,69 @@ impl Bus {
}

pub(super) fn record_render_write(&mut self, offset: u16, value: u16, source: BeamWriteSource) {
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;
}
// Register writes take effect a fixed number of colour clocks after
// the chip-bus slot that carried them, and the delay is a property
// of the register pipeline, not of the writer:
//
// - Denise-boundary registers apply to the pixel pipeline about
// four colour clocks after the slot (DENISE_WRITE_EFFECT_DELAY_
// CCK; vAmiga models this as a register-change delay plus
// pixel-domain application offsets inside Denise). Verified
// two-sided against vAmiga's VAMIGA_CPU_PROBE landing trace:
// landing-matched dense CPU COLOR00 lines rendered exactly two
// colour clocks left of vAmiga until the CPU side carried the
// same slot-referenced delay the Copper side already had.
// - Agnus's two-cycle register class (DMACON, BPLxPT, BPLxMOD,
// SPRxPT) applies two colour clocks after the slot (AGNUS_WRITE_
// EFFECT_DELAY_CCK; vAmiga `recordRegisterChange(DMA_CYCLES(2))`).
// The bitplane/sprite DMA-gating replay is calibrated against
// events recorded at that position (vAmigaTS DMACON bplon bars
// sit 8 px right of vAmiga when these events carry the Denise
// delay instead).
//
// The render-side anchors (COLOR_WRITE_HPOS_FB0, COPPER_WAIT_HPOS_
// FB0, the sprite write pipeline) are photo-calibrated against
// events recorded at the Denise-effective position. A Copper MOVE
// executes at its bus slot (the current beam position); a CPU write
// is applied once its whole bus cycle has been billed, past its
// granted slot, so the slot is taken from `cpu_custom_access_slot`
// (a direct call without a granted slot treats the current beam
// position as the slot).
//
// Copper-sourced events currently record the Denise delay for the
// Agnus two-cycle class as well: the DMA-gating replay of copper
// writes was calibrated with that offset when the copper landings
// became bus-exact. TODO: model the Agnus boundary for copper
// writes too and recalibrate the copper-driven DMA-gating replay.
let agnus_two_cycle = matches!(
offset & 0x01FE,
0x096 | 0x0E0..=0x0FE | 0x108 | 0x10A | 0x120..=0x13E
);
let (mut vpos, mut hpos, delay) = match source {
BeamWriteSource::Copper => (
self.agnus.vpos,
self.agnus.hpos,
DENISE_WRITE_EFFECT_DELAY_CCK,
),
BeamWriteSource::Cpu | BeamWriteSource::CpuCopperIrq => {
let (v, h) = self
.cpu_custom_access_slot
.unwrap_or((self.agnus.vpos, self.agnus.hpos));
let delay = if agnus_two_cycle {
AGNUS_WRITE_EFFECT_DELAY_CCK
} else {
DENISE_WRITE_EFFECT_DELAY_CCK
};
(v, h, delay)
}
};
hpos += delay;
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 {
Expand Down
Loading
Loading