diff --git a/docs/debugger/headless.md b/docs/debugger/headless.md index 4bb1ccf..fabb802 100644 --- a/docs/debugger/headless.md +++ b/docs/debugger/headless.md @@ -116,8 +116,9 @@ authoritative list. The most useful ones: Timing-model knobs that pair well with the debugger: -- `COPPERLINE_IRQ_LATENCY_CCK=N` -- override the modelled 68000 - interrupt-recognition latency (default 65 colour clocks; `0` disables). +- `COPPERLINE_IRQ_LATENCY_CCK=N` -- override the modelled Paula INTREQ-to- + IPL-pin pipe (default 5 colour clocks; `0` also disables the 68000 + boundary-sampling delay, delivering interrupts immediately). - `COPPERLINE_DBG_AFTER=SECS` / `COPPERLINE_DBG_UNTIL=SECS` -- bound debugger and renderer diagnostics to an emulated-time window. Renderer diagnostics parse these bounds once when their diagnostic option is first diff --git a/docs/index.md b/docs/index.md index c2c4a90..bf04a70 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,8 +15,8 @@ written in Rust. Cycle-driven means the whole machine -- the 68000-family CPU, Agnus, Denise, Paula, the CIAs, the floppy subsystem, and the chip bus that ties them together -- advances on one colour-clock timeline: the chip bus is arbitrated per colour clock, the Copper and blitter are scheduled -per DMA slot with the hardware bus sequences, and 68000 -interrupt-recognition latency is modelled. That timing model is what lets +per DMA slot with the hardware bus sequences, and the Paula IPL pipe plus +68000 interrupt sampling are modelled. That timing model is what lets it run the current cycle-sensitive OCS and AGA regression set, as well as Kickstart, Workbench, games, and CDTV/CD32 titles. diff --git a/docs/internals/chipset.md b/docs/internals/chipset.md index 4890cd5..08cac3f 100644 --- a/docs/internals/chipset.md +++ b/docs/internals/chipset.md @@ -112,7 +112,7 @@ scoped. ECS adds BLTSIZV/BLTSIZH for larger blits. ## Paula (`paula.rs`) Paula owns the interrupt system (INTENA/INTREQ, delivered through the -modelled 68000 recognition latency), serial, and audio: +modelled IPL-pin pipe and 68000 boundary sampling), serial, and audio: - **Audio**: four DMA channels, each with location/length/period/volume, a period accumulator clocked at CCK rate, and the hardware's one-word diff --git a/docs/internals/cpu.md b/docs/internals/cpu.md index 8131a76..e7b62a8 100644 --- a/docs/internals/cpu.md +++ b/docs/internals/cpu.md @@ -319,7 +319,7 @@ every access re-walks the tables. ## Interrupts and STOP Paula's INTENA/INTREQ levels are delivered as M68K autovectors through the -modelled recognition latency described in [](timing). When the CPU +modelled IPL pipe and boundary sampling described in [](timing). When the CPU executes `STOP`, the frame loop fast-forwards device time to the next event that can raise an interrupt instead of spinning -- behaviour the debugger's Step control inherits. diff --git a/docs/internals/timing.md b/docs/internals/timing.md index 6ebea0d..5e0bcd2 100644 --- a/docs/internals/timing.md +++ b/docs/internals/timing.md @@ -253,8 +253,12 @@ the same slot-eligibility primitive the live engine uses. ### Per-slot FSM Scheduled normal blits use explicit phases matching the hardware -controller: a one-slot BBUSY start delay, an INIT slot, source slots -A/B/C/D, and E/F flush slots for the delayed D holding register. The +controller: three internal startup slots (the BLTSIZE register-commit +cycle plus the two bus-arbitration startup cycles real Agnus spends +before the micro-program begins, verified with a BLTSIZE-to-blitter-IRQ +beam probe against vAmiga), a one-slot BBUSY start delay, an INIT slot, +source slots A/B/C/D, and E/F flush slots for the delayed D holding +register. The source cadence follows the enabled-channel speed table: A is always visited, B only when enabled, C when enabled (USEC) *or* in fill mode (an idle C slot, no bus access), and D when D is enabled or no C next-word @@ -372,25 +376,36 @@ Hardware](https://www.theflatnet.de/pub/cbm/amiga/AmigaDevDocs/hard_6.html). ## Interrupt-recognition latency -A 68000 does not enter an exception the moment INTREQ rises; recognition -plus the exception sequence takes roughly 60-100 CCK on real hardware. -Copperline models this with a configurable latency on newly-raised -interrupt levels (`DEFAULT_IRQ_LATENCY_CCK = 65`, `src/bus.rs`; -`COPPERLINE_IRQ_LATENCY_CCK` overrides, `0` disables). -The delay is attached to asynchronous Paula/CIA/blitter/Copper source -assertions. A CPU write that merely changes INTENA/INTREQ masking or -acknowledges a latch normally only updates the delayed-bit bookkeeping. PORTS -is level-fed by CIA-A/Gayle-style INT2 sources and remains immediately visible -when software unmasks an already-latched level; other newly exposed latched -sources are treated as freshly-present CPU IPL inputs and still pass through -recognition latency. - -This matters more than it sounds: a beam-bounded interrupt handler that -arrives 50 CCK early steals that time from the main loop every frame. The -canonical regression was a scene player running at half speed because -too-early vertical-blank IRQs truncated the depacker's per-frame slice; the -latency model fixed it, confirmed against real hardware with the timing-test -disk. +A 68000 does not enter an exception the moment INTREQ rises. Two hardware +mechanisms sit between the two events, and Copperline models both: + +1. **The Paula IPL pipe.** A change to the enabled-pending interrupt set + reaches the CPU's IPL pins only after a few chip clocks of pipelining + inside Paula (`DEFAULT_IRQ_LATENCY_CCK = 5`, `src/bus.rs`; + `COPPERLINE_IRQ_LATENCY_CCK` overrides, `0` disables both mechanisms). + The delay is attached to asynchronous Paula/CIA/blitter/Copper source + assertions. A CPU write that merely changes INTENA/INTREQ masking or + acknowledges a latch normally only updates the delayed-bit bookkeeping. + PORTS is level-fed by CIA-A/Gayle-style INT2 sources and remains + immediately visible when software unmasks an already-latched level; other + newly exposed latched sources are treated as freshly-present CPU IPL + inputs and still pass through the pipe. INTREQR reads are never delayed: + the pipe sits between the level encoder and the pins, not on the register. +2. **Boundary sampling.** The CPU latches its IPL pins during bus cycles, + and the take-interrupt decision at an instruction boundary uses the level + latched at the *previous* instruction's last bus access + (`CpuBus::sample_ipl`, `src/cpu.rs`). A level that rises during an + instruction's trailing internal cycles is therefore recognised one + instruction later, exactly as on silicon. + +Together these reproduce the raise-to-handler-entry positions measured +against vAmiga (and the vAmigaTS real-A500 photos) across VERTB and +copper-poked INTREQ sources under a range of foreground loops; the residual +is 0..+7 CCK of per-instruction IPL poll-point detail the vendored core does +not model. An earlier revision used a blanket 65 CCK "recognition latency" +calibrated against timing-test row 19 with a mis-decoded VHPOSR (the low +byte is the CCK position, not CCK/2); that delivered every interrupt ~50 CCK +late and dominated the vAmigaTS cputim/irqtim divergence. ## Real-time pacing diff --git a/docs/zorro.md b/docs/zorro.md index 52b4802..3672766 100644 --- a/docs/zorro.md +++ b/docs/zorro.md @@ -126,7 +126,7 @@ manifest capabilities; importing one that was not granted fails to load): Interrupt lines are level-sensitive and polled, exactly like the in-tree boards: a plugin holds `int2`/`int6` non-zero while the line is asserted, and -the bus applies the 68000 interrupt-recognition latency automatically -- the +the bus applies the interrupt-delivery pipeline automatically -- the plugin never pulses INTREQ. Plugins can be written in any language that targets `wasm32` (Rust, C, Zig, diff --git a/src/bus.rs b/src/bus.rs index 1ccc3bd..9ee7158 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -114,16 +114,28 @@ fn external_access_cck_x100_setting() -> u32 { } } -/// 68000/Amiga interrupt-recognition latency in color clocks (DEFAULT ON). -/// Real hardware takes ~96-100 cck from an interrupt request to the handler's -/// first instruction; Copperline's bare model took ~48 (finish-instruction + the -/// 44-cycle exception only), i.e. it delivered interrupts ~50 cck too early. -/// The timing-test rows 19 (handler entry) and 22 (raise position), run on -/// FS-UAE and vAmiga, localised the gap to recognition latency (the raise -/// position matches; only the raise->entry time differed). Default 65 cck makes -/// row 19 match real HW (~hpos 116 vs vAmiga 114 / FS-UAE 122). Set -/// COPPERLINE_IRQ_LATENCY_CCK to override (0 disables = the old behaviour). -const DEFAULT_IRQ_LATENCY_CCK: u32 = 65; +/// Paula INTREQ/INTENA -> CPU IPL-pin propagation delay in color clocks +/// (DEFAULT ON). A change to the enabled-pending interrupt set does not reach +/// the 68000's IPL pins combinationally: Paula pipelines the encoded level to +/// the pins over a few chip clocks (vAmiga models the same pipe as iplPipe, +/// with the pin taking the new value ~4 DMA cycles after the level change). +/// Together with the IPL sampling model in cpu.rs (the interrupt decision at +/// an instruction boundary uses the level sampled at the PREVIOUS instruction's +/// last bus access, as on the real 68000), this reproduces the raise-to-entry +/// latency measured against vAmiga and the vAmigaTS real-hardware photos. +/// +/// History: this was 65 for a while, calibrated against timing-test row 19 +/// with a mis-decoded VHPOSR (the low byte is the cck position, not cck/2); +/// that made every IRQ delivery ~50 cck late and dominated the vAmigaTS +/// cputim/irqtim/inttim divergence. Set COPPERLINE_IRQ_LATENCY_CCK to override +/// (0 disables the pipe AND the boundary-sampling delay = raw model). +/// +/// 5 = the ~4-cycle pin pipe plus the one-cck register-change commit a poked +/// INTREQ takes before the level encoder sees it (folded into one constant; +/// an IRQ-latency probe against vAmiga across seven source/loop geometries +/// lands within 0..+7 cck, the residual being per-instruction IPL poll-point +/// detail the vendored core does not model). +const DEFAULT_IRQ_LATENCY_CCK: u32 = 5; /// Read the COPPERLINE_IRQ_LATENCY_CCK setting once, at bus construction (stored in /// `irq_latency_setting`). Unset uses DEFAULT_IRQ_LATENCY_CCK; 0 disables. @@ -705,21 +717,20 @@ pub struct Bus { pub(crate) delivered_copper_irq_beam: Option<(u32, u32)>, coper_cpu_irq_delay_cck: u32, - /// General 68000 interrupt-recognition latency (COPPERLINE_IRQ_LATENCY_CCK). - /// Real HW takes ~96-100 cck from a VERTB request to the handler's first - /// instruction; Copperline's bare model takes ~48 (finish-instruction + the - /// 44-cycle exception only). The timing-test rows 19 (handler entry) and 22 - /// (raise position) localised the ~50 cck gap to interrupt RECOGNITION - /// latency, not the raise position. When the setting is non-zero, a newly + /// Paula INTREQ -> CPU IPL-pin propagation pipe (COPPERLINE_IRQ_LATENCY_CCK, + /// see DEFAULT_IRQ_LATENCY_CCK). When the setting is non-zero, a newly /// raised maskable interrupt is held invisible to the CPU for that many cck /// (`irq_latency_mask` = the delayed bits, `irq_latency_cck` = countdown, /// `irq_latency_last_pending` = previous pending set for rising-edge detect). + /// INTREQR reads are NOT delayed (the pipe sits between the level encoder + /// and the pins, not on the register). irq_latency_cck: u32, irq_latency_mask: u16, irq_latency_last_pending: u16, - /// Configured recognition latency in cck (from COPPERLINE_IRQ_LATENCY_CCK or the - /// default); 0 disables the model. A field (not a global) so tests can set - /// it per-instance -- mechanism tests run with 0 to deliver IRQs immediately. + /// Configured IPL-pipe length in cck (from COPPERLINE_IRQ_LATENCY_CCK or the + /// default); 0 disables the pipe and the cpu.rs boundary-sampling delay. A + /// field (not a global) so tests can set it per-instance -- mechanism tests + /// run with 0 to deliver IRQs immediately. pub(crate) irq_latency_setting: u32, /// Palette snapshots written by CPU interrupt handlers. The top diff --git a/src/bus/tests.rs b/src/bus/tests.rs index 7fa77be..f298d3b 100644 --- a/src/bus/tests.rs +++ b/src/bus/tests.rs @@ -374,7 +374,7 @@ fn bus_with_pending_two_word_a_to_d_blit() -> Bus { assert!(!bus.custom_write(0x058, 2, ((1 << 6) | 2) as u64)); assert!(bus.blitter.busy); - assert_eq!(bus.next_blitter_completion_cck(), Some(8)); + assert_eq!(bus.next_blitter_completion_cck(), Some(11)); bus } @@ -1569,8 +1569,8 @@ fn copper_wait_with_bfd_caps_to_blitter_completion_after_position_match() { bus.blitter.bltcon0 = 0x0100; bus.blitter.start_scheduled((1 << 6) | 1, &bus.mem.chip_ram); - assert_eq!(bus.next_blitter_completion_cck(), Some(6)); - assert_eq!(bus.next_copper_wakeup_cck(), Some(6)); + assert_eq!(bus.next_blitter_completion_cck(), Some(9)); + assert_eq!(bus.next_copper_wakeup_cck(), Some(9)); } #[test] @@ -1589,9 +1589,9 @@ fn copper_wait_with_bfd_clear_resumes_after_busy_blitter_finishes() { bus.blitter.bltcon0 = 0x0100; bus.blitter.start_scheduled((1 << 6) | 1, &bus.mem.chip_ram); - assert_eq!(bus.next_copper_wakeup_cck(), Some(6)); + assert_eq!(bus.next_copper_wakeup_cck(), Some(9)); - bus.advance_chipset(5); + bus.advance_chipset(8); assert!(bus.copper.waiting().is_some()); assert_eq!(bus.denise.palette[0], 0); @@ -1603,10 +1603,11 @@ fn copper_wait_with_bfd_clear_resumes_after_busy_blitter_finishes() { assert_eq!(bus.denise.palette[0], 0); // Once the blitter frees the bus, the Copper spends a dummy wake-up - // cycle, then its MOVE fetch writes at hpos 0x2C. - bus.advance_chipset(4); + // cycle, then its MOVE fetches land on the even copper slots and the + // write appears at hpos 0x30. + bus.advance_chipset(8); assert_eq!(bus.denise.palette[0], 0x0555); - assert_eq!(bus.current_render_events()[0].hpos, 0x2C); + assert_eq!(bus.current_render_events()[0].hpos, 0x30); } #[test] @@ -1877,7 +1878,7 @@ fn blitter_completion_deadline_skips_fixed_dma_slots() { bus.denise.ddfstrt = 0x0038; bus.denise.ddfstop = 0x00D0; bus.agnus.vpos = 0x40; // inside the default vertical display window - bus.agnus.hpos = 0x03D; + bus.agnus.hpos = 0x03A; bus.blitter.bltcon0 = 0x09F0; // A -> D copy: every A/D slot is a bus access bus.blitter.bltafwm = 0xFFFF; bus.blitter.bltalwm = 0xFFFF; @@ -1886,16 +1887,17 @@ fn blitter_completion_deadline_skips_fixed_dma_slots() { write_chip_word(&mut bus, 0x10, 0x1234); bus.blitter.start_scheduled((1 << 6) | 1, &bus.mem.chip_ram); - assert_eq!(bus.blitter.scheduled_slots_remaining(), Some(6)); - // Two internal lead-in cycles elapse at 0x3D/0x3E regardless of DMA, - // then the A fetch must skip the plane-1 bitplane fetch slot at 0x3F - // (granted at 0x40), D writes at 0x41, the internal E cycle passes at - // 0x42, and the final F write lands at 0x43: 7 color clocks in all. - assert_eq!(bus.next_blitter_completion_cck(), Some(7)); + assert_eq!(bus.blitter.scheduled_slots_remaining(), Some(9)); + // Five internal lead-in cycles elapse at 0x3A-0x3E regardless of DMA + // (the BLTSIZE commit + startup extras plus StartDelay/Init), then the + // A fetch must skip the plane-1 bitplane fetch slot at 0x3F (granted at + // 0x40), D writes at 0x41, the internal E cycle passes at 0x42, and the + // final F write lands at 0x43: 10 color clocks in all. + assert_eq!(bus.next_blitter_completion_cck(), Some(10)); // After the internal lead-in the blit's A access is pending and the // bitplane fetch owns its fixed slot. - bus.advance_chipset(2); + bus.advance_chipset(5); assert!(bus.blitter.busy); assert_eq!(bus.scheduled_dma_owner(false), ChipBusOwner::Bitplane); @@ -1929,17 +1931,17 @@ fn blitter_completion_deadline_accounts_for_copper_dma_slots() { write_chip_word(&mut bus, 0x10, 0x1234); bus.blitter.start_scheduled((1 << 6) | 1, &bus.mem.chip_ram); - assert_eq!(bus.blitter.scheduled_slots_remaining(), Some(6)); + assert_eq!(bus.blitter.scheduled_slots_remaining(), Some(9)); assert_eq!(bus.scheduled_dma_owner(false), ChipBusOwner::Copper); - // Two internal lead-in cycles pass under the Copper's fetches at - // 0x20/0x21, then the A and D accesses take the Copper's idle halves - // (0x23, 0x25), the internal E cycle passes at 0x26, and the final F - // write lands at 0x27: 8 color clocks in all. - assert_eq!(bus.next_blitter_completion_cck(), Some(8)); + // Five internal lead-in cycles pass under the Copper's fetches at + // 0x20-0x24, then the A and D accesses take the Copper's idle halves + // (0x25, 0x27), the internal E cycle passes at 0x28, and the final F + // write lands at 0x29: 10 color clocks in all. + assert_eq!(bus.next_blitter_completion_cck(), Some(10)); bus.advance_chipset(6); assert!(bus.blitter.busy); - assert_eq!(bus.next_blitter_completion_cck(), Some(2)); + assert_eq!(bus.next_blitter_completion_cck(), Some(4)); bus.advance_chipset(4); assert!(!bus.blitter.busy); @@ -7773,12 +7775,12 @@ fn bltsize_starts_dma_and_preempts_cpu_slice_without_irq_preempt() { assert!(bus.slice_preempted); assert_eq!(bus.paula.intreq & INT_BLIT, 0); assert!(bus.blitter.busy); - assert_eq!(bus.next_blitter_completion_cck(), Some(6)); + assert_eq!(bus.next_blitter_completion_cck(), Some(9)); bus.advance_chipset(1); assert_eq!(bus.paula.intreq & INT_BLIT, 0); assert!(bus.blitter.busy); - assert_eq!(bus.next_blitter_completion_cck(), Some(5)); - bus.advance_chipset(5); + assert_eq!(bus.next_blitter_completion_cck(), Some(8)); + bus.advance_chipset(8); assert_ne!(bus.paula.intreq & INT_BLIT, 0); assert!(!bus.blitter.busy); assert_eq!(bus.next_blitter_completion_cck(), None); @@ -7878,7 +7880,7 @@ fn busy_bltcon1_line_bit_write_updates_register_without_reinterpreting_pipeline_ assert_eq!(bus.blitter.bltcon1, 0x0001); assert_eq!(bus.paula.intreq & INT_BLIT, 0); - bus.advance_chipset(8); + bus.advance_chipset(11); assert!(!bus.blitter.busy); assert_eq!(&bus.mem.chip_ram[0x20..0x24], &[0x11, 0x11, 0x22, 0x22]); assert_ne!(bus.paula.intreq & INT_BLIT, 0); @@ -7909,9 +7911,9 @@ fn busy_bltsize_write_finishes_current_blit_then_starts_replacement() { // interrupt request: INTREQ.BLIT means "the last started blit has // finished", and the replacement has not finished yet. assert_eq!(bus.paula.intreq & INT_BLIT, 0); - assert_eq!(bus.next_blitter_completion_cck(), Some(6)); + assert_eq!(bus.next_blitter_completion_cck(), Some(9)); - bus.advance_chipset(6); + bus.advance_chipset(9); assert!(!bus.blitter.busy); assert_eq!(&bus.mem.chip_ram[0x24..0x26], &[0x33, 0x33]); // The replacement blit's completion raises the request. @@ -7935,8 +7937,8 @@ fn busy_blitter_dmacon_clear_gates_dma_without_finishing_pending_blit() { assert_eq!(&bus.mem.chip_ram[0x20..0x24], &[0, 0, 0, 0]); assert!(!bus.custom_write(0x096, 2, (0x8000 | DMACON_BLTEN) as u64)); - assert_eq!(bus.next_blitter_completion_cck(), Some(8)); - bus.advance_chipset(8); + assert_eq!(bus.next_blitter_completion_cck(), Some(11)); + bus.advance_chipset(11); assert!(!bus.blitter.busy); assert_eq!(&bus.mem.chip_ram[0x20..0x24], &[0x11, 0x11, 0x22, 0x22]); @@ -7977,9 +7979,9 @@ fn blithog_clear_busy_blitter_yields_to_cpu_only_after_starvation() { write_chip_word(&mut bus, 0x34, 0x7777); write_chip_word(&mut bus, 0x36, 0x8888); bus.blitter.start_scheduled((1 << 6) | 4, &bus.mem.chip_ram); - // Walk the blit past its two internal lead-in cycles (those are + // Walk the blit past its five internal lead-in cycles (those are // CPU-available) so its pending slot is an A-channel bus access. - bus.advance_chipset(2); + bus.advance_chipset(5); let initial_slots = bus.blitter.scheduled_slots_remaining(); bus.set_cpu_bus_arbitration_enabled(true); bus.begin_cpu_slice(); @@ -7998,7 +8000,7 @@ fn blithog_clear_busy_blitter_yields_to_cpu_only_after_starvation() { assert_eq!(bus.paula.intreq & INT_BLIT, 0); assert_eq!( bus.agnus.hpos, - 0x22 + u32::from(BLITTER_SLOWDOWN_CPU_MISS_LIMIT) + 2 + 0x25 + u32::from(BLITTER_SLOWDOWN_CPU_MISS_LIMIT) + 2 ); // The granted slot was the CPU's; the trailing bus-free tail cck is // reclaimed by the still-busy blitter, so it is the last chip-bus owner. @@ -8012,9 +8014,9 @@ fn blithog_clear_bls_count_yields_blitter_priority_slot_to_cpu() { bus.agnus.hpos = 0x22; bus.blitter.bltcon0 = 0x09F0; bus.blitter.start_scheduled((1 << 6) | 1, &bus.mem.chip_ram); - // Walk the blit past its two internal lead-in cycles (those are + // Walk the blit past its five internal lead-in cycles (those are // CPU-available) so its pending slot is an A-channel bus access. - bus.advance_chipset(2); + bus.advance_chipset(5); assert_eq!(bus.scheduled_dma_owner(true), ChipBusOwner::Blitter); @@ -8070,7 +8072,7 @@ fn ecs_bltcon1_doff_suppresses_destination_writes_but_advances_pointer() { assert!(!bus.custom_write(0x042, 2, BLTCON1_DOFF as u64)); assert!(!bus.custom_write(0x058, 2, ((1 << 6) | 1) as u64)); - bus.advance_chipset(2); + bus.advance_chipset(5); assert!(bus.blitter.busy); assert_eq!(&bus.mem.chip_ram[0x20..0x22], &[0xAA, 0xAA]); assert_eq!(bus.blitter.bltdpt, 0x20); @@ -8540,9 +8542,9 @@ fn bltpri_stalls_cpu_chip_access_through_blitter_access_cycles() { bus.mem.chip_ram[0x10] = 0x12; bus.mem.chip_ram[0x11] = 0x34; bus.blitter.start_scheduled((1 << 6) | 1, &bus.mem.chip_ram); - // Walk the blit past its two internal lead-in cycles so its pending + // Walk the blit past its five internal lead-in cycles so its pending // slot is the A-channel access. - bus.advance_chipset(2); + bus.advance_chipset(5); bus.set_cpu_bus_arbitration_enabled(true); bus.grant_cpu_bus_access(2, CpuBusAccessKind::Read); @@ -8592,9 +8594,9 @@ fn blithog_set_blocks_cpu_slowdown_back_pressure_until_blitter_finishes() { write_chip_word(&mut bus, 0x34, 0x7777); write_chip_word(&mut bus, 0x36, 0x8888); bus.blitter.start_scheduled((1 << 6) | 4, &bus.mem.chip_ram); - // Walk the blit past its two internal lead-in cycles so its pending + // Walk the blit past its five internal lead-in cycles so its pending // slot is the first A-channel access. - bus.advance_chipset(2); + bus.advance_chipset(5); bus.set_cpu_bus_arbitration_enabled(true); bus.grant_cpu_bus_access(2, CpuBusAccessKind::Read); diff --git a/src/chipset/blitter.rs b/src/chipset/blitter.rs index a52ce19..24ea7e3 100644 --- a/src/chipset/blitter.rs +++ b/src/chipset/blitter.rs @@ -229,6 +229,16 @@ struct NormalBlitState { #[serde(skip)] debug_watched_write: Option<(u32, u16)>, phase: NormalBlitPhase, + /// Extra internal start slots before Init. Real Agnus takes the BLTSIZE + /// write through a one-cycle register commit and then two bus-arbitration + /// startup cycles (vAmiga BLT_STRT1/BLT_STRT2) plus a cycle to begin the + /// micro-program, so the first channel slot runs ~4 cck after the poke; + /// StartDelay alone modelled only one. Verified against a + /// BLTSIZE-to-blitter-IRQ beam probe cross-checked with vAmiga. + /// TODO: STRT1/STRT2 on real hardware need free odd bus slots, so heavy + /// contention stretches the startup; these extras are plain internal + /// cycles. + start_extra: u32, slots_remaining: u32, h_remaining: u32, w: u32, @@ -1151,6 +1161,7 @@ impl NormalBlitState { debug_watch_addrs: blitter.debug_watch_addrs.clone(), debug_watched_write: None, phase: NormalBlitPhase::StartDelay, + start_extra: NORMAL_START_EXTRA_SLOTS, slots_remaining: normal_total_slots(h, w, con0, con1), h_remaining: h, w, @@ -1256,6 +1267,7 @@ impl NormalBlitState { let mut mask = 0u64; let mut phase = self.phase; + let mut start_extra = self.start_extra; let mut pipeline_full = self.pipeline_full; let mut word_idx = self.word_idx; let mut h_remaining = self.h_remaining; @@ -1277,7 +1289,13 @@ impl NormalBlitState { } match phase { - NormalBlitPhase::StartDelay => phase = NormalBlitPhase::Init, + NormalBlitPhase::StartDelay => { + if start_extra > 0 { + start_extra -= 1; + } else { + phase = NormalBlitPhase::Init; + } + } NormalBlitPhase::Init => phase = NormalBlitPhase::A, NormalBlitPhase::A => { phase = if self.use_b { @@ -1354,7 +1372,13 @@ impl NormalBlitState { fn process_phase(&mut self, ram: &mut [u8], bzero: &mut bool) { match self.phase { - NormalBlitPhase::StartDelay => self.phase = NormalBlitPhase::Init, + NormalBlitPhase::StartDelay => { + if self.start_extra > 0 { + self.start_extra -= 1; + } else { + self.phase = NormalBlitPhase::Init; + } + } NormalBlitPhase::Init => self.phase = NormalBlitPhase::A, NormalBlitPhase::A => { self.begin_word(); @@ -1635,13 +1659,20 @@ fn normal_source_slots_per_word(con0: u16, con1: u16) -> u32 { 1 + u32::from(use_b) + u32::from(c_phase) + u32::from(d_phase) } +/// Extra internal slots between the BLTSIZE write and the Init slot: the +/// register-commit cycle plus the two BLT_STRT startup cycles and the +/// micro-program begin cycle real Agnus takes (see NormalBlitState). +const NORMAL_START_EXTRA_SLOTS: u32 = 3; + fn normal_total_slots(h: u32, w: u32, con0: u16, con1: u16) -> u32 { let words = h.saturating_mul(w); if words == 0 { - return 1; + return 1 + NORMAL_START_EXTRA_SLOTS; } let use_d = con0 & BLTCON0_USE_D != 0; - 2 + words.saturating_mul(normal_source_slots_per_word(con0, con1)) + if use_d { 2 } else { 0 } + 2 + NORMAL_START_EXTRA_SLOTS + + words.saturating_mul(normal_source_slots_per_word(con0, con1)) + + if use_d { 2 } else { 0 } } fn line_total_slots(npixels: u32) -> u32 { @@ -2205,8 +2236,12 @@ mod tests { b.start_scheduled(bltsize, &ram); assert!(b.busy); - assert_eq!(b.scheduled_slots_remaining(), Some(8)); + assert_eq!(b.scheduled_slots_remaining(), Some(11)); assert_eq!(&ram[0x20..0x24], &[0xAA, 0xAA, 0xAA, 0xAA]); + // Walk the three startup extras (BLTSIZE commit + BLT_STRT cycles). + for _ in 0..3 { + assert!(!b.tick_scheduled_slot(&mut ram)); + } assert!(!b.tick_scheduled_slot(&mut ram)); assert_eq!(b.scheduled_slots_remaining(), Some(7)); assert!(b.busy); @@ -2250,9 +2285,10 @@ mod tests { pattern } - // D-only clear, 1 row x 2 words: StartDelay, Init, [A D] x2, E, F. - // Only the D write slots and the final F flush access the bus; the A - // slots are idle because A DMA is disabled (HRM: "- D0" per word). + // D-only clear, 1 row x 2 words: startup extras + StartDelay, Init, + // [A D] x2, E, F. Only the D write slots and the final F flush access + // the bus; the A slots are idle because A DMA is disabled (HRM: + // "- D0" per word). let mut ram = vec![0xAAu8; 256]; let mut b = Blitter::new(); b.bltcon0 = BLTCON0_USE_D; // minterm $00 clears @@ -2260,7 +2296,7 @@ mod tests { b.start_scheduled((1u16 << 6) | 2, &ram); assert_eq!( needs_bus_walk(&mut b, &mut ram), - [false, false, false, true, false, true, false, true] + [false, false, false, false, false, false, true, false, true, false, true] ); // A->D copy, 1 row x 2 words: the first D phase is the delayed-D @@ -2276,7 +2312,7 @@ mod tests { b.start_scheduled((1u16 << 6) | 2, &ram); assert_eq!( needs_bus_walk(&mut b, &mut ram), - [false, false, true, false, true, true, false, true] + [false, false, false, false, false, true, false, true, true, false, true] ); // ABCD cookie-cut, 1 row x 2 words: A/B/C fetch first, then the @@ -2293,7 +2329,10 @@ mod tests { b.start_scheduled((1u16 << 6) | 2, &ram); assert_eq!( needs_bus_walk(&mut b, &mut ram), - [false, false, true, true, true, false, true, true, true, true, false, true] + [ + false, false, false, false, false, true, true, true, false, true, true, true, true, + false, true + ] ); // Line blit, 2 pixels: StartDelay, Init, then [L1 L2 L3 L4] per pixel. @@ -2331,6 +2370,9 @@ mod tests { b.bltdpt = 0x20; b.start_scheduled((1u16 << 6) | 1, &ram); + for _ in 0..3 { + assert!(!b.tick_scheduled_slot(&mut ram)); // startup extras + } assert!(!b.tick_scheduled_slot(&mut ram)); // BBUSY start delay. assert!(!b.tick_scheduled_slot(&mut ram)); // INIT. assert!(!b.tick_scheduled_slot(&mut ram)); // A slot is idle when A DMA is disabled. @@ -2357,12 +2399,12 @@ mod tests { b.bltdpt = 0x20; b.start_scheduled((1u16 << 6) | 1, &ram); - assert_eq!(b.scheduled_slots_remaining(), Some(6)); + assert_eq!(b.scheduled_slots_remaining(), Some(9)); assert!(!b.tick_scheduled_slot(&mut ram)); assert_eq!(b.bltapt, 0x10); assert_eq!(read_word(&ram, 0x20), 0); - assert_eq!(b.scheduled_slots_remaining(), Some(5)); + assert_eq!(b.scheduled_slots_remaining(), Some(8)); } #[test] @@ -2377,7 +2419,10 @@ mod tests { b.bltdpt = 0x20; b.start_scheduled((1u16 << 6) | 1, &ram); - assert_eq!(b.scheduled_slots_remaining(), Some(4)); + assert_eq!(b.scheduled_slots_remaining(), Some(7)); + for _ in 0..3 { + assert!(!b.tick_scheduled_slot(&mut ram)); // startup extras + } assert!(!b.tick_scheduled_slot(&mut ram)); // BBUSY start delay. assert!(!b.tick_scheduled_slot(&mut ram)); // INIT. assert!(!b.tick_scheduled_slot(&mut ram)); // A state, empty when A is disabled. @@ -2592,11 +2637,12 @@ mod tests { let fill_total = fill.scheduled_slots_remaining(); let (fill_total_walked, fill_bus) = walk_bus(&mut fill, &mut ram); - // Copy: 2 (start delay + init) + 2 words * 2 cyc/word + 2 (D flush) = 8. - assert_eq!(copy_total, 8); - // Fill: the same, but 3 cyc/word -> 2 + 2*3 + 2 = 10 (two extra slots). - assert_eq!(fill_total, Some(10)); - assert_eq!(fill_total_walked, 10); + // Copy: 3 (startup extras) + 2 (start delay + init) + 2 words * + // 2 cyc/word + 2 (D flush) = 11. + assert_eq!(copy_total, 11); + // Fill: the same, but 3 cyc/word -> 5 + 2*3 + 2 = 13 (two extra slots). + assert_eq!(fill_total, Some(13)); + assert_eq!(fill_total_walked, 13); // The extra fill slots are IDLE: the fill performs the same number of // bus accesses as the copy (the A reads and D writes), just spread over // two more idle cycles. diff --git a/src/cpu.rs b/src/cpu.rs index 749f832..ed88674 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -209,6 +209,14 @@ struct CpuBus { /// folding on a cached fetch stream. Recomputed on every fetch, never /// carried across a save state. last_fetch_cache_hit: bool, + /// IPL level as sampled at the CPU's most recent bus access. The real + /// 68000 latches its IPL pins during bus cycles, and the take-interrupt + /// decision at an instruction boundary uses the value latched during the + /// PREVIOUS instruction's last bus access -- a level that rises during an + /// instruction's trailing internal cycles is not seen until one + /// instruction later. Transient: re-sampled on every access, never + /// serialized (a loaded state re-samples within one instruction). + sampled_irq_level: u8, } pub fn build( @@ -263,6 +271,7 @@ impl M68kMachine { dcache: None, dbg_irq_window: debug_irq_window_setting(), last_fetch_cache_hit: false, + sampled_irq_level: 0, }, hle: NoOpHleHandler, fpu_enabled, @@ -1525,7 +1534,18 @@ impl M68kMachine { // recognized at the correct boundary even within a multi-instruction // core slice. self.bus.bus.flush_timed_devices(); - let level = self.pending_irq_level(); + let mut level = self.pending_irq_level(); + // The boundary decision uses the IPL level latched at the previous + // instruction's last bus access (CpuBus::sample_ipl), not the live + // level: on real silicon a level that rose after that sample is taken + // one instruction later. A STOPped CPU keeps sampling its pins, so it + // wakes on the live level. min() so a level that dropped since the + // sample (e.g. the instruction's own INTREQ clear) is never delivered + // stale. COPPERLINE_IRQ_LATENCY_CCK=0 disables this together with the + // IPL pipe (the raw immediate model, used by mechanism tests). + if self.bus.bus.irq_latency_setting != 0 && !self.cpu.is_stopped() { + level = level.min(self.bus.sampled_irq_level); + } self.cpu.set_irq(level); } @@ -2492,40 +2512,66 @@ impl CpuBus { } } +impl CpuBus { + /// Latch the IPL level presented to the CPU by (the end of) a bus access. + /// The real 68000 samples its IPL pins during bus cycles, and the + /// take-interrupt decision at an instruction boundary uses the value + /// latched during the previous instruction's last bus access -- a level + /// that rises during an instruction's trailing internal cycles is not + /// seen until one instruction later (Moira models the same points as + /// POLL_IPL). `refresh_irq_line` consumes this latch. + #[inline] + fn sample_ipl(&mut self) { + self.sampled_irq_level = if self.bus.paula.intena & INT_MASTER != 0 { + pending_ipl(self.bus.paula.intena & self.bus.cpu_visible_intreq()) + } else { + 0 + }; + } +} + impl AddressBus for CpuBus { fn last_fetch_was_cached(&self) -> bool { self.last_fetch_cache_hit } fn read_byte(&mut self, address: u32) -> u8 { + self.sample_ipl(); self.read_sized(address, 1, CpuBusAccessKind::Read) as u8 } fn read_word(&mut self, address: u32) -> u16 { + self.sample_ipl(); self.read_sized(address, 2, CpuBusAccessKind::Read) as u16 } fn read_long(&mut self, address: u32) -> u32 { + self.sample_ipl(); self.read_sized(address, 4, CpuBusAccessKind::Read) } fn write_byte(&mut self, address: u32, value: u8) { + self.sample_ipl(); self.write_sized(address, 1, u32::from(value)); } fn write_word(&mut self, address: u32, value: u16) { + self.sample_ipl(); self.write_sized(address, 2, u32::from(value)); } fn write_long(&mut self, address: u32, value: u32) { + self.sample_ipl(); self.write_sized(address, 4, value); } fn read_immediate_word(&mut self, address: u32) -> u16 { + self.sample_ipl(); self.read_sized(address, 2, CpuBusAccessKind::Fetch) as u16 } fn read_immediate_long(&mut self, address: u32) -> u32 { + self.sample_ipl(); self.read_sized(address, 4, CpuBusAccessKind::Fetch) } @@ -2993,6 +3039,7 @@ mod tests { dcache: None, dbg_irq_window: None, last_fetch_cache_hit: false, + sampled_irq_level: 0, }; assert_eq!(bus.read_long(0), 0x1111_4EF9); @@ -3017,6 +3064,7 @@ mod tests { dcache: None, dbg_irq_window: None, last_fetch_cache_hit: false, + sampled_irq_level: 0, }; bus.bus.set_cpu_bus_arbitration_enabled(true); // Start on a refresh slot (0x003) so the fetch both waits for and then @@ -3048,6 +3096,7 @@ mod tests { dcache: None, dbg_irq_window: None, last_fetch_cache_hit: false, + sampled_irq_level: 0, }; bus.bus.set_cpu_bus_arbitration_enabled(true); @@ -3259,6 +3308,7 @@ mod tests { dcache: Some(Box::new(dcache)), dbg_irq_window: None, last_fetch_cache_hit: false, + sampled_irq_level: 0, }; bus.bus.set_cpu_bus_arbitration_enabled(true); let fast = FAST_RAM_BASE as u32 + 0x40; @@ -3304,6 +3354,7 @@ mod tests { dcache: None, dbg_irq_window: None, last_fetch_cache_hit: false, + sampled_irq_level: 0, }; let before = bus.read_long(ROM_BASE as u32); bus.write_long(ROM_BASE as u32, 0xDEAD_BEEF); @@ -3327,6 +3378,7 @@ mod tests { dcache: None, dbg_irq_window: None, last_fetch_cache_hit: false, + sampled_irq_level: 0, }; let addr = SLOW_RAM_BASE as u32 + 64 * 1024; // A write into unmapped space still drives the bus: the following @@ -3852,6 +3904,55 @@ mod tests { Ok(()) } + #[test] + fn irq_recognition_uses_previous_instruction_ipl_sample() -> Result<()> { + // The 68000 latches its IPL pins during bus cycles: a level that + // rises AFTER an instruction's last bus access is not seen at the + // next instruction boundary -- it is recognized one instruction + // later, once a bus access has sampled the new level. + let mut bus = test_bus_with_pc(0x0000_0100); + write_program( + &mut bus, + 0x0000_0100, + &[ + 0x46FC, 0x2000, // MOVE #$2000,SR opens the IPL mask + 0x4E71, 0x4E71, 0x4E71, 0x4E71, 0x4E71, 0x4E71, // NOP run + ], + ); + write_program(&mut bus, 0x0000_0200, &[0x7007]); // handler: MOVEQ #7,D0 + set_autovector(&mut bus, 3, 0x0000_0200); + bus.paula.intena = INT_MASTER | INT_VERTB; + // Non-zero setting = the IPL pipe (armed when the device tick sees + // the poked level below) plus the boundary-sampling delay. + bus.irq_latency_setting = 5; + + let mut machine = M68kMachine::new(bus, CpuModel::M68000, false)?; + machine.bus_mut().agnus.hpos = 0x21; + + machine.step_slice(1)?; // MOVE to SR; samples see no pending level + machine.bus_mut().paula.intreq = INT_VERTB; // level 3 rises post-sample + + machine.step_slice(1)?; // boundary sample is stale: must not be taken + assert_eq!( + machine.d(0), + 0, + "IRQ must not be taken before the pipe expires and a bus access \ + samples the new level" + ); + assert_eq!(machine.pc(), 0x0000_0106); + + // The 5-cck pipe expires within the next couple of 2-cck NOPs; the + // first bus access after that samples the level and the following + // boundary takes the interrupt. + machine.step_slice(5)?; + assert_eq!( + machine.d(0), + 7, + "handler must run once pipe + sample elapse" + ); + Ok(()) + } + #[test] fn paula_irq_rte_restores_status_and_return_pc() -> Result<()> { let mut bus = test_bus_with_pc(0x0000_0100); diff --git a/src/savestate.rs b/src/savestate.rs index f5d3fe7..ecb7303 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 = 17; +pub const STATE_VERSION: u32 = 18; /// Default state file name, timestamped like the screenshot/recorder names. pub fn auto_filename() -> std::path::PathBuf {