diff --git a/docs/internals/timing.md b/docs/internals/timing.md index d8a3c7e..d54a78b 100644 --- a/docs/internals/timing.md +++ b/docs/internals/timing.md @@ -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 diff --git a/src/bus.rs b/src/bus.rs index b185a55..de2359b 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -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; @@ -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, dbg_slotmap: Vec>, dbg_slotmap_on: bool, @@ -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 = 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", @@ -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], @@ -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); @@ -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. @@ -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 => { @@ -4384,6 +4440,10 @@ 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 @@ -4391,6 +4451,9 @@ impl Bus { _ => { 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) } } diff --git a/src/bus/frame_capture.rs b/src/bus/frame_capture.rs index bea6137..d0d9f77 100644 --- a/src/bus/frame_capture.rs +++ b/src/bus/frame_capture.rs @@ -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 { diff --git a/src/bus/tests.rs b/src/bus/tests.rs index 72815b5..f87da27 100644 --- a/src/bus/tests.rs +++ b/src/bus/tests.rs @@ -2731,9 +2731,13 @@ fn cpu_copper_irq_render_event_uses_actual_write_beam() { bus.agnus.hpos = 0x40; assert!(!bus.custom_write(0x180, 2, 0x0222)); + // The render event records at the write's chip-bus slot plus the + // Denise write-effect delay (source-independent, see + // record_render_write); the beam-retargeted bottom-palette twin keeps + // the delivered-IRQ beam verbatim. let render_event = bus.frame_render_events().last().unwrap(); assert_eq!(render_event.vpos, 0xFA); - assert_eq!(render_event.hpos, 0x40); + assert_eq!(render_event.hpos, 0x44); assert_eq!(bus.pending_beam_bottom_palette_events[0].vpos, 0xD4); assert_eq!(bus.pending_beam_bottom_palette_events[0].hpos, 0x16); } @@ -6494,7 +6498,10 @@ fn manual_sprite_data_writes_accumulate_live_sprite_sprite_clxdat() { let mut bus = empty_bus(); let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0083); bus.agnus.vpos = 0x2C; - bus.agnus.hpos = 0x38; + // The staged beam position is the write's chip-bus slot; the collision + // event records at slot + DENISE_WRITE_EFFECT_DELAY_CCK ($38), and the + // advance below runs the beam past it. + bus.agnus.hpos = 0x34; bus.denise.diwstrt = 0x2C83; bus.denise.diwstop = 0x2DC1; bus.denise.sprpos[0] = pos; @@ -6506,7 +6513,7 @@ fn manual_sprite_data_writes_accumulate_live_sprite_sprite_clxdat() { bus.write_custom_word_from(0x144, 0x8000, BeamWriteSource::Cpu); bus.write_custom_word_from(0x154, 0x8000, BeamWriteSource::Cpu); - bus.advance_chipset(2); + bus.advance_chipset(6); assert_eq!(bus.custom_read(0x00E, 2), 0x8200); assert_eq!(bus.custom_read(0x00E, 2), 0x8000); @@ -6517,7 +6524,8 @@ fn frame_end_completes_unread_live_sprite_sprite_clxdat() { let mut bus = empty_bus(); let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0083); bus.agnus.vpos = 0x2C; - bus.agnus.hpos = 0x38; + // Slot $34: the write events record at $38 (slot + write-effect delay). + bus.agnus.hpos = 0x34; bus.denise.diwstrt = 0x2C83; bus.denise.diwstop = 0x2DC1; bus.denise.sprpos[0] = pos; @@ -6540,7 +6548,8 @@ fn attached_manual_sprite_data_writes_accumulate_live_sprite_sprite_clxdat() { let mut bus = empty_bus(); let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0083); bus.agnus.vpos = 0x2C; - bus.agnus.hpos = 0x38; + // Slot $34: the write events record at $38 (slot + write-effect delay). + bus.agnus.hpos = 0x34; bus.denise.diwstrt = 0x2C83; bus.denise.diwstop = 0x2DC1; bus.denise.sprpos[0] = pos; @@ -6556,7 +6565,7 @@ fn attached_manual_sprite_data_writes_accumulate_live_sprite_sprite_clxdat() { bus.write_custom_word_from(0x144, 0x0000, BeamWriteSource::Cpu); bus.write_custom_word_from(0x14C, 0x8000, BeamWriteSource::Cpu); bus.write_custom_word_from(0x154, 0x8000, BeamWriteSource::Cpu); - bus.advance_chipset(2); + bus.advance_chipset(6); assert_eq!(bus.custom_read(0x00E, 2), 0x8200); assert_eq!(bus.custom_read(0x00E, 2), 0x8000); @@ -6565,12 +6574,14 @@ fn attached_manual_sprite_data_writes_accumulate_live_sprite_sprite_clxdat() { #[test] fn attached_manual_sprite_odd_data_writes_accumulate_later_live_sprite_sprite_clxdat() { let mut bus = empty_bus(); - // hstart +1 vs the pre-fix value: sprite comparator positions share - // Denise's counter and moved with the corrected window-edge anchor - // (2H-196); the beam-anchored playfield sample did not. - let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0084); + // The sprites sit at hstart $8C so the overlap pixels land exactly at + // the second odd-DATA write's effective position (slot $3A plus the + // write-effect delay = event $3E, write-domain x 88): pixels before it + // replay the first odd word, pixels from it replay the rewrite. + let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x008C); bus.agnus.vpos = 0x2C; - bus.agnus.hpos = 0x38; + // Slot $34: the write events record at $38 (slot + write-effect delay). + bus.agnus.hpos = 0x34; bus.denise.diwstrt = 0x2C83; bus.denise.diwstop = 0x2DC1; bus.denise.sprpos[0] = pos; @@ -6586,11 +6597,11 @@ fn attached_manual_sprite_odd_data_writes_accumulate_later_live_sprite_sprite_cl bus.write_custom_word_from(0x144, 0x0000, BeamWriteSource::Cpu); bus.write_custom_word_from(0x14C, 0x8000, BeamWriteSource::Cpu); bus.write_custom_word_from(0x154, 0x2000, BeamWriteSource::Cpu); - bus.advance_chipset(2); + bus.advance_chipset(6); assert_eq!(bus.custom_read(0x00E, 2), 0x8000); bus.write_custom_word_from(0x14C, 0x2000, BeamWriteSource::Cpu); - bus.advance_chipset(1); + bus.advance_chipset(9); assert_eq!(bus.custom_read(0x00E, 2), 0x8200); assert_eq!(bus.custom_read(0x00E, 2), 0x8000); @@ -6699,7 +6710,8 @@ fn manual_sprite_data_writes_accumulate_live_sprite_playfield_clxdat() { // (2H-196); the beam-anchored playfield sample did not. let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0082); bus.agnus.vpos = 0x2C; - bus.agnus.hpos = 0x38; + // Slot $34: the write event records at $38 (slot + write-effect delay). + bus.agnus.hpos = 0x34; bus.denise.diwstrt = 0x2C81; bus.denise.diwstop = 0x2DC1; bus.denise.ddfstrt = 0x0038; @@ -6730,7 +6742,7 @@ fn manual_sprite_data_writes_accumulate_live_sprite_playfield_clxdat() { }); bus.write_custom_word_from(0x144, 0x8000, BeamWriteSource::Cpu); - bus.advance_chipset(2); + bus.advance_chipset(6); assert_eq!(bus.custom_read(0x00E, 2), 0x8022); assert_eq!(bus.custom_read(0x00E, 2), 0x8000); @@ -6741,7 +6753,8 @@ fn attached_manual_sprite_data_writes_accumulate_live_sprite_playfield_clxdat() let mut bus = empty_bus(); let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0083); bus.agnus.vpos = 0x2C; - bus.agnus.hpos = 0x38; + // Slot $34: the write events record at $38 (slot + write-effect delay). + bus.agnus.hpos = 0x34; bus.denise.diwstrt = 0x2C83; bus.denise.diwstop = 0x2DC1; bus.denise.ddfstrt = 0x0038; @@ -6772,7 +6785,7 @@ fn attached_manual_sprite_data_writes_accumulate_live_sprite_playfield_clxdat() bus.write_custom_word_from(0x144, 0x0000, BeamWriteSource::Cpu); bus.write_custom_word_from(0x14C, 0x8000, BeamWriteSource::Cpu); - bus.advance_chipset(2); + bus.advance_chipset(6); assert_eq!(bus.custom_read(0x00E, 2), 0x8022); assert_eq!(bus.custom_read(0x00E, 2), 0x8000); @@ -6873,9 +6886,13 @@ fn same_line_clxcon_odd_sprite_enable_does_not_retime_earlier_live_sprite_sprite bus.custom_read(0x00E, 2) }; + // The CLXCON events record at the write slot + the write-effect delay: + // slot $34 -> event $38 (before the sprite pixels), slot $36 -> event + // $3A (after them) -- the same recorded boundary the pre-delay test + // staged directly at $38/$3A. assert_eq!(clxdat_after_visible_sprite_pixels(1 << 12, None), 0x8200); - assert_eq!(clxdat_after_visible_sprite_pixels(0, Some(0x38)), 0x8200); - assert_eq!(clxdat_after_visible_sprite_pixels(0, Some(0x3A)), 0x8000); + assert_eq!(clxdat_after_visible_sprite_pixels(0, Some(0x34)), 0x8200); + assert_eq!(clxdat_after_visible_sprite_pixels(0, Some(0x36)), 0x8000); } #[test] @@ -7483,8 +7500,10 @@ fn bpldat_writes_update_latched_planes_for_live_playfield_clxdat() { bus.custom_read(0x00E, 2) }; + // Slot $3A: the BPLxDAT event records at $3E (slot + write-effect + // delay), the boundary the pre-delay test staged directly. assert_eq!(clxdat_after_row_capture(None), 0x8000); - assert_eq!(clxdat_after_row_capture(Some(0x3E)), 0x8001); + assert_eq!(clxdat_after_row_capture(Some(0x3A)), 0x8001); } #[test] @@ -7605,10 +7624,12 @@ fn explicit_bpl1dat_output_accumulates_live_sprite_playfield_clxdat() { bus.advance_chipset(2); assert_eq!(bus.custom_read(0x00E, 2), 0x8000); - let before_bpl1dat = 0x38 - bus.agnus.hpos; + // Slot $34: the BPL1DAT event records at $38 (slot + write-effect + // delay), the same boundary the pre-delay test staged directly. + let before_bpl1dat = 0x34 - bus.agnus.hpos; bus.advance_chipset(before_bpl1dat); bus.write_custom_word_from(0x110, 0x8000, BeamWriteSource::Cpu); - bus.advance_chipset(2); + bus.advance_chipset(6); assert_eq!(bus.custom_read(0x00E, 2), 0x8020); assert_eq!(bus.custom_read(0x00E, 2), 0x8000); @@ -7619,7 +7640,8 @@ fn manual_sprite_and_bpl1dat_writes_accumulate_live_sprite_playfield_clxdat() { let mut bus = empty_bus(); let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0083); bus.agnus.vpos = 0x2C; - bus.agnus.hpos = 0x38; + // Slot $34: the write events record at $38 (slot + write-effect delay). + bus.agnus.hpos = 0x34; bus.denise.diwstrt = 0x2C83; bus.denise.diwstop = 0x2DC1; bus.denise.bplcon0 = 0x1000; @@ -7632,7 +7654,7 @@ fn manual_sprite_and_bpl1dat_writes_accumulate_live_sprite_playfield_clxdat() { bus.write_custom_word_from(0x144, 0x8000, BeamWriteSource::Cpu); bus.write_custom_word_from(0x110, 0x8000, BeamWriteSource::Cpu); - bus.advance_chipset(2); + bus.advance_chipset(6); assert_eq!(bus.custom_read(0x00E, 2), 0x8020); assert_eq!(bus.custom_read(0x00E, 2), 0x8000); @@ -7672,7 +7694,9 @@ fn same_line_bplcon1_scroll_increase_latches_later_live_sprite_playfield_clxdat( bus.current_frame_render_base = bus.capture_render_snapshot(); write_chip_word(&mut bus, 0x0100, 0x0001); - let before_scroll_write = 0x40 - bus.agnus.hpos; + // Slot $3C: the BPLCON1 event records at $40 (slot + write-effect + // delay), the same boundary the pre-delay test staged directly. + let before_scroll_write = 0x3C - bus.agnus.hpos; // Control words load at the vertical-blank reset line. sprite_fetch_control_words_at_reset_line(&mut bus); bus.agnus.vpos = 0x2C; @@ -7846,9 +7870,13 @@ fn same_line_bplcon3_spres_write_does_not_retime_earlier_live_sprite_playfield_c bus.custom_read(0x00E, 2) }; + // The BPLCON3 events record at the write slot + the write-effect delay: + // slot $34 -> event $38 (retimes the sprite before its pixels), slot + // $36 -> event $3A (too late) -- the same recorded boundary the + // pre-delay test staged directly at $38/$3A. assert_eq!(clxdat_after_bitplane_row_capture(None), 0x8022); - assert_eq!(clxdat_after_bitplane_row_capture(Some(0x38)), 0x8020); - assert_eq!(clxdat_after_bitplane_row_capture(Some(0x3A)), 0x8022); + assert_eq!(clxdat_after_bitplane_row_capture(Some(0x34)), 0x8020); + assert_eq!(clxdat_after_bitplane_row_capture(Some(0x36)), 0x8022); } #[test] diff --git a/src/video/bitplane.rs b/src/video/bitplane.rs index ad7a051..c2e775f 100644 --- a/src/video/bitplane.rs +++ b/src/video/bitplane.rs @@ -65,13 +65,15 @@ const DIW_HSTART_FETCH_REFERENCE_HIRES: i32 = 0x82; // and bitplane pixels still register against each other after widening. // // This anchor is the beam-position -> framebuffer-x mapping of the write -// domain and is calibrated by CPU-driven evidence (live collision family, -// manual sprite/BPL1DAT output), so it did NOT move when the Copper WAIT -// comparator lookahead moved copper write landings 4 colour clocks earlier -// on the bus: a write at a given beam position produces pixels at the same -// place regardless of who performed it. Copper-vs-fetch races compare both -// sides through this same anchor, so they follow the corrected bus landings -// automatically. +// domain. It maps events recorded at their Denise-effective position (the +// carrying chip-bus slot plus DENISE_WRITE_EFFECT_DELAY_CCK, source +// independent -- see `Bus::record_render_write`), so it did NOT move when +// the Copper WAIT comparator lookahead moved copper write landings 4 colour +// clocks earlier on the bus, nor when CPU-sourced events switched from the +// post-bus-cycle beam position to the same slot-referenced delay: a write +// carried by a given bus slot produces pixels at the same place regardless +// of who performed it. Copper-vs-fetch races compare both sides through +// this same anchor, so they follow the corrected bus landings automatically. const COPPER_WAIT_HPOS_FB0: i32 = 0x28; /// COLORxx writes feed Denise's final colour-selection/output path. Denise /// applies copper/CPU colour-register changes in the palette/output phase,