diff --git a/docs/debugger/headless.md b/docs/debugger/headless.md index a1e52bd..6251886 100644 --- a/docs/debugger/headless.md +++ b/docs/debugger/headless.md @@ -93,6 +93,7 @@ authoritative list. The most useful ones: | `COPPERLINE_DIAG_PCSAMPLE` | Top-50 executed-PC histogram every 50 frames | | `COPPERLINE_DIAG_PCHIST` | Full PC history (with `COPPERLINE_DIAG_PCHIST_START=SECS`) | | `COPPERLINE_DIAG_COPLEN` | Copper list length (optionally at a given emulated time) | +| `COPPERLINE_DIAG_COP_WRITES` | Every Copper MOVE's landing colour clock (beam position, register, value), for cross-emulator write-landing comparison against vAmiga's `VAMIGA_COP_PROBE` trace | | `COPPERLINE_DIAG_DISPLAY` | Display-register change log | | `COPPERLINE_DIAG_CAPROW` | `=all`, `=V`, or `=START:END`: per-line bitplane capture state at DDF start, including DMACON, current and DDF-anchor BPLCON0, FMODE/DIW/DDF, effective fetch window, unit/period/quantum, words/row, modulos, and all BPLxPTs -- separates wrong-pointer from wrong-decode display bugs | | `COPPERLINE_DIAG_PALETTE_ROW` | `=all`, `=V`, or `=START:END`: log beam-timed COLOR writes for selected beam lines, including source, framebuffer x, palette entry, LOCT, value, and BPLCON3; the setting is cached after first use | diff --git a/docs/internals/timing.md b/docs/internals/timing.md index 5fe0a59..d8a3c7e 100644 --- a/docs/internals/timing.md +++ b/docs/internals/timing.md @@ -162,12 +162,24 @@ clocks and idles on the odd ones (`Copper::hpos_is_access_cycle`). So: the alternate clocks free for the blitter/CPU. Modelling the Copper as owning every clock while running would starve a chip-bus-bound CPU during dense Copper effects such as horizontal colour gradients. -- WAIT and SKIP span **six** colour clocks, spending a dummy-plus-compare +- WAIT and SKIP span **eight** colour clocks, spending a two-Copper-cycle tail after their two word fetches (the Minimig - FETCH1/FETCH2/WAITSKIP1/WAITSKIP2 sequence). + FETCH1/FETCH2/WAITSKIP1/WAITSKIP2 sequence; vAmiga COP_WAIT1/COP_WAIT2 + at fetch2+2 and fetch2+4): an immediately-true WAIT or a SKIP resumes + fetching at fetch2+6. - The custom-register side effect occurs on the second word fetch, i.e. the third of the four colour clocks: three back-to-back MOVEs starting at beam `hpos` write at `hpos + 2`, `hpos + 6`, and `hpos + 10`. +- The WAIT comparator's horizontal input runs **two colour clocks ahead** + of the beam (`CopperWait::comparator_is_satisfied`), wrapping through 0 + over the last three clocks of a line. A sleeping WAIT therefore wakes at + target-2 -- the match colour clock is the bus-free wake-up cycle -- and + the next instruction's first fetch lands exactly on the masked target, + its write two clocks later. Cross-verified against vAmiga with the + two-sided landing probes (`COPPERLINE_DIAG_COP_WRITES` / + `VAMIGA_COP_PROBE`): a `WAIT $4721` + `MOVE SPR0CTL` lands the write at + hpos $22 in both emulators (vAmigaTS spritedma/interfere2, matching the + real-A500 photo). Anchoring the cadence to the beam rather than to a carried-over flip-flop is what makes a back-to-back colour MOVE list land its writes at the @@ -178,7 +190,16 @@ 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. -For the low-res renderer, a same-line `COLORxx` write at beam `hpos` +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`). + +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 `src/video/bitplane.rs`); beam-timed placement is anchored at `COPPER_WAIT_HPOS_FB0` ($28), and bitplane-control writes add the @@ -234,7 +255,12 @@ wait for the blitter to go idle. gets DMA slots to fetch it. - A Copper MOVE can update COP1LC/COP2LC; a later COPJMP strobe branches through the *current* value. A Copper MOVE to COPJMP1/COPJMP2 spends its - second word fetch on the strobe. + second word fetch on the strobe, then two more bus-free Copper cycles + (vAmiga COP_JMP1/COP_JMP2); the program counter reloads on the second of + those, so the first fetch from the new list lands three Copper cycles + after the strobe (verified against the vAmiga copper trace: a COPJMP2 + MOVE at hpos $04 fetches the target list's first MOVE at $0A, its write + landing at $0C). - The automatic frame reload latches the current COP1LC at end of frame and restarts the Copper at the top of the next frame (vpos 0) through the vertical-blank lines -- it branches through a Copper-programmed diff --git a/src/bus.rs b/src/bus.rs index 4f2c509..b185a55 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -240,7 +240,13 @@ const DMACON_BLTEN: u16 = 1 << 6; const DMACON_BPLEN: u16 = 1 << 8; const DMACON_BLTPRI: u16 = 1 << 10; const BLTCON1_DOFF: u16 = 1 << 7; -const COPPER_BUS_LOCKOUT_HPOS: u32 = 0x00E1; +// The Copper cannot take (or hold) a bus slot on the last-but-two color +// clock of the line: $E0 on short lines, $E1 on NTSC long lines (vAmiga +// busIsFree, keyed on the long-line flag). A fetch chain that hits +// the lockout resumes past the line wrap, which is what a line-end SKIP's +// deferred decision and the cycleE0 vAmigaTS case observe. +const COPPER_BUS_LOCKOUT_HPOS_SHORT_LINE: u32 = 0x00E0; +const COPPER_BUS_LOCKOUT_HPOS_LONG_LINE: u32 = 0x00E1; const COPER_CPU_IRQ_DELAY_CCK: u32 = 2; const RENDER_VISIBLE_START_VPOS: u32 = 0x2C; const RENDER_MIN_OVERSCAN_START_VPOS: u32 = 0x1C; @@ -271,6 +277,9 @@ const RENDER_DIW_HSTART_FETCH_REFERENCE_HIRES: i32 = 0x84; 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`). +const DENISE_WRITE_EFFECT_DELAY_CCK: u32 = 4; const BPLCON0_ECSENA: u16 = 1 << 0; const BPLCON0_SHRES: u16 = 1 << 6; const BPLCON3_BRDSPRT: u16 = 1 << 1; @@ -5095,7 +5104,14 @@ fn chip_dma_addr_mask(chip_ram_len: usize) -> u32 { (bytes - 1) as u32 } -pub(crate) const COPPER_FRAME_START_HPOS: u32 = 6; +// The vertical-blank COP1LC strobe wakes the Copper early on the restart +// line: its first instruction-word fetch lands on the hpos $02 access cycle +// and a leading MOVE's write on $04, matching the vAmiga copper trace +// (jumpbpu image-list upload; first MOVE write at v=0 h=$04). The value was +// 6 while copper WAIT releases ran four colour clocks late; it moved in +// lockstep with the WAIT comparator lookahead fix so un-waited frame-start +// streams keep their calibrated screen positions. +pub(crate) const COPPER_FRAME_START_HPOS: u32 = 2; fn copper_frame_start_vpos(_video_standard: VideoStandard) -> u32 { // The Copper is restarted (COP1LC reloaded into the Copper PC) at the very diff --git a/src/bus/dma_slots.rs b/src/bus/dma_slots.rs index 7865598..d35ffc8 100644 --- a/src/bus/dma_slots.rs +++ b/src/bus/dma_slots.rs @@ -7,6 +7,16 @@ use super::*; +/// One-shot env flag for the Copper write-landing trace +/// (`COPPERLINE_DIAG_COP_WRITES=1`): logs every Copper MOVE's landing color +/// clock (beam position, register, value) to stderr for cross-emulator +/// comparison against vAmiga's `VAMIGA_COP_PROBE` trace. +fn diag_cop_writes_on() -> bool { + use std::sync::OnceLock; + static V: OnceLock = OnceLock::new(); + *V.get_or_init(|| crate::envcfg::flag("COPPERLINE_DIAG_COP_WRITES")) +} + impl Bus { pub(super) fn advance_one_chip_bus_quantum( &mut self, @@ -199,6 +209,12 @@ impl Bus { CopperSlotAction::Idle => false, CopperSlotAction::BusUsed => true, CopperSlotAction::Move { register, value } => { + if diag_cop_writes_on() { + eprintln!( + "COPPROBE MOVE v={:03x} h={:02x} reg={:03x} val={:04x}", + vpos, hpos, register, value + ); + } if self.copper_can_write_custom(register) { let _ = self.write_custom_word_from(register, value, BeamWriteSource::Copper); } else { @@ -503,7 +519,7 @@ impl Bus { if quantum >= CHIP_BUS_SLOT_CCK && pending_copper_frame_start.is_none() && self.copper_dma_enabled() - && hpos != COPPER_BUS_LOCKOUT_HPOS + && !self.copper_bus_lockout_active_at(hpos) { let _ = copper.step_eligible_slot( &self.mem.chip_ram, @@ -521,7 +537,7 @@ impl Bus { true } else if !self.copper_dma_enabled() { false - } else if hpos == COPPER_BUS_LOCKOUT_HPOS { + } else if self.copper_bus_lockout_active_at(hpos) { copper.is_running() } else { !matches!( @@ -967,14 +983,22 @@ impl Bus { } pub(super) fn copper_bus_lockout_active_at(&self, hpos: u32) -> bool { - hpos == COPPER_BUS_LOCKOUT_HPOS + hpos == self.copper_bus_lockout_hpos() + } + + pub(super) fn copper_bus_lockout_hpos(&self) -> u32 { + if self.agnus.lol { + COPPER_BUS_LOCKOUT_HPOS_LONG_LINE + } else { + COPPER_BUS_LOCKOUT_HPOS_SHORT_LINE + } } pub(super) fn cck_until_copper_wait_position(&self, wait: CopperWait) -> Option { if wait.is_end_of_list() { return None; } - if wait.is_satisfied(self.agnus.vpos, self.agnus.hpos) { + if wait.comparator_is_satisfied(self.agnus.vpos, self.agnus.hpos) { return Some(0); } @@ -996,7 +1020,7 @@ impl Bus { vpos = 0; } } - if wait.is_satisfied(vpos, hpos) { + if wait.comparator_is_satisfied(vpos, hpos) { return Some(delta); } } @@ -1004,7 +1028,12 @@ impl Bus { } pub(super) fn cck_until_full_mask_copper_wait(&self, wait: CopperWait) -> Option { + // The comparator's horizontal input runs two color clocks ahead of + // the beam, so a sleeping full-mask wait releases two color clocks + // before its masked horizontal target (see + // `CopperWait::comparator_is_satisfied`). let target_h = (wait.position_bits() & 0x00FE) as u32; + let release_h = target_h.saturating_sub(2); let frame_lines = self.agnus.current_frame_lines(); for line_delta in 0..=frame_lines { @@ -1017,16 +1046,16 @@ impl Bus { let target_line_cck = self.line_cck_after_lines(line_delta); if line_delta == 0 { - if target_h < target_line_cck - && self.agnus.hpos <= target_h - && wait.is_satisfied(vpos, target_h) + if release_h < target_line_cck + && self.agnus.hpos <= release_h + && wait.comparator_is_satisfied(vpos, release_h) { - return Some(target_h - self.agnus.hpos); + return Some(release_h - self.agnus.hpos); } - } else if wait.is_satisfied(vpos, 0) { + } else if wait.comparator_is_satisfied(vpos, 0) { return Some(line_start_delta); - } else if target_h < target_line_cck && wait.is_satisfied(vpos, target_h) { - return Some(line_start_delta + target_h); + } else if release_h < target_line_cck && wait.comparator_is_satisfied(vpos, release_h) { + return Some(line_start_delta + release_h); } } diff --git a/src/bus/frame_capture.rs b/src/bus/frame_capture.rs index 7b501b7..bea6137 100644 --- a/src/bus/frame_capture.rs +++ b/src/bus/frame_capture.rs @@ -1301,7 +1301,31 @@ impl Bus { } pub(super) fn record_render_write(&mut self, offset: u16, value: u16, source: BeamWriteSource) { - let (vpos, hpos) = (self.agnus.vpos, self.agnus.hpos); + 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; + } + } + } let event = BeamRegisterWrite { vpos, hpos, diff --git a/src/bus/tests.rs b/src/bus/tests.rs index 3867bf4..72815b5 100644 --- a/src/bus/tests.rs +++ b/src/bus/tests.rs @@ -14,10 +14,10 @@ use super::{ CpuBusAccessKind, DeviceClock, DisplaySpriteDmaState, DisplaySpriteLineData, FrameBusTrace, LiveCollisionControl, LiveCollisionLineReplay, LiveSpriteCollisionSource, RenderRegisterSnapshot, BLITTER_SLOWDOWN_CPU_MISS_LIMIT, BLTCON1_DOFF, BPLCON0_ECSENA, - BPLCON3_BRDSPRT, BPLCON3_SPRES_HIRES, COPPER_BUS_LOCKOUT_HPOS, DENISE_HPOS_LAG_CCK, - DMACON_BLTEN, DMACON_BLTPRI, DMACON_BPLEN, DMACON_SPREN, PAL_SPRITE_DMA_FIRST_ACTIVE_VPOS, - RENDER_COPPER_WAIT_HPOS_FB0, RENDER_DIW_HSTART_FB0, RENDER_MIN_OVERSCAN_START_VPOS, - RENDER_VISIBLE_LINES, RENDER_VISIBLE_START_VPOS, SPRITE_DMA_SLOT1_HPOS, + BPLCON3_BRDSPRT, BPLCON3_SPRES_HIRES, DENISE_HPOS_LAG_CCK, DMACON_BLTEN, DMACON_BLTPRI, + DMACON_BPLEN, DMACON_SPREN, PAL_SPRITE_DMA_FIRST_ACTIVE_VPOS, RENDER_COPPER_WAIT_HPOS_FB0, + RENDER_DIW_HSTART_FB0, RENDER_MIN_OVERSCAN_START_VPOS, RENDER_VISIBLE_LINES, + RENDER_VISIBLE_START_VPOS, SPRITE_DMA_SLOT1_HPOS, }; use crate::audio::AudioSink; use crate::chipset::agnus::{ @@ -1221,11 +1221,11 @@ fn copper_interrupt_wait_fires_coper_at_programmed_line() { bus.advance_chipset(target_cck); assert_eq!(bus.paula.intreq & INT_COPER, 0); - // The Copper wakes on its tail check at the target position (hpos 0x20), - // spends a dummy wake-up cycle, then fetches its INTREQ MOVE on the - // next two even (access-parity) color clocks (0x24, 0x26) before the - // write lands. - bus.advance_chipset(6); + // The comparator's horizontal input runs two color clocks ahead of the + // beam, so the wait wakes on the bus-free color clock $1E, the first + // fetch of the INTREQ MOVE lands on the $20 target, and the write lands + // with the second fetch at $22 (the vAmiga/interfere-photo landing). + bus.advance_chipset(2); assert_eq!(bus.paula.intreq & INT_COPER, 0); bus.advance_chipset(1); @@ -1474,14 +1474,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(7); + bus.advance_chipset(5); 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(7); + // The restarted Copper's first instruction-word fetch lands on the hpos + // 0x02 access cycle (COPPER_FRAME_START_HPOS, vAmiga-trace calibrated), + // so the single MOVE's write lands at hpos 0x04. + bus.advance_chipset(2); assert_eq!(bus.denise.palette[0], 0x0666); } @@ -1493,7 +1493,9 @@ fn next_copper_wakeup_cck_tracks_wait_beam_position() { bus.agnus.hpos = 0x10; bus.copper.wait(CopperWait::new(0x5021, 0xFFFE)); - assert_eq!(bus.next_copper_wakeup_cck(), Some(0x10)); + // The comparator's horizontal input runs two color clocks ahead of the + // beam, so the wakeup lands at $1E, two before the masked $20 target. + assert_eq!(bus.next_copper_wakeup_cck(), Some(0x0E)); bus.agnus.hpos = 0x20; assert_eq!(bus.next_copper_wakeup_cck(), Some(0)); @@ -1659,14 +1661,15 @@ fn copper_wait_with_bfd_clear_resumes_after_busy_blitter_finishes() { bus.advance_chipset(2); assert_eq!(bus.denise.palette[0], 0); - // Once the blitter frees the bus, the Copper spends a dummy wake-up - // cycle, then its MOVE fetches land on the even copper slots and the - // write appears at hpos 0x2E (the engine finishes one colour clock - // earlier now that the startup models the hardware's poke+4 first - // body cycle). + // Once the blitter frees the bus (the engine finishes one colour clock + // earlier now that its startup models the hardware's poke+4 first body + // cycle), the color clock where the released comparator is evaluated is + // itself the bus-free wake-up cycle; the MOVE fetches land on the next + // even copper slots and the write lands at hpos 0x2E (recorded at its + // Denise-effective position, 0x32). bus.advance_chipset(8); assert_eq!(bus.denise.palette[0], 0x0555); - assert_eq!(bus.current_render_events()[0].hpos, 0x2E); + assert_eq!(bus.current_render_events()[0].hpos, 0x32); } #[test] @@ -1689,12 +1692,14 @@ fn copper_wait_with_bfd_set_ignores_busy_blitter_after_position_match() { bus.advance_chipset(2); assert_eq!(bus.denise.palette[0], 0); - // BFD set: the Copper ignores the busy blitter and resumes; it wakes on - // its tail check at hpos 0x21, spends a dummy wake-up cycle, and the - // MOVE write lands at hpos 0x26. - bus.advance_chipset(4); + // BFD set: the Copper ignores the busy blitter. The WAIT pays its + // WAIT1/WAIT2 tail through hpos 0x22, the already-true comparator is + // seen at 0x23, the wake-up spends the 0x24 access cycle, and the MOVE + // write lands at hpos 0x28 (fetch2+6 fetch, +2 write; recorded at its + // Denise-effective position, 0x2C). + bus.advance_chipset(6); assert_eq!(bus.denise.palette[0], 0x0666); - assert_eq!(bus.current_render_events()[0].hpos, 0x26); + assert_eq!(bus.current_render_events()[0].hpos, 0x2C); assert!(bus.blitter.busy); } @@ -1708,15 +1713,15 @@ fn copper_wait_immediate_match_uses_free_cycle_before_next_fetch() { bus.agnus.hpos = 0x20; bus.copper.jump(cop1 as u32); - // WAIT fetch parks the Copper already past the target; it wakes on its - // tail check at hpos 0x23, spends a dummy wake-up cycle, then the MOVE - // write lands at hpos 0x28. + // WAIT fetch parks the Copper already past the target; it pays the + // WAIT1/WAIT2 tail through hpos 0x24, sees the true comparator at 0x25, + // wakes on the 0x26 access cycle, and the MOVE write lands at hpos 0x2A. bus.advance_chipset(6); assert_eq!(bus.denise.palette[0], 0); - bus.advance_chipset(3); + bus.advance_chipset(5); assert_eq!(bus.denise.palette[0], 0x0777); - assert_eq!(bus.current_render_events()[0].hpos, 0x28); + assert_eq!(bus.current_render_events()[0].hpos, 0x2E); } #[test] @@ -1736,11 +1741,11 @@ fn copper_wait_wakeup_yields_free_cycle_after_late_match() { bus.advance_chipset(3); assert_eq!(bus.denise.palette[0], 0x0888); - assert_eq!(bus.current_render_events()[0].hpos, 0x2A); + assert_eq!(bus.current_render_events()[0].hpos, 0x2E); } #[test] -fn copper_e1_bus_lockout_defers_transfer_until_e2() { +fn copper_line_end_bus_lockout_defers_transfer_past_the_wrap() { let mut bus = empty_bus(); let cop1 = 0x0100usize; let start_vpos = 0x40; @@ -1751,20 +1756,19 @@ fn copper_e1_bus_lockout_defers_transfer_until_e2() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_COPEN; bus.agnus.vpos = start_vpos; - bus.agnus.hpos = COPPER_BUS_LOCKOUT_HPOS; + bus.agnus.hpos = bus.copper_bus_lockout_hpos(); bus.copper.jump(cop1 as u32); - // E1 is odd, i.e. the Copper's idle half under beam-parity locking, and - // it is also the end-of-line bus lockout, so the first-word fetch cannot - // happen here; the slot is free. + // $E0 on a PAL short line is the Copper's end-of-line bus lockout: the + // slot is claimed (nobody else can use it, vAmiga's BLOCKED marker) but + // no instruction word is fetched. bus.advance_chipset(1); - assert_eq!(bus.last_chip_bus_owner(), ChipBusOwner::Idle); assert_eq!(bus.copper.pc(), cop1 as u32); assert_eq!(bus.denise.palette[0], 0); - // The first-word fetch lands on the next access-parity color clock, E2, - // and the beam wraps to the next line. - bus.advance_chipset(1); + // $E1 is odd (the Copper's idle half), and $E2 is the next access-parity + // color clock: the first-word fetch lands there and the beam wraps. + bus.advance_chipset(2); assert_eq!(bus.copper.pc(), cop1 as u32 + 2); assert_eq!(bus.agnus.vpos, start_vpos + 1); assert_eq!(bus.agnus.hpos, 0); @@ -1772,11 +1776,12 @@ fn copper_e1_bus_lockout_defers_transfer_until_e2() { // With the hardware refresh model (4 slots at 0x004/6/8/A), hpos 0x00 // is a free access-parity color clock, so the Copper fetches the second - // word there immediately after the line wrap and its write lands at 0x00. + // word there immediately after the line wrap and its write lands at 0x00 + // (recorded at its Denise-effective position, 0x04). bus.advance_chipset(1); assert_eq!(bus.denise.palette[0], 0x0999); assert_eq!(bus.current_render_events()[0].vpos, start_vpos + 1); - assert_eq!(bus.current_render_events()[0].hpos, 0x00); + assert_eq!(bus.current_render_events()[0].hpos, 0x04); } #[test] @@ -1797,10 +1802,11 @@ fn copper_skip_does_not_skip_wait_instruction() { bus.agnus.hpos = 0x20; bus.copper.jump(cop1 as u32); // The SKIP runs the full hardware sequence: a 4-color-clock fetch plus - // its two bus-free tail cycles (WAITSKIP1/WAITSKIP2), then the WAIT - // spends its own 4-color-clock fetch, so the Copper reaches and parks on - // the WAIT after 9 color clocks. - bus.advance_chipset(9); + // its bus-free WAITSKIP1/WAITSKIP2 tail on the two following access + // cycles (next fetch at fetch2+6), then the WAIT spends its own + // 4-color-clock fetch, so the Copper reaches and parks on the WAIT + // after 11 color clocks. + bus.advance_chipset(11); assert!(bus.copper.waiting().is_some()); assert_eq!(bus.denise.palette[0], 0); @@ -1829,10 +1835,10 @@ fn copper_skip_over_move_consumes_move_fetch_slots_before_next_instruction() { assert_eq!(bus.denise.palette[0], 0); assert_eq!(bus.denise.palette[1], 0); - // The SKIP's two tail cycles elapse, the skipped MOVE still spends its - // 4-color-clock fetch, then the second MOVE spends its own before - // writing palette[1] at hpos 0x2C. - bus.advance_chipset(9); + // The SKIP's tail runs through the compare cycle at fetch2+4, the + // skipped MOVE still spends its 4-color-clock fetch, then the second + // MOVE spends its own before writing palette[1] at hpos 0x2E. + bus.advance_chipset(11); assert_eq!(bus.denise.palette[0], 0); assert_eq!(bus.denise.palette[1], 0x0222); @@ -1906,6 +1912,11 @@ fn copper_wait_wakeup_keeps_vp7_loop_switch_on_scanline_boundary() { }) .map(|event| event.hpos) .collect(); + // Each blast starts fetching at h=$32 (the WAIT's comparator lookahead + // wakes it at $30), so the first BPLCON1 write lands at $34 and the + // 42nd at $D8 -- the same positions on every line. The recorded render + // events sit four colour clocks later, at the writes' Denise-effective + // positions ($38..$DC). assert_eq!(line_128_hpos.first(), Some(&0x38)); assert_eq!(line_128_hpos.last(), Some(&0xDC)); } @@ -2035,22 +2046,25 @@ fn copper_move_writes_visible_registers_on_second_dma_slot() { .map(|event| (event.offset, event.value, event.hpos, event.source)) .collect::>(), vec![ + // Writes land at +2/+6/+10 on the second-word fetches; the + // recorded render events sit at the writes' Denise-effective + // positions (+4). ( 0x0182, 0x00F0, - RENDER_COPPER_WAIT_HPOS_FB0 + 2, + RENDER_COPPER_WAIT_HPOS_FB0 + 6, BeamWriteSource::Copper ), ( 0x0092, 0x0040, - RENDER_COPPER_WAIT_HPOS_FB0 + 6, + RENDER_COPPER_WAIT_HPOS_FB0 + 10, BeamWriteSource::Copper ), ( 0x0100, 0x1200, - RENDER_COPPER_WAIT_HPOS_FB0 + 10, + RENDER_COPPER_WAIT_HPOS_FB0 + 14, BeamWriteSource::Copper ), ] @@ -2082,8 +2096,9 @@ fn copper_move_palette_write_affects_pixels_after_second_dma_slot() { let event_hpos = bus.current_render_events()[0].hpos; // MOVE write lands on its second-word fetch, two color clocks into the - // 4-color-clock cadence from the start hpos (+30). - assert_eq!(event_hpos, RENDER_COPPER_WAIT_HPOS_FB0 + 32); + // 4-color-clock cadence from the start hpos (+30); the recorded event + // sits at its Denise-effective position (+4). + assert_eq!(event_hpos, RENDER_COPPER_WAIT_HPOS_FB0 + 36); let words_per_row = bitplane_words_per_row( bus.agnus.revision(), bus.denise.bplcon0, @@ -2349,11 +2364,11 @@ fn cpu_copjmp1_strobe_waits_for_target_instruction_dma_slots() { assert!(bus.current_render_events().is_empty()); // First-word fetch at 0x20, idle half at 0x21, second-word fetch+write - // at 0x22. + // at 0x22 (recorded at its Denise-effective position, 0x26). bus.advance_chipset(2); assert_eq!(bus.denise.palette[0], 0x0123); let event = &bus.current_render_events()[0]; - assert_eq!(event.hpos, 0x22); + assert_eq!(event.hpos, 0x26); assert_eq!(event.source, super::BeamWriteSource::Copper); } @@ -2378,9 +2393,10 @@ fn copper_move_to_copjmp2_loads_second_list() { assert!(!bus.custom_write(0x084, 2, 0x0000)); assert!(!bus.custom_write(0x086, 2, cop2 as u64)); assert!(!bus.custom_write(0x088, 2, 0xFFFF)); - // COPJMP2 MOVE fetch (4 cck) then the cop2 MOVE fetch (4 cck): write at - // hpos 0x26. - bus.advance_chipset(7); + // COPJMP2 MOVE fetch (4 cck), the strobe's two tail cycles + // (COP_JMP1/COP_JMP2, reloading the program counter), then the cop2 + // MOVE fetch: write at hpos 0x2A. + bus.advance_chipset(11); assert_eq!(bus.denise.palette[0], 0x0456); assert_eq!( @@ -2419,12 +2435,14 @@ fn copper_copjmp2_strobe_waits_for_target_instruction_dma_slots() { assert_eq!(bus.denise.palette[0], 0); assert!(bus.current_render_events().is_empty()); - // COPJMP2 strobe completes at 0x22; then idle half, the cop2 MOVE fetch - // (4 cck), writing at hpos 0x26. - bus.advance_chipset(4); + // COPJMP2 strobe completes at 0x22, spends its two tail cycles + // (COP_JMP1 0x24, COP_JMP2 0x26 reloading the program counter), then + // the cop2 MOVE fetch writes at hpos 0x2A (recorded at its + // Denise-effective position, 0x2E). + bus.advance_chipset(8); assert_eq!(bus.denise.palette[0], 0x0456); let event = &bus.current_render_events()[0]; - assert_eq!(event.hpos, 0x26); + assert_eq!(event.hpos, 0x2E); assert_eq!(event.source, super::BeamWriteSource::Copper); } @@ -2456,9 +2474,10 @@ fn copper_can_program_cop2lc_before_copjmp2_loop_branch() { assert!(!bus.custom_write(0x084, 2, 0x0000)); assert!(!bus.custom_write(0x086, 2, stale_cop2 as u64)); assert!(!bus.custom_write(0x088, 2, 0xFFFF)); - // Three programming MOVEs plus the jumped-to MOVE, each a 4-color-clock - // fetch: the final write lands at hpos 0x2E. - bus.advance_chipset(15); + // Three programming MOVEs, the strobe's two tail cycles + // (COP_JMP1/COP_JMP2), then the jumped-to MOVE: the final write lands + // at hpos 0x32. + bus.advance_chipset(19); assert_eq!(bus.agnus.cop2lc, programmed_cop2 as u32); assert_eq!(bus.denise.palette[0], 0x0789); @@ -2487,9 +2506,10 @@ fn copper_can_program_cop1lc_before_copjmp1_loop_branch() { assert!(!bus.custom_write(0x080, 2, 0x0000)); assert!(!bus.custom_write(0x082, 2, cop1 as u64)); assert!(!bus.custom_write(0x088, 2, 0xFFFF)); - // Two programming MOVEs and the COPJMP1 MOVE, then the jumped-to MOVE, - // each a 4-color-clock fetch: the final write lands at hpos 0x2E. - bus.advance_chipset(15); + // Two programming MOVEs and the COPJMP1 MOVE, the strobe's two tail + // cycles (COP_JMP1/COP_JMP2), then the jumped-to MOVE: the final write + // lands at hpos 0x32. + bus.advance_chipset(19); assert_eq!(bus.agnus.cop1lc, programmed_cop1 as u32); assert_eq!(bus.denise.palette[0], 0x0789); @@ -2554,17 +2574,19 @@ fn automatic_vblank_reload_restarts_cop1_after_copjmp2_branch() { assert!(!bus.custom_write(0x084, 2, 0x0000)); assert!(!bus.custom_write(0x086, 2, cop2 as u64)); assert!(!bus.custom_write(0x088, 2, 0xFFFF)); - // MOVE 0x0111 writes at 0x22; the COPJMP2 MOVE then the cop2 MOVE follow - // at the 4-color-clock cadence, writing 0x0222 at hpos 0x2A. - bus.advance_chipset(11); + // MOVE 0x0111 writes at 0x22; the COPJMP2 MOVE, its two strobe tail + // cycles (COP_JMP1/COP_JMP2), then the cop2 MOVE follow, writing 0x0222 + // at hpos 0x2E. Recorded render events sit at the writes' + // Denise-effective positions (+4). + bus.advance_chipset(15); assert_eq!(bus.denise.palette[0], 0x0222); - assert_eq!(bus.current_render_events()[0].hpos, 0x22); - assert_eq!(bus.current_render_events()[1].hpos, 0x2A); + assert_eq!(bus.current_render_events()[0].hpos, 0x26); + assert_eq!(bus.current_render_events()[1].hpos, 0x32); bus.agnus.vpos = crate::chipset::agnus::PAL_LINES - 1; bus.agnus.hpos = COLORCLOCKS_PER_LINE - 1; - bus.advance_chipset(7); + bus.advance_chipset(5); // 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. @@ -2574,9 +2596,9 @@ fn automatic_vblank_reload_restarts_cop1_after_copjmp2_branch() { let event_count = bus.current_render_events().len(); // 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. + // (vAmiga-trace calibrated), so the restarted MOVE fetches on the first + // free access-parity color clocks from there and the write lands at + // hpos 0x04 (recorded at its Denise-effective position, 0x08). bus.advance_chipset(6); assert_eq!(bus.denise.palette[0], 0x0111); let event = &bus.current_render_events()[event_count]; diff --git a/src/chipset/copper.rs b/src/chipset/copper.rs index b658c20..e7116c7 100644 --- a/src/chipset/copper.rs +++ b/src/chipset/copper.rs @@ -102,6 +102,34 @@ impl CopperWait { (self.second & 0x7FFE) | 0x8000 } + /// Whether the WAIT/SKIP comparator output is true at beam color clock + /// `(vpos, hpos)`. + /// + /// The horizontal count Agnus feeds the comparator runs two color clocks + /// ahead of the bus cycle, so a sleeping WAIT wakes two color clocks + /// before its masked horizontal target and the first fetch of the next + /// instruction lands exactly on the target color clock (vAmiga + /// `runHorizontalComparator`; confirmed against the real-A500 + /// vAmigaTS spritedma/interfere photos, where a MOVE behind + /// `WAIT $4721` writes SPR0CTL at hpos $22). At the end of the line the + /// comparator's horizontal input wraps through zero early: during the + /// last color clocks it presents the next line's first positions while + /// the vertical count still holds the current line, so waits cannot + /// release in the line-end tail. + pub fn comparator_is_satisfied(self, vpos: u32, hpos: u32) -> bool { + self.is_satisfied(vpos, Self::comparator_hpos(hpos)) + } + + /// The horizontal position the WAIT/SKIP comparator sees at beam color + /// clock `hpos` (see [`CopperWait::comparator_is_satisfied`]). + pub fn comparator_hpos(hpos: u32) -> u32 { + if hpos < 0xE0 { + hpos + 2 + } else { + hpos - 0xE0 + } + } + pub fn is_satisfied(self, vpos: u32, hpos: u32) -> bool { if self.is_end_of_list() { return false; @@ -150,12 +178,9 @@ impl CopperWait { if self.is_end_of_list() { return None; } - if self.is_satisfied(start_vpos, start_hpos) { + if self.comparator_is_satisfied(start_vpos, start_hpos) { return Some(0); } - if self.compare_mask() == 0xFFFE { - return self.cck_until_full_mask_satisfied(start_vpos, start_hpos); - } let mut vpos = start_vpos; let mut hpos = start_hpos; @@ -169,43 +194,12 @@ impl CopperWait { vpos = 0; } } - if self.is_satisfied(vpos, hpos) { + if self.comparator_is_satisfied(vpos, hpos) { return Some(delta); } } None } - - #[cfg(test)] - fn cck_until_full_mask_satisfied(self, start_vpos: u32, start_hpos: u32) -> Option { - let target_h = (self.position_bits() & 0x00FE) as u32; - - for line_delta in 0..=PAL_LINES { - let vpos = (start_vpos + line_delta) % PAL_LINES; - let line_start_delta = if line_delta == 0 { - 0 - } else { - COLORCLOCKS_PER_LINE - .saturating_sub(start_hpos) - .saturating_add((line_delta - 1).saturating_mul(COLORCLOCKS_PER_LINE)) - }; - - if line_delta == 0 { - if target_h < COLORCLOCKS_PER_LINE - && start_hpos <= target_h - && self.is_satisfied(vpos, target_h) - { - return Some(target_h - start_hpos); - } - } else if self.is_satisfied(vpos, 0) { - return Some(line_start_delta); - } else if target_h < COLORCLOCKS_PER_LINE && self.is_satisfied(vpos, target_h) { - return Some(line_start_delta + target_h); - } - } - - None - } } /// The Copper's WAIT comparator cannot release a wait during the last color @@ -263,11 +257,20 @@ enum CopperState { /// before the comparator output takes effect. On real Agnus a SKIP runs the /// identical 4-cycle FETCH1/FETCH2/WAITSKIP1/WAITSKIP2 sequence as a WAIT /// (Minimig Copper.v): a dummy cycle then the compare cycle, both bus-free. - /// The skip condition is evaluated on the final (compare) cycle. + /// The decision is sampled at the next instruction's first-word fetch. Skipping { skip: CopperWait, phase: CopperSkipPhase, }, + /// A Copper MOVE to COPJMP1/COPJMP2 spends two more Copper cycles after + /// its second-word fetch (vAmiga COP_JMP1/COP_JMP2) before the program + /// counter is reloaded from the live location register; the first fetch + /// from the new list lands one Copper cycle after that. (vAmiga's + /// "$E0 continues in $E1" sub-quirk of COP_JMP1 is not modelled.) + Jumping { + second_list: bool, + phase: CopperJumpPhase, + }, Stopped, } @@ -278,6 +281,16 @@ enum CopperWaitPhase { Wakeup, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +enum CopperJumpPhase { + /// COP_JMP1: a first bus-free tail cycle after the strobe's second + /// instruction-word fetch. + First, + /// COP_JMP2: the cycle that loads the program counter from the (live) + /// COP1LC/COP2LC, after which the Copper fetches from the new list. + Second, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] enum CopperSkipPhase { /// WAITSKIP1: the dummy cycle that requests no DMA. @@ -323,6 +336,13 @@ pub struct Copper { state: CopperState, pending_first: Option, skip_next_move: bool, + /// A SKIP whose condition is still to be sampled: the compare cycle only + /// hands control back, and the decision samples the comparator at the + /// next instruction's first-word fetch color clock (vAmiga evaluates the + /// skip flag inside COP_FETCH). A line-end SKIP whose fetch is pushed + /// past the line wrap by the copper bus lockout therefore sees the next + /// line's vertical phase. + skip_eval: Option, /// Monotonic count of completed instructions (a MOVE applied or /// skipped, a WAIT/SKIP/COPJMP started). Debugger-only bookkeeping /// for copper single-stepping; transient, never serialized. @@ -343,6 +363,7 @@ impl Copper { state: CopperState::Stopped, pending_first: None, skip_next_move: false, + skip_eval: None, instructions_retired: 0, } } @@ -361,12 +382,14 @@ impl Copper { self.pc = address & !1; self.pending_first = None; self.skip_next_move = false; + self.skip_eval = None; self.state = CopperState::Running; } pub fn stop(&mut self) { self.pending_first = None; self.skip_next_move = false; + self.skip_eval = None; self.state = CopperState::Stopped; } @@ -389,6 +412,41 @@ impl Copper { }; } + /// Begin a COPJMP strobe's two bus-free tail cycles (COP_JMP1/COP_JMP2). + /// The program counter reloads on the second tail cycle, from the live + /// COP1LC/COP2LC at that moment. + fn start_jump_tail(&mut self, second_list: bool) { + self.pending_first = None; + self.skip_next_move = false; + self.skip_eval = None; + self.state = CopperState::Jumping { + second_list, + phase: CopperJumpPhase::First, + }; + } + + /// Advance a COPJMP strobe's tail: each phase elapses on an access-parity + /// color clock, and the second one performs the jump. + fn advance_jump_free_cycle(&mut self, hpos: u32, cop1lc: u32, cop2lc: u32) { + let CopperState::Jumping { second_list, phase } = self.state else { + return; + }; + if !Self::hpos_is_access_cycle(hpos) { + return; + } + match phase { + CopperJumpPhase::First => { + self.state = CopperState::Jumping { + second_list, + phase: CopperJumpPhase::Second, + }; + } + CopperJumpPhase::Second => { + self.jump(if second_list { cop2lc } else { cop1lc }); + } + } + } + /// Begin a SKIP's two bus-free tail cycles (WAITSKIP1/WAITSKIP2). The skip /// decision is deferred to the compare cycle so a SKIP occupies the same /// four Copper cycles as a WAIT, matching real Agnus timing. @@ -432,17 +490,15 @@ impl Copper { } } CopperWaitPhase::InstructionTail => { - // The comparator cannot release a wait in the line-end blackout - // (see WAIT_RELEASE_LINE_END_BLACKOUT_CCK); the wait keeps - // sleeping and is re-evaluated at the next line's positions. - let released = !wait_release_blocked_at_line_end(wait, vpos, hpos, line_cck) - && wait_is_satisfied_with_blitter(wait, vpos, hpos, blitter_busy); - if released { - self.state = CopperState::Waiting { - wait, - phase: CopperWaitPhase::Wakeup, - }; - } else { + // A WAIT spends two bus-free tail cycles after its second + // instruction-word fetch (WAITSKIP1/WAITSKIP2 in Minimig, + // COP_WAIT1/COP_WAIT2 in vAmiga at fetch2+2/fetch2+4) before + // the comparator output can act: the tail runs to the next + // access-parity color clock and the comparator takes over on + // the following one, so an immediately-true WAIT's next fetch + // lands at fetch2+6, exactly one Copper cycle later than a + // sleeping wait's post-wake fetch. + if Self::hpos_is_access_cycle(hpos) { self.state = CopperState::Waiting { wait, phase: CopperWaitPhase::Waiting, @@ -450,39 +506,66 @@ impl Copper { } } CopperWaitPhase::Waiting => { + // The comparator cannot release a wait in the line-end blackout + // (see WAIT_RELEASE_LINE_END_BLACKOUT_CCK); the wait keeps + // sleeping and is re-evaluated at the next line's positions. let released = !wait_release_blocked_at_line_end(wait, vpos, hpos, line_cck) - && wait_is_satisfied_with_blitter(wait, vpos, hpos, blitter_busy); + && wait_comparator_released(wait, vpos, hpos, blitter_busy); if released { - self.state = CopperState::Waiting { - wait, - phase: CopperWaitPhase::Wakeup, + // The color clock where the comparator goes true is the + // wake-up cycle itself (no DMA request); the next access + // cycle already fetches. A match evaluated on an access + // cycle therefore resumes Running directly, so the fetch + // lands one Copper cycle (2 ccks) after the match -- + // vAmiga's COP_WAKEUP -> COP_FETCH spacing. A match on an + // off-parity color clock spends the following access cycle + // as the wake-up cycle instead. + self.state = if Self::hpos_is_access_cycle(hpos) { + CopperState::Running + } else { + CopperState::Waiting { + wait, + phase: CopperWaitPhase::Wakeup, + } }; } } } } - /// Advance a SKIP through its two bus-free tail cycles. The dummy cycle - /// elapses first; on the following compare cycle (a Copper access cycle) the - /// skip condition is evaluated against the live beam position and, if true, - /// the next MOVE is marked for discard. The Copper then resumes fetching. - pub fn advance_skip_free_cycle(&mut self, vpos: u32, hpos: u32, blitter_busy: bool) { + /// Advance a SKIP through its two bus-free tail cycles + /// (WAITSKIP1/WAITSKIP2). The compare cycle hands control back with the + /// decision still pending; the condition is sampled at the next + /// instruction's first-word fetch (see `Copper::step_eligible_slot`). + pub fn advance_skip_free_cycle(&mut self, hpos: u32) { let CopperState::Skipping { skip, phase } = self.state else { return; }; match phase { CopperSkipPhase::Dummy => { - self.state = CopperState::Skipping { - skip, - phase: CopperSkipPhase::Compare, - }; + // Like a WAIT's tail, the dummy cycle runs to the next + // access-parity color clock (fetch2+2); the compare cycle is + // the access cycle after it (fetch2+4), so the post-SKIP + // fetch lands at fetch2+6 -- the same footprint as a WAIT. + if Self::hpos_is_access_cycle(hpos) { + self.state = CopperState::Skipping { + skip, + phase: CopperSkipPhase::Compare, + }; + } } CopperSkipPhase::Compare => { if Self::hpos_is_access_cycle(hpos) { - if wait_is_satisfied_with_blitter(skip, vpos, hpos, blitter_busy) { - self.skip_next_move = true; - } + // The compare cycle only hands control back; the SKIP + // decision samples the comparator at the next + // instruction's first-word fetch (vAmiga evaluates the + // skip flag inside COP_FETCH). A line-end SKIP whose + // fetch is pushed past the line wrap by the copper bus + // lockout therefore sees the next line's vertical phase, + // which is what lets a `SKIP vp,$E0`-style list switch + // fire at the end of a full blast line. + self.skip_eval = Some(skip); self.state = CopperState::Running; } } @@ -601,9 +684,17 @@ impl Copper { } CopperState::Skipping { .. } => { // A SKIP's two tail cycles request no DMA, exactly like a WAIT's - // tail, so the blitter/CPU keep them. The skip decision lands on - // the compare cycle. - self.advance_skip_free_cycle(vpos, hpos, blitter_busy); + // tail, so the blitter/CPU keep them. The decision is sampled + // later, at the next instruction's first-word fetch. + self.advance_skip_free_cycle(hpos); + CopperSlotAction::Idle + } + CopperState::Jumping { .. } => { + // A COPJMP strobe's two tail cycles request no DMA either; the + // program counter reloads on the second one, so the first fetch + // from the new list lands three Copper cycles after the strobe + // (vAmiga COP_JMP1/COP_JMP2 then COP_FETCH). + self.advance_jump_free_cycle(hpos, cop1lc, cop2lc); CopperSlotAction::Idle } CopperState::Running => { @@ -613,6 +704,15 @@ impl Copper { if !allow_fetch || !Self::hpos_is_access_cycle(hpos) { return CopperSlotAction::Idle; } + // A pending SKIP decision samples the comparator on the + // color clock of the next instruction's first-word fetch. + if self.pending_first.is_none() { + if let Some(skip) = self.skip_eval.take() { + if wait_comparator_released(skip, vpos, hpos, blitter_busy) { + self.skip_next_move = true; + } + } + } match self.fetch_decode(chip_ram) { CopperFetch::Idle => CopperSlotAction::Idle, CopperFetch::FirstWord { .. } => CopperSlotAction::BusUsed, @@ -624,13 +724,13 @@ impl Copper { CopperInstruction::Move { register: 0x088, .. } => { - self.jump(cop1lc); + self.start_jump_tail(false); CopperSlotAction::BusUsed } CopperInstruction::Move { register: 0x08A, .. } => { - self.jump(cop2lc); + self.start_jump_tail(true); CopperSlotAction::BusUsed } CopperInstruction::Move { register, value } => { @@ -660,13 +760,11 @@ impl Copper { } } -fn wait_is_satisfied_with_blitter( - wait: CopperWait, - vpos: u32, - hpos: u32, - blitter_busy: bool, -) -> bool { - wait.is_satisfied(vpos, hpos) && (!wait.blitter_wait_enabled() || !blitter_busy) +/// Whether the WAIT/SKIP comparator (with its two-color-clock horizontal +/// lookahead, see [`CopperWait::comparator_is_satisfied`]) releases at +/// `(vpos, hpos)`, including the blitter-finished gate for BFD-clear waits. +fn wait_comparator_released(wait: CopperWait, vpos: u32, hpos: u32, blitter_busy: bool) -> bool { + wait.comparator_is_satisfied(vpos, hpos) && (!wait.blitter_wait_enabled() || !blitter_busy) } #[cfg(test)] @@ -732,7 +830,9 @@ mod tests { assert!(wait_next_line.is_satisfied(0x00, 0x40)); // Still unsatisfied before the target horizontal position is reached. assert!(!wait_next_line.is_satisfied(0x00, 0x10)); - assert_eq!(wait_next_line.cck_until_satisfied(0x49, 0x0E), Some(0x1A)); + // The comparator's two-color-clock horizontal lookahead releases the + // sleeping wait at h = $26, two before the masked target $28. + assert_eq!(wait_next_line.cck_until_satisfied(0x49, 0x0E), Some(0x18)); assert!(wait_next_line.is_satisfied(0x100, 0x28)); assert!(wait_next_line.is_satisfied(0x80, 0xE0)); @@ -751,7 +851,9 @@ mod tests { fn full_mask_waits_stay_satisfied_after_target_beam_position() { let wait = CopperWait::new(0x5021, 0xFFFE); - assert_eq!(wait.cck_until_satisfied(0x50, 0x10), Some(0x10)); + // Sleeping release happens two color clocks before the target (the + // comparator's horizontal input runs two ahead of the beam). + assert_eq!(wait.cck_until_satisfied(0x50, 0x10), Some(0x0E)); assert_eq!(wait.cck_until_satisfied(0x50, 0x20), Some(0)); assert_eq!(wait.cck_until_satisfied(0x50, 0x22), Some(0)); assert_eq!(wait.cck_until_satisfied(0x51, 0x00), Some(0)); @@ -795,12 +897,13 @@ mod tests { assert!(!wait.is_satisfied(0x7F, 0x20)); assert!(wait.is_satisfied(0x80, 0x20)); assert!(wait.is_satisfied(0x100, 0x00)); - // At the end of line 255 the high-half (VP7=1) target has already been - // reached, so the wait is satisfied immediately (reach-or-pass) rather - // than one colorclock later. + // At the last color clock of line 255 the comparator's wrapped + // horizontal input already presents the next line's first positions + // (0..2), below the $20 target, so the release waits for the + // rollover color clock where the VP7 phase has passed. assert_eq!( wait.cck_until_satisfied(0xFF, COLORCLOCKS_PER_LINE - 1), - Some(0) + Some(1) ); } @@ -822,23 +925,33 @@ mod tests { "wait must not release at line-end hpos {hpos}" ); } - // Next line, before the horizontal target: still waiting. + // Next line, before the horizontal target: still waiting (the + // comparator input runs two color clocks ahead of the beam). copper.advance_wait_free_cycle(137, 0x10, false, COLORCLOCKS_PER_LINE); assert!(!copper.is_running()); - // Next line, at the horizontal target: comparator matches, then the - // Copper spends one bus-free wake-up cycle before running. - copper.advance_wait_free_cycle(137, 0x32, false, COLORCLOCKS_PER_LINE); + copper.advance_wait_free_cycle(137, 0x2E, false, COLORCLOCKS_PER_LINE); assert!(!copper.is_running()); - copper.advance_wait_free_cycle(137, 0x34, false, COLORCLOCKS_PER_LINE); + // Two color clocks before the target the comparator matches; the + // match color clock is the bus-free wake-up cycle and the Copper + // resumes so its first fetch lands exactly on the $32 target. + copper.advance_wait_free_cycle(137, 0x30, false, COLORCLOCKS_PER_LINE); assert!(copper.is_running()); - // Away from the line end the same satisfied wait still pays the wake-up - // cycle before the next fetch can run. + // An already-true wait still pays the WAIT1/WAIT2 tail after its + // second-word fetch (here on the access cycle 0x80): the tail runs + // through the next access cycle (0x82), the comparator matches on + // 0x83, the wake-up spends the 0x84 access cycle, and the next fetch + // lands at fetch2+6. let mut copper = Copper::new(); copper.start_wait_instruction(wait); - copper.advance_wait_free_cycle(136, 0x80, false, COLORCLOCKS_PER_LINE); - assert!(!copper.is_running()); - copper.advance_wait_free_cycle(136, 0x82, false, COLORCLOCKS_PER_LINE); + for hpos in [0x81, 0x82, 0x83] { + copper.advance_wait_free_cycle(136, hpos, false, COLORCLOCKS_PER_LINE); + assert!( + !copper.is_running(), + "still in the instruction tail or wakeup at hpos {hpos}" + ); + } + copper.advance_wait_free_cycle(136, 0x84, false, COLORCLOCKS_PER_LINE); assert!(copper.is_running()); } @@ -852,14 +965,14 @@ mod tests { let wait = CopperWait::new(0x80E1, 0x80FE); assert!(wait.is_satisfied(200, 0xE0)); // Just before the blackout the wait is not yet satisfied, so it is not - // deferred; it releases at its target hpos inside the blackout. + // deferred. The comparator's lookahead sees the $E0 target two color + // clocks early, so the wake-up cycle is $DE and the first fetch lands + // on the $E0 target itself. let mut copper = Copper::new(); - copper.start_wait_instruction(wait); - copper.advance_wait_free_cycle(200, 0xDE, false, COLORCLOCKS_PER_LINE); + copper.wait(wait); + copper.advance_wait_free_cycle(200, 0xDC, false, COLORCLOCKS_PER_LINE); assert!(!copper.is_running()); - copper.advance_wait_free_cycle(200, 0xE0, false, COLORCLOCKS_PER_LINE); - assert!(!copper.is_running(), "match cycle is bus-free wake-up"); - copper.advance_wait_free_cycle(200, 0xE2, false, COLORCLOCKS_PER_LINE); + copper.advance_wait_free_cycle(200, 0xDE, false, COLORCLOCKS_PER_LINE); assert!( copper.is_running(), "wait whose target is in the blackout must release at that target" @@ -867,13 +980,21 @@ mod tests { } #[test] - fn wait_wakeup_spends_dummy_access_cycle_before_running() { + fn wait_wakeup_match_cycle_is_the_dummy_cycle() { + // The color clock where the comparator goes true is the bus-free + // wake-up cycle: a match evaluated on an access cycle resumes the + // Copper immediately (its next fetch lands one Copper cycle later), + // while a match on an off-parity color clock spends the following + // access cycle waking up instead. let wait = CopperWait::new(0x0033, 0x80FE); let mut copper = Copper::new(); - copper.start_wait_instruction(wait); + copper.wait(wait); copper.advance_wait_free_cycle(0, 0x34, false, COLORCLOCKS_PER_LINE); - assert!(!copper.is_running()); + assert!(copper.is_running()); + + let mut copper = Copper::new(); + copper.wait(wait); copper.advance_wait_free_cycle(0, 0x35, false, COLORCLOCKS_PER_LINE); assert!(!copper.is_running()); copper.advance_wait_free_cycle(0, 0x36, false, COLORCLOCKS_PER_LINE); @@ -1029,8 +1150,11 @@ mod tests { #[test] fn finds_future_beam_time_for_waits() { + // The comparator's horizontal input runs two color clocks ahead of + // the beam, so a sleeping wait releases two color clocks before its + // masked horizontal target (the fetch then lands on the target). let same_line = CopperWait::new(0x5021, 0xFFFE); - assert_eq!(same_line.cck_until_satisfied(0x50, 0x10), Some(0x10)); + assert_eq!(same_line.cck_until_satisfied(0x50, 0x10), Some(0x0E)); let later_line = CopperWait::new(0x5101, 0xFFFE); assert_eq!( diff --git a/src/savestate.rs b/src/savestate.rs index 85ff8e4..e52cc07 100644 --- a/src/savestate.rs +++ b/src/savestate.rs @@ -88,7 +88,10 @@ const STATE_MAGIC: &[u8; 8] = b"CLSSTATE"; // irq_pin_delay_eticks - the 8520 one-E-cycle interrupt delay) // 25: Blitter gained the early-dropping DMACONR BBUSY flag (bbusy) and // Bus the one-cck INTREQ.BLIT raise delay (blit_irq_delay_cck) -pub const STATE_VERSION: u32 = 25; +// 26: Copper gained the deferred SKIP decision (skip_eval - the condition +// sampled at the next instruction's first-word fetch) and the COPJMP +// strobe tail state (CopperState::Jumping, COP_JMP1/COP_JMP2) +pub const STATE_VERSION: u32 = 26; /// Default state file name, timestamped like the screenshot/recorder names. pub fn auto_filename() -> std::path::PathBuf { diff --git a/src/video/bitplane.rs b/src/video/bitplane.rs index 86e2e85..ad7a051 100644 --- a/src/video/bitplane.rs +++ b/src/video/bitplane.rs @@ -63,6 +63,15 @@ const DIW_HSTART_FETCH_REFERENCE_HIRES: i32 = 0x82; // Register/copper-write x=0 anchor, in colour clocks. Moved left by 8 colour // clocks in lockstep with DIW_HSTART_FB0 (16 lo-res pixels) so register writes // 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. 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, @@ -81,7 +90,11 @@ const COPPER_WAIT_HPOS_FB0: i32 = 0x28; /// STOP before retuning this. If a scene's colours or copper-driven picture /// look horizontally shifted, the cause is usually bitplane fetch/DDF /// alignment, sprite arming, or a missed write-domain delay, not this final -/// colour-output anchor. +/// colour-output anchor. The anchor maps events recorded at their +/// Denise-effective position (bus landing plus the write-effect delay, see +/// `Bus::record_render_write`), so it did not move when the Copper WAIT +/// comparator lookahead fix moved copper bus landings four colour clocks +/// earlier. const COLOR_WRITE_HPOS_FB0: i32 = 0x35; /// Denise's texture/output line starts at the hblank-start counter value. Beam /// positions before this are the wrapped tail of the previous output line, so diff --git a/src/video/bitplane/tests.rs b/src/video/bitplane/tests.rs index 2bbd98a..0c06f42 100644 --- a/src/video/bitplane/tests.rs +++ b/src/video/bitplane/tests.rs @@ -5951,9 +5951,11 @@ fn color00_overscan_write_does_not_backfill_row_start() { &mut manual_bpl_segments, ); - assert_eq!(palette_segments[0][0].x, 60); + let x_on = color_write_framebuffer_x(68); + let x_off = color_write_framebuffer_x(76); + assert_eq!(palette_segments[0][0].x, x_on); assert_eq!(palette_segments[0][0].value, 0x087A); - assert_eq!(palette_segments[0][1].x, 92); + assert_eq!(palette_segments[0][1].x, x_off); assert_eq!(palette_segments[0][1].value, 0x0000); assert_eq!(base_palettes[0][0], 0x0000); @@ -5967,10 +5969,10 @@ fn color00_overscan_write_does_not_backfill_row_start() { ); assert_eq!(fb[0], rgb12_to_rgba8(0x0000)); - assert_eq!(fb[59], rgb12_to_rgba8(0x0000)); - assert_eq!(fb[60], rgb12_to_rgba8(0x087A)); - assert_eq!(fb[91], rgb12_to_rgba8(0x087A)); - assert_eq!(fb[92], rgb12_to_rgba8(0x0000)); + assert_eq!(fb[x_on - 1], rgb12_to_rgba8(0x0000)); + assert_eq!(fb[x_on], rgb12_to_rgba8(0x087A)); + assert_eq!(fb[x_off - 1], rgb12_to_rgba8(0x087A)); + assert_eq!(fb[x_off], rgb12_to_rgba8(0x0000)); } #[test]