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
16 changes: 16 additions & 0 deletions src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,18 @@ pub struct Bus {
/// collapse into one pending VERTB bit.
pub pending_vbi: u32,
pending_copper_frame_start: Option<u32>,
/// 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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions src/bus/custom_regs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
36 changes: 36 additions & 0 deletions src/bus/dma_slots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
42 changes: 42 additions & 0 deletions src/bus/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion src/savestate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading