diff --git a/docs/internals/chipset.md b/docs/internals/chipset.md index 531a226..5667155 100644 --- a/docs/internals/chipset.md +++ b/docs/internals/chipset.md @@ -47,6 +47,18 @@ Alice adds the FMODE wide-fetch latch, which scales the bitplane and sprite fetch quanta (FMODE=0 stays byte-identical to the OCS/ECS slot timing). +The CPU sees the reach too: the motherboard decode (Gary and equivalents) +routes the whole $000000-$1FFFFF window to Agnus, which decodes only as +many address bits as its DRAM reach, so the fitted chip RAM image repeats +across the window. A 512K OCS machine mirrors chip RAM at +$080000/$100000/$180000, and the 8372A (A20 unused) repeats its 1M image +at $100000, while addresses past the fitted RAM inside one image select +no DRAM bank and stay open bus ($080000-$0FFFFF on an 8372A with 512K +fitted). Kickstart's chip sizing detects the wrap and still reports the +fitted size. Action Replay freeze-disk loaders are a regression example: +they park the supervisor stack at $100000 on a 512K OCS machine and rely +on the pushes landing at the top of chip RAM. + Sprite DMA is modelled at the register level, the way the chips work: there is no separate "descriptor" concept. Each channel keeps Agnus-side copies of its SPRxPOS/SPRxCTL words and the vertical comparator values diff --git a/src/cpu.rs b/src/cpu.rs index c334e7d..ca6c8b3 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -6,7 +6,8 @@ use crate::bus::{Bus, CpuBusAccessKind}; use crate::chipset::paula::{pending_ipl, INT_MASTER}; use crate::config::CpuModel; use crate::memory::{ - AUTOCONFIG_BASE, AUTOCONFIG_SIZE, CHIP_RAM_BASE, ROM_BASE, SLOW_RAM_BASE, WCS_BASE, + AUTOCONFIG_BASE, AUTOCONFIG_SIZE, CHIP_RAM_BASE, CHIP_WINDOW_SIZE, ROM_BASE, SLOW_RAM_BASE, + WCS_BASE, }; use anyhow::{anyhow, Result}; use log::{debug, trace}; @@ -2223,7 +2224,7 @@ impl CpuBus { if let Some(off) = self.overlay_rom_offset(addr, size) { return Some(PlainMemRegion::OverlayRom(off)); } - if let Some(off) = region_offset(self.bus.mem.chip_ram.len(), CHIP_RAM_BASE, addr, size) { + if let Some(off) = self.chip_window_offset(addr, size) { return Some(PlainMemRegion::ChipRam(off)); } if let Some((board, off)) = self.bus.mem.zorro.region_at(addr, size) { @@ -2249,6 +2250,26 @@ impl CpuBus { None } + /// CPU-side chip RAM decode. The motherboard address decode (Gary and + /// equivalents) routes the whole $000000-$1FFFFF window to Agnus, and + /// Agnus only decodes as many address bits as its DRAM reach + /// (dma_addr_capability_mask): an OCS 8370/8371 ignores A19/A20, so its + /// 512 KiB image repeats at $080000/$100000/$180000; the 1 MiB 8372A + /// ignores A20, repeating its 1 MiB image at $100000; the 2 MiB + /// 8375/Alice decode the full window with no repeat. Within one image, + /// addresses past the fitted RAM select no DRAM bank and stay open bus + /// (e.g. $080000-$0FFFFF on an 8372A with 512 KiB fitted). Kickstart's + /// chip sizing copes: it detects the wrap by checking whether a probe + /// write aliased the bottom of RAM. + fn chip_window_offset(&self, addr: u32, size: usize) -> Option { + let len = self.bus.mem.chip_ram.len(); + if len == 0 || u64::from(addr) >= CHIP_WINDOW_SIZE { + return None; + } + let off = (addr & self.bus.agnus.revision().dma_addr_capability_mask()) as usize; + (off.checked_add(size)? <= len).then_some(off) + } + /// Canonical custom-register page offset for a CPU access that the /// motherboard address decode routes to the custom chips: the canonical /// $DFF000 page on every machine, plus the mirrors Gary's coarse decode @@ -2464,7 +2485,7 @@ impl CpuBus { if self.overlay_rom_offset(addr, 1).is_some() { return false; } - region_offset(self.bus.mem.chip_ram.len(), CHIP_RAM_BASE, addr, 1).is_some() + self.chip_window_offset(addr, 1).is_some() || self.bus.mem.zorro.region_at(addr, 1).is_some() || region_offset(self.bus.mem.slow_ram.len(), SLOW_RAM_BASE, addr, 1).is_some() || region_offset(self.bus.mem.rom.len(), ROM_BASE, addr, 1).is_some() @@ -2722,6 +2743,14 @@ impl CpuBus { self.bus.ui_note_cpu_ram_write(addr, size); write_be(&mut self.bus.mem.chip_ram, off, size, value); self.dbg_note_memw(addr, size); + if off as u32 != addr { + // The write landed through an Agnus image repeat: note + // the canonical chip address as well as the CPU's bus + // address, so watchpoints and UI highlights fire + // whichever alias of the RAM cell they were set on. + self.bus.ui_note_cpu_ram_write(off as u32, size); + self.dbg_note_memw(off as u32, size); + } return; } Some(PlainMemRegion::ZorroRam(board, off)) => { @@ -3444,6 +3473,66 @@ mod tests { Ok(()) } + // The motherboard decode routes the whole $000000-$1FFFFF window to + // Agnus, which decodes only as many address bits as its DRAM reach, so + // the fitted chip RAM image repeats across the window (every 512 KiB on + // OCS). Regression example: Action Replay freeze-disk loaders point the + // supervisor stack at $100000 on a 512 KiB machine and rely on the + // pushes landing at the top of chip RAM. + #[test] + fn cpu_chip_window_image_aliases_fitted_ram() -> Result<()> { + // move.l #$DEADBEEF,$180010.l ; move.w $100012.l,d1 + let mut machine = machine_with_program( + 0x0500, + &[ + 0x23FC, 0xDEAD, 0xBEEF, 0x0018, 0x0010, // move.l #imm,$180010.l + 0x3239, 0x0010, 0x0012, // move.w $100012.l,d1 + ], + )?; + // Watch both aliases of the target cell: the mirror write must fire + // the watch whichever alias it was set on. + machine.bus_mut().set_ui_mem_watches(&[0x10, 0x180010]); + machine.step_slice(2)?; + // On the fixture's OCS Agnus both $180010 and $100012 are images of + // chip $10/$12. + assert_eq!(read_chip_long(machine.bus(), 0x10), 0xDEADBEEF); + assert_eq!(machine.d(1) & 0xFFFF, 0xBEEF); + assert!(machine.bus_mut().ui_take_mem_writer(0x10).is_some()); + assert!(machine.bus_mut().ui_take_mem_writer(0x180010).is_some()); + Ok(()) + } + + #[test] + fn chip_window_images_follow_agnus_reach() -> Result<()> { + use crate::chipset::agnus::AgnusRevision; + let mut machine = machine_with_program(0x0500, &[0x4E71])?; + machine.bus_mut().mem.chip_ram[0x60000] = 0xA5; + + // OCS (512 KiB reach, A19/A20 unused): the image repeats every + // 512 KiB, and a write through an image lands in the RAM cell. + assert_eq!(machine.debug_read_memory(0x0E0000, 1)[0], 0xA5); + assert_eq!(machine.debug_read_memory(0x160000, 1)[0], 0xA5); + assert_eq!(machine.debug_write_memory(0x1E0004, &[0x77]), 1); + assert_eq!(machine.bus().mem.chip_ram[0x60004], 0x77); + + // 8372A (1 MiB reach, A20 unused): $0E0000 selects the unfitted + // second DRAM bank (open bus) while $160000 is the A20 image of + // $060000. + machine + .bus_mut() + .set_agnus_revision(AgnusRevision::Ecs8372Rev4); + assert_eq!(machine.debug_read_memory(0x0E0000, 1)[0], 0xFF); + assert_eq!(machine.debug_write_memory(0x0E0000, &[0x11]), 0); + assert_eq!(machine.debug_read_memory(0x160000, 1)[0], 0xA5); + + // 8375/Alice (2 MiB reach): the full window is decoded, so nothing + // past the fitted RAM answers. + machine.bus_mut().set_agnus_revision(AgnusRevision::Ecs8375); + assert_eq!(machine.debug_read_memory(0x0E0000, 1)[0], 0xFF); + assert_eq!(machine.debug_read_memory(0x160000, 1)[0], 0xFF); + Ok(()) + } + fn assert_single_instruction_timing( label: &str, slice: CpuStepSlice, diff --git a/src/memory.rs b/src/memory.rs index e8ed5fa..c06cafb 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -12,6 +12,11 @@ pub const ROM_SIZE: usize = 512 * 1024; pub const ROM_SIZE_256K: usize = 256 * 1024; pub const ROM_BASE: u64 = 0x00F8_0000; pub const CHIP_RAM_BASE: u64 = 0x0000_0000; +/// Size of the chip-RAM select window the motherboard address decode (Gary +/// and equivalents) routes to Agnus: $000000-$1FFFFF. Agnus decodes fewer +/// address bits than the window on the smaller parts, so the fitted RAM +/// image repeats inside it (see CpuBus::chip_window_offset). +pub const CHIP_WINDOW_SIZE: u64 = 0x0020_0000; /// Conventional base of the first Zorro II RAM board (the start of the /// Zorro II expansion space, where the ROM assigns it). Test fixtures /// pre-configure their fast RAM boards here. diff --git a/src/video/window/tests.rs b/src/video/window/tests.rs index 5461a5c..162359e 100644 --- a/src/video/window/tests.rs +++ b/src/video/window/tests.rs @@ -3192,13 +3192,17 @@ fn memory_tab_find_scroll_and_bitmap_toggle() { assert_eq!(panel.mem_last_find, Some(0x60000)); assert_eq!(panel.mem_addr, 0x60000); } - // Find again continues past the hit; with a single match the page - // wraps back around to the same place. - app.activate_ui_control(UiControl::DebugMemFind); - assert_eq!( - app.debugger_panel.as_ref().unwrap().mem_last_find, - Some(0x60000) - ); + // Find again continues past the hit. The pattern is CPU-visible again + // at each Agnus image repeat of the 512 KiB chip RAM (OCS Agnus decodes + // only A1-A18, so the image recurs every $80000 across the $000000- + // $1FFFFF chip window), then the search wraps back to the original. + for expected in [0xE0000, 0x160000, 0x1E0000, 0x60000] { + app.activate_ui_control(UiControl::DebugMemFind); + assert_eq!( + app.debugger_panel.as_ref().unwrap().mem_last_find, + Some(expected) + ); + } // Scrolling moves by 16-byte hex rows. app.debugger_mem_scroll(2);