diff --git a/src/bus.rs b/src/bus.rs index 9ee7158..faece82 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -707,6 +707,18 @@ pub struct Bus { /// collapse into one pending VERTB bit. pub pending_vbi: u32, pending_copper_frame_start: Option, + /// Whether the Copper has been active at any point in the current field: + /// true from the vertical-blank COP1LC strobe if Copper DMA was enabled + /// there, or from any COPEN 0->1 write since. While false (a dormant + /// Copper), a COPxLC write for the Copper's current list retargets the + /// Copper PC directly instead of only loading the location latch -- + /// hardware behaviour the vAmigaTS Agnus/Copper/lc family photographs + /// (vAmiga models it as activeInThisFrame in pokeCOPxLC). + copper_active_in_frame: bool, + /// Which location register the Copper PC was last strobed from (1 or 2): + /// the vertical-blank restart selects COP1LC, COPJMPx selects its list. + /// The dormant-retarget rule above only applies to the matching list. + copper_current_list: u8, /// Paula interrupt bits that were pending when the current /// autovector was delivered. CPU INTREQ clears use this to tell @@ -1964,6 +1976,8 @@ impl Bus { diag_lowmem_blit: false, pending_vbi: 0, pending_copper_frame_start: None, + copper_active_in_frame: false, + copper_current_list: 1, delivered_irq_pending: 0, pending_copper_irq_beam: None, delivered_copper_irq_beam: None, @@ -2609,6 +2623,8 @@ impl Bus { self.slice_preempted = false; self.pending_vbi = 0; self.pending_copper_frame_start = None; + self.copper_active_in_frame = false; + self.copper_current_list = 1; self.delivered_irq_pending = 0; self.pending_copper_irq_beam = None; self.delivered_copper_irq_beam = None; diff --git a/src/bus/custom_regs.rs b/src/bus/custom_regs.rs index 49f44aa..11a678d 100644 --- a/src/bus/custom_regs.rs +++ b/src/bus/custom_regs.rs @@ -449,33 +449,46 @@ impl Bus { } 0x080 => { self.agnus.set_cop1lc_high(val); + self.copper_lc_written(1); false } 0x082 => { self.agnus.set_cop1lc_low(val); + self.copper_lc_written(1); false } 0x084 => { self.agnus.set_cop2lc_high(val); + self.copper_lc_written(2); false } 0x086 => { self.agnus.set_cop2lc_low(val); + self.copper_lc_written(2); false } 0x088 => { self.pending_copper_frame_start = None; + self.copper_current_list = 1; self.copper.jump(self.agnus.cop1lc); false } 0x08A => { self.pending_copper_frame_start = None; + self.copper_current_list = 2; self.copper.jump(self.agnus.cop2lc); false } 0x096 => { let previous = self.effective_bitplane_dmacon(); + let copen_before = self.agnus.dmacon & crate::chipset::copper::DMACON_COPEN != 0; self.agnus.write_dmacon(val); + if !copen_before && self.agnus.dmacon & crate::chipset::copper::DMACON_COPEN != 0 { + // COPEN switched on mid-field: the Copper counts as + // active this field, so COPxLC writes stop retargeting + // its PC (see copper_lc_written). + self.copper_active_in_frame = true; + } if crate::envcfg::flag("COPPERLINE_DIAG_AUDIO_NOTES") && (val & 0x000F != 0 || self.agnus.dmacon & 0x000F != previous & 0x000F) { diff --git a/src/bus/dma_slots.rs b/src/bus/dma_slots.rs index e4ad07a..c0e7742 100644 --- a/src/bus/dma_slots.rs +++ b/src/bus/dma_slots.rs @@ -994,6 +994,42 @@ impl Bus { } self.pending_copper_frame_start = None; self.copper.frame_start(cop1lc); + // The vertical-blank strobe selects COP1LC and records whether the + // Copper is live this field; a dormant Copper (DMA off here) has its + // PC retargeted by later COPxLC writes (copper_lc_written). + self.copper_current_list = 1; + self.copper_active_in_frame = self.copper_dma_enabled(); + } + + /// A COPxLC location register was rewritten. While the Copper has not + /// been active in the current field, a write to the location register it + /// was last strobed from retargets its program counter directly (real + /// Agnus behaviour, photographed by the vAmigaTS Copper/lc family); + /// otherwise the write only loads the latch for the next strobe. A + /// rewrite in the wrap-to-strobe window refreshes the pending strobe's + /// address so the restart uses the live COP1LC value. + pub(super) fn copper_lc_written(&mut self, list: u8) { + let lc = if list == 1 { + self.agnus.cop1lc + } else { + self.agnus.cop2lc + }; + if self.pending_copper_frame_start.is_some() { + if list == 1 { + self.pending_copper_frame_start = Some(lc); + } + return; + } + // A dormant Copper necessarily has its DMA off: any COPEN edge sets + // copper_active_in_frame. The explicit DMA check keeps directly + // constructed test states (DMACON preset without a register write) + // on the latch-only path a live Copper uses. + if !self.copper_active_in_frame + && !self.copper_dma_enabled() + && self.copper_current_list == list + { + self.copper.jump(lc); + } } pub(super) fn record_slice_bus_advance(&mut self, cck: u32, tick: AgnusTick) { diff --git a/src/bus/tests.rs b/src/bus/tests.rs index f298d3b..c126810 100644 --- a/src/bus/tests.rs +++ b/src/bus/tests.rs @@ -1363,6 +1363,43 @@ fn copper_dma_enable_gates_instruction_execution() { assert_eq!(bus.denise.bplcon0, 0x4200); } +#[test] +fn copper_lc_write_retargets_dormant_copper_pc() { + // Real Agnus: while the Copper has NOT been active in the current field + // (Copper DMA off at the vertical-blank strobe and no COPEN edge since), + // writing its current list's location register moves the Copper PC + // directly instead of only loading the latch. The vAmigaTS + // Agnus/Copper/lc tests photograph exactly this: a VERTB handler that + // rewrites COP1LC and then enables Copper DMA runs the NEW list. + let mut bus = empty_bus(); + let cop1 = 0x0100usize; + let redirected = 0x0200usize; + write_chip_word(&mut bus, cop1, 0x0180); + write_chip_word(&mut bus, cop1 + 2, 0x0111); + write_chip_word(&mut bus, cop1 + 4, 0xFFFF); + write_chip_word(&mut bus, cop1 + 6, 0xFFFE); + write_chip_word(&mut bus, redirected, 0x0180); + write_chip_word(&mut bus, redirected + 2, 0x0999); + write_chip_word(&mut bus, redirected + 4, 0xFFFF); + write_chip_word(&mut bus, redirected + 6, 0xFFFE); + + bus.agnus.dmacon = DMACON_DMAEN; // Copper DMA off: the Copper is dormant + bus.agnus.hpos = 0x20; + assert!(!bus.custom_write(0x080, 2, 0x0000)); + assert!(!bus.custom_write(0x082, 2, cop1 as u64)); + assert!(!bus.custom_write(0x088, 2, 0xFFFF)); // strobe: list 1, PC = cop1 + + // Dormant retarget: the rewrite moves the PC, no strobe needed. + assert!(!bus.custom_write(0x082, 2, redirected as u64)); + + assert!(!bus.custom_write(0x096, 2, (0x8000 | DMACON_COPEN) as u64)); + bus.advance_chipset(4); + assert_eq!( + bus.denise.palette[0], 0x0999, + "the redirected list must run without a COPJMP strobe" + ); +} + #[test] fn copper_dma_enable_gates_current_pc_until_copjmp_strobe() { let mut bus = empty_bus(); @@ -1379,6 +1416,11 @@ fn copper_dma_enable_gates_current_pc_until_copjmp_strobe() { bus.agnus.dmacon = DMACON_DMAEN; bus.copper.jump(stale as u32); + // The park at `stale` implies the Copper ran this field: COPxLC writes + // must only load the latch. (A DORMANT Copper - not active since the + // vertical blank - has its PC retargeted by the write instead; see + // copper_lc_written and the Copper/lc tests.) + bus.copper_active_in_frame = true; bus.agnus.hpos = 0x20; assert!(!bus.custom_write(0x080, 2, 0x0000)); assert!(!bus.custom_write(0x082, 2, cop1 as u64)); diff --git a/src/savestate.rs b/src/savestate.rs index ecb7303..2217872 100644 --- a/src/savestate.rs +++ b/src/savestate.rs @@ -78,7 +78,7 @@ const STATE_MAGIC: &[u8; 8] = b"CLSSTATE"; // for rows whose fetch diverges from the register-derived window) // 17: DisplaySpriteDmaState gained the two-slot sprite fetch fields // (data_words_fetched pointer progression, pending_data) -pub const STATE_VERSION: u32 = 18; +pub const STATE_VERSION: u32 = 19; /// Default state file name, timestamped like the screenshot/recorder names. pub fn auto_filename() -> std::path::PathBuf {