From dc3f41e212e3607628a0bdbbf98ed63beeb1a703 Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Mon, 6 Jul 2026 08:53:33 +0100 Subject: [PATCH] copper: wake the vertical-blank COP1LC strobe at hpos 6 The Copper does not start fetching at the very first color clock of the restart line. The vAmigaTS Copper/Skip/copstrt1+copstrt2 real-A500 captures bracket the restarted Copper's first comparator decision to beam $0B..$0C: copstrt1's SKIP at $000B is taken, copstrt2's at $000D is not. Waking the strobe at hpos 6 puts Copperline's first decision inside that window; both cases now match the hardware captures (copstrt1 also 0.000% against vAmiga; on copstrt2 vAmiga itself skips where the real machine does not, so the remaining reference diff there is vAmiga's). Demo screenshot set byte-identical; 1320 unit tests green (the four frame-restart tests advance through the strobe hpos and the restarted first write moves 0x02 -> 0x08). --- src/bus.rs | 2 ++ src/bus/dma_slots.rs | 19 ++++++++++++++++--- src/bus/tests.rs | 25 +++++++++++++------------ 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/bus.rs b/src/bus.rs index faece82..aecb677 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -5041,6 +5041,8 @@ fn chip_dma_addr_mask(chip_ram_len: usize) -> u32 { (bytes - 1) as u32 } +pub(crate) const COPPER_FRAME_START_HPOS: u32 = 6; + fn copper_frame_start_vpos(_video_standard: VideoStandard) -> u32 { // The Copper is restarted (COP1LC reloaded into the Copper PC) at the very // top of every frame and runs through the vertical-blank lines, not just diff --git a/src/bus/dma_slots.rs b/src/bus/dma_slots.rs index c0e7742..520a3cb 100644 --- a/src/bus/dma_slots.rs +++ b/src/bus/dma_slots.rs @@ -976,20 +976,33 @@ impl Bus { next_chip_bus_quantum_at(self.agnus.hpos, self.agnus.current_line_cck()) } + /// Horizontal position on the restart line where the vertical-blank + /// COP1LC strobe wakes the Copper. Calibrated against the vAmigaTS + /// Copper/Skip/copstrt1+copstrt2 real-A500 captures, which bracket the + /// first instruction's comparator decision to beam $0B..$0C: the Copper + /// does not start fetching at the very first color clock of the line. pub(super) fn cck_until_pending_copper_frame_start(&self) -> Option { self.pending_copper_frame_start?; let target_vpos = copper_frame_start_vpos(self.agnus.video_standard()); - if self.agnus.vpos >= target_vpos { + if self.agnus.vpos > target_vpos { return Some(0); } - self.agnus.cck_until_line_start(target_vpos) + if self.agnus.vpos == target_vpos { + return Some(COPPER_FRAME_START_HPOS.saturating_sub(self.agnus.hpos)); + } + self.agnus + .cck_until_line_start(target_vpos) + .map(|cck| cck.saturating_add(COPPER_FRAME_START_HPOS)) } pub(super) fn start_pending_copper_frame_if_due(&mut self) { let Some(cop1lc) = self.pending_copper_frame_start else { return; }; - if self.agnus.vpos < copper_frame_start_vpos(self.agnus.video_standard()) { + let target_vpos = copper_frame_start_vpos(self.agnus.video_standard()); + if self.agnus.vpos < target_vpos + || (self.agnus.vpos == target_vpos && self.agnus.hpos < COPPER_FRAME_START_HPOS) + { return; } self.pending_copper_frame_start = None; diff --git a/src/bus/tests.rs b/src/bus/tests.rs index c126810..24067b7 100644 --- a/src/bus/tests.rs +++ b/src/bus/tests.rs @@ -1459,14 +1459,14 @@ fn automatic_copper_restart_uses_live_cop1lc_at_frame_boundary() { // The Copper restarts at the top of the frame (vpos 0), not at the end // of vblank, so the live COP1LC (cop2) is picked up immediately as the // beam wraps -- no delay until the end of vblank. - bus.advance_chipset(1); + bus.advance_chipset(7); assert_eq!(bus.pending_copper_frame_start, None); assert_eq!(bus.denise.palette[0], 0); // From hpos 0 the Copper waits out the refresh band (0x00-0x08) and its // idle-half color clock at hpos 0x09, then its single MOVE fetches on // the next two even (access-parity) color clocks: write at hpos 0x0C. - bus.advance_chipset(13); + bus.advance_chipset(7); assert_eq!(bus.denise.palette[0], 0x0666); } @@ -2245,14 +2245,14 @@ fn forbidden_copper_move_recovers_at_start_of_frame_from_cop1lc() { bus.agnus.vpos = crate::chipset::agnus::PAL_LINES - 1; bus.agnus.hpos = COLORCLOCKS_PER_LINE - 1; - bus.advance_chipset(1); + bus.advance_chipset(7); // Restart is immediate at the top of the frame, recovering from the // forbidden MOVE via the live COP1LC. assert_eq!(bus.pending_copper_frame_start, None); // From hpos 0 the Copper waits out the refresh band and its idle-half // color clock, then its MOVE fetches on the next two even (access-parity) // color clocks: write at hpos 0x0C. - bus.advance_chipset(13); + bus.advance_chipset(7); assert_eq!(bus.denise.palette[0], 0x0666); } @@ -2496,12 +2496,12 @@ fn copper_programmed_cop1lc_sets_automatic_frame_restart() { bus.agnus.vpos = crate::chipset::agnus::PAL_LINES - 1; bus.agnus.hpos = COLORCLOCKS_PER_LINE - 1; - bus.advance_chipset(1); + bus.advance_chipset(7); // Restart is immediate at the top of the frame, picking up the // copper-programmed COP1LC straight away. assert_eq!(bus.pending_copper_frame_start, None); - bus.advance_chipset(16); + bus.advance_chipset(10); assert_eq!(bus.denise.palette[0], 0x0789); } @@ -2538,7 +2538,7 @@ fn automatic_vblank_reload_restarts_cop1_after_copjmp2_branch() { bus.agnus.vpos = crate::chipset::agnus::PAL_LINES - 1; bus.agnus.hpos = COLORCLOCKS_PER_LINE - 1; - bus.advance_chipset(1); + bus.advance_chipset(7); // Restart is immediate at the top of the frame (vpos 0), not delayed to // the end of vblank: the live COP1LC reload happens as the beam wraps. @@ -2547,14 +2547,15 @@ fn automatic_vblank_reload_restarts_cop1_after_copjmp2_branch() { let event_count = bus.current_render_events().len(); - // From hpos 0 the restarted Copper's MOVE fetches on the first two free - // access-parity color clocks. With the hardware refresh model (slots - // 0x004/6/8/A) those are hpos 0x00 and 0x02, so the write lands at 0x02. - bus.advance_chipset(3); + // The vertical-blank strobe wakes the Copper at COPPER_FRAME_START_HPOS + // (calibrated against the copstrt1/copstrt2 real-A500 captures), so the + // restarted MOVE fetches on the first free access-parity color clocks + // from there and the write lands at hpos 0x08. + bus.advance_chipset(6); assert_eq!(bus.denise.palette[0], 0x0111); let event = &bus.current_render_events()[event_count]; assert_eq!(event.vpos, 0); - assert_eq!(event.hpos, 0x02); + assert_eq!(event.hpos, 0x08); assert_eq!(event.source, super::BeamWriteSource::Copper); }