diff --git a/copperline.example.toml b/copperline.example.toml index 8e91966..b66625f 100644 --- a/copperline.example.toml +++ b/copperline.example.toml @@ -182,6 +182,14 @@ slow = "512K" # 64K to 1G. # z3 = "16M" +# Ramsey-controlled motherboard fast RAM (A3000/A4000): 32-bit local RAM +# ending at $08000000 and growing downward, sized by Kickstart's own +# probe rather than autoconfig. Needs a Ramsey memory controller (the +# A3000/A4000 profiles fit one, and 4M of this RAM, by default) and a +# 32-bit CPU. Must fill whole Ramsey banks: 1M-4M in 1M steps, or +# 8M/12M/16M. Set to 0 to remove it. +# motherboard = "16M" + # Additional Zorro boards described by TOML metadata files, configured # in file order after the built-in [memory] fast/z3 boards. The metadata diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 7b370eb..b50059c 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -43,6 +43,7 @@ range checks as the equivalent TOML fields: | `--chip SIZE` | `[memory] chip` | `512K`, `1M`, `2M`, ... | | `--fast SIZE` | `[memory] fast` | `0`, `1M`, `4M`, `8M`, ... | | `--slow SIZE` | `[memory] slow` | `0`, up to `512K` | +| `--motherboard SIZE` | `[memory] motherboard` | Ramsey RAM (A3000/A4000): `0`, `1M`..`4M`, `8M`, `12M`, `16M` | | `--floppy-drives COUNT` | `[floppy] drives` | `1` to `4` wired drives (`DF0:` plus external drives) | | `--floppy-speed PERCENT` | `[floppy] speed` | `100` (real), `200`, `400`, `800`, or `0` (turbo) | | `--joystick MODE` | `[input] joystick` | `gamepad` (default), `keyboard` | @@ -178,8 +179,8 @@ turns the driver back on automatically (it is the boot path for those drives), and setting the flag explicitly wins in either direction. The ROM file itself is never modified. -Motherboard fast RAM is not emulated on either; use `[memory] z3` instead, -which the OS is equally happy with. +Both profiles fit their stock 4M of Ramsey-controlled motherboard fast RAM; +`[memory] motherboard` resizes it up to 16M (see the `[memory]` section). `mem_controller` is normally left to the profile. It is broken out because Ramsey's registers collide with nothing else, so it can be fitted to @@ -311,10 +312,11 @@ clock_mhz = 14.0 # optional; defaults to the model's stock speed ```toml [memory] -chip = "512K" # OCS max 512K; ECS/AGA max 2M -fast = "0" # Zorro II fast RAM at $200000: 64K..8M board sizes -slow = "512K" # A500 trapdoor RAM at $C00000: 0 or up to 512K -z3 = "0" # Zorro III RAM (needs a 32-bit CPU): 64K..1G, power of two +chip = "512K" # OCS max 512K; ECS/AGA max 2M +fast = "0" # Zorro II fast RAM at $200000: 64K..8M board sizes +slow = "512K" # A500 trapdoor RAM at $C00000: 0 or up to 512K +motherboard = "0" # Ramsey motherboard RAM (A3000/A4000): up to 16M +z3 = "0" # Zorro III RAM (needs a 32-bit CPU): 64K..1G, power of two ``` Sizes accept `K`/`KB`/`M`/`MB` (and `G`/`GB` for Zorro III) suffixes or @@ -327,6 +329,13 @@ plain byte counts, and must be multiples of 4 KiB. 4M, or 8M. - **Slow RAM** ($C00000 "ranger" RAM) is arbitrated on the chip bus through Agnus exactly like chip RAM -- it is slow in the authentic way. +- **Motherboard RAM** is the 32-bit local memory Ramsey drives on the + A3000/A4000: it ends at `$08000000` and grows downward (16M reaches + `$07000000`), and Kickstart sizes it with its own probe -- no autoconfig + involved. It needs a Ramsey (`[machine] mem_controller`, fitted by the + A3000/A4000 profiles, which also fit their stock 4M of this RAM by + default) and a 32-bit CPU, and must fill whole Ramsey banks: 1M-4M in + 1M steps, or 8M, 12M, 16M. Set `motherboard = "0"` to remove it. - **Z3 RAM** requires a 68020/68030/68040/68060 (a 24-bit bus cannot reach it); Kickstart assigns its base address, usually `$40000000`. diff --git a/docs/guide/ui.md b/docs/guide/ui.md index 1be1c0a..70dcefa 100644 --- a/docs/guide/ui.md +++ b/docs/guide/ui.md @@ -232,7 +232,8 @@ The layout is: on a machine with no CD drive) are dropped so they cannot block a launch. - **Category tabs** (left sidebar). *System* (chipset and Agnus/Denise overrides, video standard, RTC, identify board), *CPU* (model, FPU, clock, - caches), *Memory* (chip/fast/slow/Zorro III RAM), *ROM* (Kickstart and + caches), *Memory* (chip/fast/slow/motherboard/Zorro III RAM), *ROM* + (Kickstart and extended ROM), *Floppy* (drive count and speed, per-drive image and write-protect), *Hard Disk* (IDE master/slave, the SCSI controller -- A2091, A4091, or the diff --git a/src/a2065.rs b/src/a2065.rs index b0c0990..232cc13 100644 --- a/src/a2065.rs +++ b/src/a2065.rs @@ -555,6 +555,7 @@ mod tests { crate::memory::Memory { chip_ram: vec![0u8; 0x100], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/a2091.rs b/src/a2091.rs index 47b8116..9a5f6be 100644 --- a/src/a2091.rs +++ b/src/a2091.rs @@ -481,6 +481,7 @@ mod tests { Memory { chip_ram: vec![0u8; 512 * 1024], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: ZorroChain::default(), diff --git a/src/a4091.rs b/src/a4091.rs index c44b8dd..c1fa6ef 100644 --- a/src/a4091.rs +++ b/src/a4091.rs @@ -1008,6 +1008,7 @@ mod tests { Memory { chip_ram: vec![0u8; 512 * 1024], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: ZorroChain::default(), diff --git a/src/bus.rs b/src/bus.rs index 50fa4fd..60be527 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -5330,8 +5330,8 @@ impl Bus { } /// The writable RAM regions a memory hunt should scan: chip RAM, - /// slow/ranger RAM, and configured Zorro RAM boards, as - /// (base, length) pairs. + /// slow/ranger RAM, motherboard fast RAM, and configured Zorro RAM + /// boards, as (base, length) pairs. pub fn writable_ram_regions(&self) -> Vec<(u32, u32)> { let mut regions = Vec::new(); if !self.mem.chip_ram.is_empty() { @@ -5346,13 +5346,16 @@ impl Bus { self.mem.slow_ram.len() as u32, )); } + if !self.mem.mb_ram.is_empty() { + regions.push((self.mem.mb_ram_base() as u32, self.mem.mb_ram.len() as u32)); + } regions.extend(self.mem.zorro.ram_regions()); regions } /// Read a 16-bit big-endian word from whichever RAM/ROM region maps - /// `addr` (chip, fast, slow, or ROM), for the debugger's memory dumps. - /// Returns 0 for unmapped addresses. + /// `addr` (chip, fast, slow, motherboard, or ROM), for the debugger's + /// memory dumps. Returns 0 for unmapped addresses. pub fn peek_word_any(&self, addr: u32) -> u16 { use crate::memory::{CHIP_RAM_BASE, ROM_BASE, SLOW_RAM_BASE}; if let Some((board, off)) = self.mem.zorro.region_at(addr, 2) { @@ -5369,9 +5372,10 @@ impl Bus { .unwrap_or(0); } let a = addr as usize; - let regions: [(usize, &[u8]); 4] = [ + let regions: [(usize, &[u8]); 5] = [ (CHIP_RAM_BASE as usize, &self.mem.chip_ram), (SLOW_RAM_BASE as usize, &self.mem.slow_ram), + (self.mem.mb_ram_base() as usize, &self.mem.mb_ram), (ROM_BASE as usize, &self.mem.rom), (self.mem.extended_rom_base as usize, &self.mem.extended_rom), ]; diff --git a/src/bus/tests.rs b/src/bus/tests.rs index f1b667c..63bed8e 100644 --- a/src/bus/tests.rs +++ b/src/bus/tests.rs @@ -215,6 +215,7 @@ fn empty_bus_with_chip_ram(chip_ram_bytes: usize) -> Bus { Memory { chip_ram: vec![0; chip_ram_bytes], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom: vec![0; 512 * 1024], overlay: true, zorro: crate::zorro::ZorroChain::default(), @@ -234,6 +235,7 @@ fn empty_bus_with_collect_audio() -> (Bus, SharedFrames) { Memory { chip_ram: vec![0; 512 * 1024], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom: vec![0; 512 * 1024], overlay: true, zorro: crate::zorro::ZorroChain::default(), @@ -10844,3 +10846,23 @@ fn waveform_stop_finishes_early_and_rearm_replaces() { let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&path2); } + +/// The debugger-facing helpers must reach the Ramsey motherboard RAM bank: +/// peek_word_any serves memory dumps at its bus address and +/// writable_ram_regions offers it to the memory hunt. +#[test] +fn motherboard_ram_reaches_the_debugger_helpers() { + let mut bus = empty_bus(); + bus.mem.fit_mb_ram(1024 * 1024); + let base = bus.mem.mb_ram_base() as u32; + assert_eq!(base, 0x07F0_0000); + let top = bus.mem.mb_ram.len(); + bus.mem.mb_ram[top - 2] = 0xBE; + bus.mem.mb_ram[top - 1] = 0xEF; + assert_eq!(bus.peek_word_any(0x07FF_FFFE), 0xBEEF); + // Below the fitted bank nothing answers. + assert_eq!(bus.peek_word_any(base - 2), 0); + assert!(bus + .writable_ram_regions() + .contains(&(base, 1024 * 1024_u32))); +} diff --git a/src/cdtv.rs b/src/cdtv.rs index 6ade2aa..5020bde 100644 --- a/src/cdtv.rs +++ b/src/cdtv.rs @@ -1339,6 +1339,7 @@ mod tests { Memory { chip_ram: vec![0u8; chip_bytes], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/config.rs b/src/config.rs index ef51b1c..219ab04 100644 --- a/src/config.rs +++ b/src/config.rs @@ -59,6 +59,12 @@ pub struct Config { pub chip_ram_bytes: usize, pub fast_ram_bytes: usize, pub slow_ram_bytes: usize, + /// Ramsey-controlled motherboard fast RAM (`[memory] motherboard`): + /// 32-bit local RAM ending at $08000000 and growing downward, sized by + /// Kickstart's own probe rather than autoconfig. Needs a Ramsey + /// ([`MemController`]) and a CPU with a 32-bit address bus. The + /// A3000/A4000 profiles fit 4 MiB by default. + pub mb_ram_bytes: usize, /// Zorro III autoconfig RAM (`[memory] z3`). Needs a CPU with a 32-bit /// address bus (68020/030/040; not the 24-bit 68000/68EC020). pub z3_ram_bytes: usize, @@ -820,14 +826,13 @@ pub enum MachineModel { /// stock chip RAM, no trapdoor slow RAM, no RTC. A1000, /// A3000: ECS, 68030 at 25 MHz, 2 MB chip RAM, a Ramsey-04 memory - /// controller and the battery-backed MSM6242 clock. No Gayle -- the - /// big-box machines carry Gary -- and no slow RAM. Motherboard fast RAM is - /// not emulated; the OS is happy taking its fast RAM from the Zorro III - /// board instead. + /// controller with the stock 4 MB of motherboard fast RAM + /// (`[memory] motherboard` resizes it), and the battery-backed MSM6242 + /// clock. No Gayle -- the big-box machines carry Gary -- and no slow RAM. A3000, /// A4000: the same board a generation later -- AGA, a 25 MHz 68040, and - /// Ramsey-07. Same story on Gayle, slow RAM and motherboard fast RAM as - /// the A3000. + /// Ramsey-07 with the same stock 4 MB of motherboard fast RAM. Same + /// story on Gayle and slow RAM as the A3000. A4000, } @@ -878,6 +883,9 @@ pub struct MachineDescriptor { pub chip_ram_bytes: usize, pub fast_ram_bytes: usize, pub slow_ram_bytes: usize, + /// Ramsey-controlled motherboard fast RAM (A3000/A4000). + #[serde(default)] + pub mb_ram_bytes: usize, pub chipset: Chipset, pub video_standard: VideoStandard, pub machine: Option, @@ -898,6 +906,7 @@ impl Default for MachineDescriptor { chip_ram_bytes: 512 * 1024, fast_ram_bytes: 0, slow_ram_bytes: 0, + mb_ram_bytes: 0, chipset: Chipset::Ocs, video_standard: VideoStandard::Pal, machine: None, @@ -928,13 +937,14 @@ impl MachineDescriptor { None => String::new(), }; format!( - "{profile} / {:?} / {:?} / {:?} / chip {}K fast {}K slow {}K / ROM {}{ext}", + "{profile} / {:?} / {:?} / {:?} / chip {}K fast {}K slow {}K mb {}K / ROM {}{ext}", self.cpu, self.chipset, self.video_standard, self.chip_ram_bytes / 1024, self.fast_ram_bytes / 1024, self.slow_ram_bytes / 1024, + self.mb_ram_bytes / 1024, self.rom.label(), ) } @@ -980,6 +990,13 @@ impl MachineDescriptor { other.slow_ram_bytes / 1024 )); } + if self.mb_ram_bytes != other.mb_ram_bytes { + diffs.push(format!( + "motherboard RAM {}K -> {}K", + self.mb_ram_bytes / 1024, + other.mb_ram_bytes / 1024 + )); + } if self.rom != other.rom { diffs.push(format!("ROM {} -> {}", self.rom.label(), other.rom.label())); } @@ -1079,6 +1096,7 @@ impl Default for Config { chip_ram_bytes: 512 * 1024, fast_ram_bytes: 0, slow_ram_bytes: A500_TRAPDOOR_RAM_BYTES, + mb_ram_bytes: 0, z3_ram_bytes: 0, zorro_boards: Vec::new(), wasm_boards: Vec::new(), @@ -1170,6 +1188,7 @@ impl Config { chip_ram_bytes: self.chip_ram_bytes, fast_ram_bytes: self.fast_ram_bytes, slow_ram_bytes: self.slow_ram_bytes, + mb_ram_bytes: self.mb_ram_bytes, chipset: self.chipset, video_standard: self.video_standard, machine: self.machine, @@ -1259,6 +1278,9 @@ pub struct ConfigOverrides { pub chip: Option, pub fast: Option, pub slow: Option, + /// Ramsey motherboard fast RAM size (`--motherboard`). Same parser as + /// `[memory] motherboard`. + pub motherboard: Option, pub floppy_drives: Option, /// Drive speed override (`--floppy-speed`): a percentage (100/200/400/ /// 800) or 0 for turbo. Same values as `[floppy] speed`. @@ -1321,6 +1343,7 @@ impl ConfigOverrides { && self.chip.is_none() && self.fast.is_none() && self.slow.is_none() + && self.motherboard.is_none() && self.floppy_drives.is_none() && self.floppy_speed.is_none() && self.joystick.is_none() @@ -1368,6 +1391,9 @@ impl ConfigOverrides { if let Some(slow) = &self.slow { raw.memory.slow = Some(slow.clone()); } + if let Some(motherboard) = &self.motherboard { + raw.memory.motherboard = Some(motherboard.clone()); + } if let Some(drives) = self.floppy_drives { raw.floppy.drives = Some(drives); } @@ -1850,6 +1876,10 @@ pub(crate) struct RawMemory { pub(crate) fast: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) slow: Option, + /// Ramsey-controlled motherboard fast RAM size (e.g. "16M"); needs a + /// Ramsey (A3000/A4000 profiles) and a 32-bit CPU. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) motherboard: Option, /// Zorro III autoconfig RAM size (e.g. "16M"); 32-bit CPUs only. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) z3: Option, @@ -2136,6 +2166,10 @@ impl TryFrom for Config { None => defaults.slow_ram_bytes, Some(s) => parse_size(s, "slow RAM")?, }; + let mb_ram_bytes = match raw.memory.motherboard.as_deref() { + None => defaults.mb_ram_bytes, + Some(s) => parse_size(s, "motherboard RAM")?, + }; let z3_ram_bytes = match raw.memory.z3.as_deref() { None => defaults.z3_ram_bytes, Some(s) => parse_size(s, "Zorro III RAM")?, @@ -2421,9 +2455,21 @@ impl TryFrom for Config { Some(s) => parse_denise_revision(s)?, }; + let mem_controller = match raw.machine.mem_controller.as_deref() { + None => defaults.mem_controller, + Some("none") => MemController::None, + Some("ramsey-04") => MemController::Ramsey4, + Some("ramsey-07") => MemController::Ramsey7, + Some(other) => anyhow::bail!( + "[machine] mem_controller {other:?} is not one of \ + none, ramsey-04, ramsey-07" + ), + }; + errors.extend(validate_chip_ram(chip_ram_bytes, chipset, agnus_revision).err()); errors.extend(validate_fast_ram(fast_ram_bytes, chip_ram_bytes).err()); errors.extend(validate_slow_ram(slow_ram_bytes).err()); + errors.extend(validate_mb_ram(mb_ram_bytes, mem_controller, cpu).err()); errors.extend(validate_z3_ram(z3_ram_bytes, cpu).err()); errors.extend(validate_rtg_card(rtg, cpu).err()); let board_specs = zorro_boards @@ -2498,6 +2544,7 @@ impl TryFrom for Config { chip_ram_bytes, fast_ram_bytes, slow_ram_bytes, + mb_ram_bytes, z3_ram_bytes, zorro_boards, wasm_boards, @@ -2568,16 +2615,7 @@ impl TryFrom for Config { .as_deref() .map(parse_log_unmapped) .transpose()?, - mem_controller: match raw.machine.mem_controller.as_deref() { - None => defaults.mem_controller, - Some("none") => MemController::None, - Some("ramsey-04") => MemController::Ramsey4, - Some("ramsey-07") => MemController::Ramsey7, - Some(other) => anyhow::bail!( - "[machine] mem_controller {other:?} is not one of \ - none, ramsey-04, ramsey-07" - ), - }, + mem_controller, video_standard, audio, ide, @@ -2862,6 +2900,8 @@ pub(crate) fn machine_profile_defaults(model: MachineModel) -> Config { d.chipset = Chipset::Ecs; d.chip_ram_bytes = 2 * 1024 * 1024; d.slow_ram_bytes = 0; + // Stock motherboard fast RAM: four banks of 256Kx4 ZIPs. + d.mb_ram_bytes = 4 * 1024 * 1024; d.cpu = CpuModel::M68030; d.cpu_clock_mhz = 25.0; d.mem_controller = MemController::Ramsey4; @@ -2876,6 +2916,8 @@ pub(crate) fn machine_profile_defaults(model: MachineModel) -> Config { d.chipset = Chipset::Aga; d.chip_ram_bytes = 2 * 1024 * 1024; d.slow_ram_bytes = 0; + // Stock motherboard fast RAM: one 4 MiB bank of 1Mx4 SIMMs. + d.mb_ram_bytes = 4 * 1024 * 1024; d.cpu = CpuModel::M68040; d.cpu_clock_mhz = 25.0; d.mem_controller = MemController::Ramsey7; @@ -3085,6 +3127,42 @@ fn validate_fast_ram(fast: usize, chip: usize) -> Result<()> { Ok(()) } +/// Motherboard fast RAM must land on Ramsey's bank layout: four banks of +/// either 256Kx4 parts (1 MiB per bank) or 1Mx4 parts (4 MiB per bank), so +/// 1M-4M in 1M steps or 4M/8M/12M/16M. It also needs the Ramsey itself and +/// a CPU whose address bus reaches $07000000 at all. +fn validate_mb_ram(mb: usize, mem_controller: MemController, cpu: CpuModel) -> Result<()> { + const BANK_1M: usize = 1024 * 1024; + const BANK_4M: usize = 4 * 1024 * 1024; + if mb == 0 { + return Ok(()); + } + if mem_controller.ramsey_revision().is_none() { + bail!( + "motherboard RAM needs a Ramsey memory controller \ + ([machine] mem_controller = \"ramsey-04\" or \"ramsey-07\", \ + fitted by the A3000/A4000 profiles)" + ); + } + if !cpu_has_32bit_bus(cpu) { + bail!( + "motherboard RAM ends at $08000000, beyond a 24-bit address bus: \ + {:?} cannot reach it (needs a 68020/68030/68040/68060)", + cpu + ); + } + let on_1m_banks = mb.is_multiple_of(BANK_1M) && mb <= 4 * BANK_1M; + let on_4m_banks = mb.is_multiple_of(BANK_4M) && mb <= 4 * BANK_4M; + if !(on_1m_banks || on_4m_banks) { + bail!( + "motherboard RAM {} bytes does not fill Ramsey banks \ + (1M-4M in 1M steps, or 8M, 12M, 16M)", + mb + ); + } + Ok(()) +} + fn cpu_has_32bit_bus(cpu: CpuModel) -> bool { matches!( cpu, @@ -3271,6 +3349,9 @@ pub fn about_machine_lines(cfg: &Config) -> Vec { if cfg.fast_ram_bytes > 0 { ram.push_str(&format!(", {}K fast", cfg.fast_ram_bytes / 1024)); } + if cfg.mb_ram_bytes > 0 { + ram.push_str(&format!(", {}K motherboard", cfg.mb_ram_bytes / 1024)); + } if cfg.z3_ram_bytes > 0 { ram.push_str(&format!(", {}K Z3", cfg.z3_ram_bytes / 1024)); } @@ -3529,6 +3610,7 @@ mod tests { chip: Some("2M".to_string()), fast: Some("8M".to_string()), slow: None, + motherboard: None, z3: None, }, chipset: RawChipset { @@ -4399,6 +4481,69 @@ mod tests { ); } + /// The big-box profiles fit their stock 4 MB of Ramsey motherboard RAM; + /// `[memory] motherboard` resizes it within Ramsey's bank layout, and it + /// is refused where no Ramsey (or no 32-bit CPU) could drive it. + #[test] + fn motherboard_ram_defaults_and_constraints() -> Result<()> { + let cfg = parse_config("[machine]\nprofile = \"A3000\"")?; + assert_eq!(cfg.mb_ram_bytes, 4 * 1024 * 1024); + let cfg = parse_config("[machine]\nprofile = \"A4000\"")?; + assert_eq!(cfg.mb_ram_bytes, 4 * 1024 * 1024); + + // Resizable up to the full 16 MB, and removable. + let cfg = parse_config( + r#" + [machine] + profile = "A4000" + [memory] + motherboard = "16M" + "#, + )?; + assert_eq!(cfg.mb_ram_bytes, 16 * 1024 * 1024); + let cfg = parse_config( + r#" + [machine] + profile = "A4000" + [memory] + motherboard = "0" + "#, + )?; + assert_eq!(cfg.mb_ram_bytes, 0); + + // A total that fills no whole bank layout is refused. + let err = parse_config( + r#" + [machine] + profile = "A4000" + [memory] + motherboard = "5M" + "#, + ) + .unwrap_err(); + assert!(err.to_string().contains("Ramsey banks"), "{err:#}"); + + // No Ramsey to drive it on the default A500-class machine. + let err = parse_config("[memory]\nmotherboard = \"4M\"").unwrap_err(); + assert!( + err.to_string().contains("Ramsey memory controller"), + "{err:#}" + ); + + // A 24-bit CPU cannot reach $08000000 at all. + let err = parse_config( + r#" + [machine] + profile = "A3000" + [cpu] + model = "68000" + "#, + ) + .unwrap_err(); + assert!(err.to_string().contains("24-bit"), "{err:#}"); + Ok(()) + } + #[test] fn cpu_cache_flags_gate_on_model() -> Result<()> { let cfg = parse_config( diff --git a/src/control.rs b/src/control.rs index 0a837ce..7503764 100644 --- a/src/control.rs +++ b/src/control.rs @@ -182,6 +182,7 @@ pub(crate) fn test_emulator() -> crate::emulator::Emulator { crate::memory::Memory { chip_ram, slow_ram: Vec::new(), + mb_ram: Vec::new(), rom, overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/cpu.rs b/src/cpu.rs index c1ab994..cb8f82a 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -2219,8 +2219,8 @@ impl M68kMachine { /// offset into that region's backing store. Shared by every access path /// (peek/debug-write for the debugger, and the real uncached read/write) /// so the plain-memory part of the address map -- overlay ROM, chip RAM, -/// Zorro RAM, slow RAM, ROM, WCS, extended ROM -- is only decoded in one -/// place. I/O regions (CIA, RTC, Gayle, custom chips, Zorro devices, +/// Zorro RAM, slow RAM, motherboard RAM, ROM, WCS, extended ROM -- is only +/// decoded in one place. I/O regions (CIA, RTC, Gayle, custom chips, Zorro devices, /// autoconfig) are not plain memory and stay in `read_sized_uncached` / /// `write_sized`, which only reach them once this classifier returns /// `None`. @@ -2229,6 +2229,7 @@ enum PlainMemRegion { ChipRam(usize), ZorroRam(usize, usize), SlowRam(usize), + MbRam(usize), Rom(usize), Wcs(usize), ExtendedRom(usize), @@ -2252,6 +2253,14 @@ impl CpuBus { if let Some(off) = region_offset(self.bus.mem.slow_ram.len(), SLOW_RAM_BASE, addr, size) { return Some(PlainMemRegion::SlowRam(off)); } + if let Some(off) = region_offset( + self.bus.mem.mb_ram.len(), + self.bus.mem.mb_ram_base(), + addr, + size, + ) { + return Some(PlainMemRegion::MbRam(off)); + } if let Some(off) = region_offset(self.bus.mem.rom.len(), ROM_BASE, addr, size) { return Some(PlainMemRegion::Rom(off)); } @@ -2403,6 +2412,7 @@ impl CpuBus { Some(PlainMemRegion::ChipRam(off)) => self.bus.mem.chip_ram[off], Some(PlainMemRegion::ZorroRam(board, off)) => self.bus.mem.zorro.board_ram(board)[off], Some(PlainMemRegion::SlowRam(off)) => self.bus.mem.slow_ram[off], + Some(PlainMemRegion::MbRam(off)) => self.bus.mem.mb_ram[off], Some(PlainMemRegion::Wcs(off)) => self.bus.mem.wcs[off], Some(PlainMemRegion::ExtendedRom(off)) => self.bus.mem.extended_rom[off], None => 0xFF, @@ -2427,6 +2437,11 @@ impl CpuBus { self.invalidate_debug_write(addr, 1); true } + Some(PlainMemRegion::MbRam(off)) => { + self.bus.mem.mb_ram[off] = value; + self.invalidate_debug_write(addr, 1); + true + } // Overlay ROM/ROM/WCS/extended ROM are not debugger-writable. _ => false, } @@ -2554,6 +2569,13 @@ impl CpuBus { 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.mb_ram.len(), + self.bus.mem.mb_ram_base(), + addr, + 1, + ) + .is_some() || region_offset(self.bus.mem.rom.len(), ROM_BASE, addr, 1).is_some() || region_offset( self.bus.mem.extended_rom.len(), @@ -2564,14 +2586,21 @@ impl CpuBus { .is_some() } - /// What the data cache may hold: expansion RAM and ROM only. Chip and - /// slow RAM are excluded (CIIN), as on real Amigas, because DMA writes - /// them behind the CPU's back. + /// What the data cache may hold: expansion RAM, motherboard RAM, and ROM + /// only. Chip and slow RAM are excluded (CIIN), as on real Amigas, + /// because DMA writes them behind the CPU's back. fn dcache_cacheable(&self, addr: u32) -> bool { if self.overlay_rom_offset(addr, 1).is_some() { return false; } self.bus.mem.zorro.region_at(addr, 1).is_some() + || region_offset( + self.bus.mem.mb_ram.len(), + self.bus.mem.mb_ram_base(), + addr, + 1, + ) + .is_some() || region_offset(self.bus.mem.rom.len(), ROM_BASE, addr, 1).is_some() || region_offset( self.bus.mem.extended_rom.len(), @@ -2599,6 +2628,13 @@ impl CpuBus { self.bus.cpu_external_access(Self::access_words(size)); return read_be(self.bus.mem.zorro.board_ram(board), off, size); } + Some(PlainMemRegion::MbRam(off)) => { + // Ramsey-controlled motherboard RAM (A3000/A4000): 32-bit + // local RAM off the chip bus, at uncontended external speed + // like expansion RAM. + self.bus.cpu_external_access(Self::access_words(size)); + return read_be(&self.bus.mem.mb_ram, off, size); + } Some(PlainMemRegion::SlowRam(off)) => { // "Slow"/trapdoor RAM at $C00000 is decoded by Gary and reached // through Agnus on the chip bus, so the CPU contends for it cycle @@ -2824,6 +2860,11 @@ impl CpuBus { write_be(self.bus.mem.zorro.board_ram_mut(board), off, size, value); return; } + Some(PlainMemRegion::MbRam(off)) => { + self.bus.cpu_external_access(Self::access_words(size)); + write_be(&mut self.bus.mem.mb_ram, off, size, value); + return; + } Some(PlainMemRegion::SlowRam(off)) => { // Slow/trapdoor RAM at $C00000 is on the chip bus (reached through // Agnus), so CPU writes contend cycle by cycle like chip RAM rather @@ -3353,6 +3394,7 @@ mod tests { Memory { chip_ram, slow_ram: vec![0; 64 * 1024], + mb_ram: Vec::new(), rom, overlay: false, zorro, @@ -3407,6 +3449,45 @@ mod tests { bus.mem.chip_ram[off..off + 4].copy_from_slice(&value.to_be_bytes()); } + /// Ramsey motherboard RAM decodes at the top of its window (ending at + /// $08000000) for a 32-bit CPU: stores land in the bank and read back + /// through the same decode. The bank sits beyond a 24-bit address bus, + /// so the same access on a 68000 wraps into the low 16 MB and must + /// leave the fitted RAM untouched. + #[test] + fn motherboard_ram_decodes_at_the_top_of_its_window() -> Result<()> { + let program = [ + 0x23FCu16, 0xCAFE, 0xBABE, 0x07FF, 0xFFFC, // move.l #$CAFEBABE,$07FFFFFC + 0x2039, 0x07FF, 0xFFFC, // move.l $07FFFFFC,d0 + 0x23C0, 0x0000, 0x1000, // move.l d0,$1000 + 0x60FE, // bra.s * + ]; + + let mut bus = test_bus_with_pc(0x00F8_0100); + bus.mem.fit_mb_ram(2 * 1024 * 1024); + assert_eq!(bus.mem.mb_ram_base(), 0x07E0_0000); + write_program(&mut bus, 0x00F8_0100, &program); + let mut machine = build(bus, CpuModel::M68030, false, 2, Default::default(), false)?; + machine.step_slice(3)?; + let mem = &machine.bus.bus.mem; + let top = mem.mb_ram.len(); + assert_eq!(&mem.mb_ram[top - 4..], &0xCAFE_BABEu32.to_be_bytes()); + // The readback flowed through d0 into chip RAM. + assert_eq!(&mem.chip_ram[0x1000..0x1004], &0xCAFE_BABEu32.to_be_bytes()); + // The debugger's any-region peek sees the bank at its bus address. + assert_eq!(machine.bus.bus.peek_word_any(0x07FF_FFFC), 0xCAFE); + + let mut bus = test_bus_with_pc(0x00F8_0100); + bus.mem.fit_mb_ram(2 * 1024 * 1024); + write_program(&mut bus, 0x00F8_0100, &program); + let mut machine = build(bus, CpuModel::M68000, false, 2, Default::default(), false)?; + machine.step_slice(3)?; + let mem = &machine.bus.bus.mem; + let top = mem.mb_ram.len(); + assert_eq!(&mem.mb_ram[top - 4..], &[0, 0, 0, 0]); + Ok(()) + } + fn write_chip_word(bus: &mut Bus, address: u32, value: u16) { let off = region_offset(bus.mem.chip_ram.len(), CHIP_RAM_BASE, address, 2) .expect("test chip word address must fit chip RAM"); @@ -3674,6 +3755,7 @@ mod tests { Memory { chip_ram: vec![0; 512 * 1024], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom, overlay: true, zorro: ZorroChain::default(), diff --git a/src/emulator.rs b/src/emulator.rs index 7a77d62..009936f 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -1976,6 +1976,15 @@ pub fn build_machine( } else { Memory::load(&cfg.rom_path, cfg.chip_ram_bytes, cfg.slow_ram_bytes, zorro)? }; + if cfg.mb_ram_bytes > 0 { + mem.fit_mb_ram(cfg.mb_ram_bytes); + info!( + "ramsey: {}K motherboard fast RAM at {:#010X}-{:#010X}", + cfg.mb_ram_bytes / 1024, + mem.mb_ram_base(), + crate::memory::MB_RAM_TOP - 1 + ); + } if let Some(path) = &cfg.extended_rom_path { if rom_optional && !path.is_file() { // As with the main ROM above, the save state carries the extended @@ -2090,9 +2099,11 @@ pub fn build_machine( } } if let Some(revision) = cfg.mem_controller.ramsey_revision() { - // TODO(codewiz): pass the real bank size once motherboard fast RAM - // exists; until then Ramsey describes the stock DRAM for the part. - let bank_bytes = revision.stock_bank_bytes(); + // Seed the control register with the DRAM geometry backing the fitted + // motherboard RAM (stock geometry when none is fitted), so Kickstart's + // sizing probe and the diagnostic tools read a description matching + // the RAM that answers. + let bank_bytes = revision.bank_bytes_for(cfg.mb_ram_bytes); bus.attach_ramsey(crate::ramsey::Ramsey::new(revision, bank_bytes)); } // Gary and Ramsey share one address decode -- Gary owns byte lanes 0-2 of @@ -2551,6 +2562,7 @@ mod tests { crate::memory::Memory { chip_ram: vec![0u8; 512 * 1024], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom, overlay: false, zorro: crate::zorro::ZorroChain::default(), @@ -2606,6 +2618,7 @@ mod tests { crate::memory::Memory { chip_ram, slow_ram: Vec::new(), + mb_ram: Vec::new(), rom, overlay: false, zorro: crate::zorro::ZorroChain::default(), @@ -2661,6 +2674,7 @@ mod tests { crate::memory::Memory { chip_ram, slow_ram: Vec::new(), + mb_ram: Vec::new(), rom, overlay: false, zorro: crate::zorro::ZorroChain::default(), @@ -2823,6 +2837,7 @@ mod tests { crate::memory::Memory { chip_ram, slow_ram: Vec::new(), + mb_ram: Vec::new(), rom, overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/filesys.rs b/src/filesys.rs index 2056263..c672c9d 100644 --- a/src/filesys.rs +++ b/src/filesys.rs @@ -2257,6 +2257,7 @@ mod tests { let mut mem = Memory { chip_ram: vec![0u8; 0x1_0000], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/gdbstub.rs b/src/gdbstub.rs index f174ca9..b5d71c7 100644 --- a/src/gdbstub.rs +++ b/src/gdbstub.rs @@ -1218,6 +1218,7 @@ mod tests { crate::memory::Memory { chip_ram, slow_ram: Vec::new(), + mb_ram: Vec::new(), rom, overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/main.rs b/src/main.rs index 4038b73..e9768f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -391,6 +391,12 @@ where .ok_or_else(|| anyhow!("--slow requires a size (e.g. 0, 512K)"))?, ); } + "--motherboard" => { + overrides.motherboard = + Some(args.next().ok_or_else(|| { + anyhow!("--motherboard requires a size (e.g. 0, 4M, 16M)") + })?); + } "--floppy-drives" | "--fdd-drives" => { let value = args .next() @@ -897,6 +903,7 @@ fn print_help() { --chip SIZE chip RAM size, e.g. 512K, 1M, 2M\n \ --fast SIZE Zorro II fast RAM size, e.g. 0, 1M, 4M, 8M\n \ --slow SIZE trapdoor slow RAM at $C00000, e.g. 0, 512K\n \ + --motherboard SIZE Ramsey motherboard fast RAM (A3000/A4000), e.g. 0, 4M, 16M\n \ --floppy-drives COUNT wired floppy drives, 1-4 (DF0 plus externals)\n \ --floppy-speed PERCENT drive speed: 100, 200, 400, 800, or 0 (turbo)\n \ --rtc-time TIME seed the battery clock (implies fitting one) with\n \ @@ -1635,6 +1642,7 @@ fn build_placeholder_machine() -> Result { let mem = Memory { chip_ram: vec![0u8; 512 * 1024], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom, overlay: true, zorro: copperline::zorro::ZorroChain::default(), diff --git a/src/memory.rs b/src/memory.rs index c06cafb..6b82b41 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -23,6 +23,15 @@ pub const CHIP_WINDOW_SIZE: u64 = 0x0020_0000; #[cfg_attr(not(test), allow(dead_code))] pub const FAST_RAM_BASE: u64 = 0x0020_0000; pub const SLOW_RAM_BASE: u64 = 0x00C0_0000; +/// Exclusive top of the Ramsey-controlled motherboard fast RAM (A3000/A4000): +/// the bank ends here and grows downward, so a full 16 MiB reaches $07000000. +/// Kickstart sizes it from the top down; unpopulated space below the fitted +/// RAM stays undecoded (Fat Gary times the cycle out). +pub const MB_RAM_TOP: u64 = 0x0800_0000; +/// The most motherboard RAM Ramsey can drive: four 4 MiB banks of 1Mx4 +/// parts. [`Memory::fit_mb_ram`] enforces this, which keeps the +/// [`Memory::mb_ram_base`] subtraction from ever underflowing. +pub const MB_RAM_MAX: usize = 16 * 1024 * 1024; /// Amiga 1000 WCS / WOM: 256 KiB of writable control store at $FC0000 that /// the boot ROM loads Kickstart into and then write-protects. The 256 KiB /// boot-ROM window ($F80000-$FBFFFF) sits immediately below it, so a boot @@ -82,6 +91,10 @@ pub fn normalize_a1000_boot_rom(rom: Vec) -> Result> { pub struct Memory { pub chip_ram: Vec, pub slow_ram: Vec, + /// Ramsey-controlled motherboard fast RAM (A3000/A4000): 32-bit local + /// RAM off the chip bus, ending at [`MB_RAM_TOP`] and growing downward. + /// Empty on machines without a Ramsey; fitted via [`Memory::fit_mb_ram`]. + pub mb_ram: Vec, pub rom: Vec, pub overlay: bool, /// Zorro expansion boards (autoconfig chain plus their RAM windows). @@ -170,6 +183,7 @@ impl Memory { Self { chip_ram: vec![0u8; chip_ram_bytes], slow_ram: vec![0u8; slow_ram_bytes], + mb_ram: Vec::new(), rom, overlay: true, zorro, @@ -180,6 +194,25 @@ impl Memory { } } + /// Fit `bytes` of Ramsey-controlled motherboard fast RAM (see + /// [`Memory::mb_ram`]). Zero removes the bank. Panics beyond + /// [`MB_RAM_MAX`]: config validation rejects such sizes long before + /// this, and enforcing the bound at the only mutation point keeps + /// [`Memory::mb_ram_base`] a plain subtraction. + pub fn fit_mb_ram(&mut self, bytes: usize) { + assert!( + bytes <= MB_RAM_MAX, + "motherboard RAM {bytes} bytes exceeds Ramsey's {MB_RAM_MAX}-byte maximum" + ); + self.mb_ram = vec![0u8; bytes]; + } + + /// Base address of the fitted motherboard RAM: the bank ends at + /// [`MB_RAM_TOP`] and grows downward, so the base depends on its size. + pub fn mb_ram_base(&self) -> u64 { + MB_RAM_TOP - self.mb_ram.len() as u64 + } + /// Attach an extended ROM image: 512 KiB maps at $E00000 (CD32), /// 256 KiB at $F00000 (CDTV). pub fn attach_extended_rom(&mut self, image: Vec) -> Result<()> { @@ -211,6 +244,7 @@ impl Memory { pub fn power_on_reset(&mut self) { self.chip_ram.fill(0); self.slow_ram.fill(0); + self.mb_ram.fill(0); // Cold boot loses the WCS contents and returns the latch to boot mode // (WCS writable), so the A1000 reloads Kickstart from disk. A warm // (keyboard) reset preserves the WCS, as the real machine does. @@ -266,6 +300,29 @@ mod tests { assert_eq!(&mem.rom[0x10..0x12], &0x60FEu16.to_be_bytes()); // bra.s self } + #[test] + fn mb_ram_ends_at_its_top_and_clears_on_power_on() { + let mut mem = Memory::placeholder(1024, 0, ZorroChain::default()); + assert_eq!(mem.mb_ram_base(), MB_RAM_TOP); // empty bank: zero-length + mem.fit_mb_ram(2 * 1024 * 1024); + // The bank grows downward from MB_RAM_TOP, so the base tracks size. + assert_eq!(mem.mb_ram_base(), MB_RAM_TOP - 2 * 1024 * 1024); + mem.mb_ram[0] = 0xAA; + // Power-on loses the contents like every other RAM. + mem.power_on_reset(); + assert_eq!(mem.mb_ram[0], 0); + assert_eq!(mem.mb_ram.len(), 2 * 1024 * 1024); + } + + /// The only mutation point enforces Ramsey's 16 MiB maximum, which is + /// what keeps `mb_ram_base` a plain subtraction. + #[test] + #[should_panic(expected = "exceeds Ramsey")] + fn mb_ram_beyond_the_ramsey_maximum_is_refused() { + let mut mem = Memory::placeholder(1024, 0, ZorroChain::default()); + mem.fit_mb_ram(MB_RAM_MAX + 1); + } + #[test] fn a1000_boot_rom_echoes_64k_across_the_256k_window() { // The 64 KiB A1000 bootstrap ROM is echoed four times to fill the diff --git a/src/ramsey.rs b/src/ramsey.rs index 33b357b..0f59719 100644 --- a/src/ramsey.rs +++ b/src/ramsey.rs @@ -1,11 +1,12 @@ //! Ramsey: the memory controller of the A3000 and A4000. //! -//! Ramsey drives the motherboard fast RAM (32-bit local RAM at $07000000 on -//! the A3000, $08000000 on the A4000): it refreshes the DRAM and decides -//! whether to use page mode, burst mode and, on Ramsey-07, cycle-skip mode. -//! The CPU sees only two registers. +//! Ramsey drives the motherboard fast RAM (32-bit local RAM ending at +//! $08000000 and growing downward, so a full 16 MiB reaches $07000000): it +//! refreshes the DRAM and decides whether to use page mode, burst mode and, +//! on Ramsey-07, cycle-skip mode. The CPU sees only two registers. //! -//! Only the register file is modelled. Refresh has no observable effect on an +//! Only the register file lives here; the RAM bank itself is +//! [`crate::memory::Memory::mb_ram`]. Refresh has no observable effect on an //! emulator that never loses a DRAM cell, and page/burst/skip mode only change //! how fast a real memory cycle completes, which we do not simulate either. //! The bits are still stored and read back, because Kickstart and the @@ -84,6 +85,27 @@ impl RamseyRevision { Self::Rev7 => 4 * 1024 * 1024, } } + + /// Bytes per bank of the DRAM that would carry `total_bytes` of fitted + /// motherboard RAM: the geometry the control register should describe so + /// Kickstart's sizing probe and the diagnostic tools see parts matching + /// the RAM that answers. The board has four banks, so 256Kx4 parts + /// (1 MiB banks) cover totals up to 4 MiB and 1Mx4 parts (4 MiB banks) + /// anything larger; each machine stays on its stock part where both + /// could fit the total. Zero (no RAM fitted) falls back to the stock + /// geometry. + pub fn bank_bytes_for(self, total_bytes: usize) -> u32 { + const BANK_1M: u32 = 1024 * 1024; + const BANK_4M: u32 = 4 * 1024 * 1024; + if total_bytes == 0 { + return self.stock_bank_bytes(); + } + match self { + Self::Rev4 if total_bytes <= 4 * BANK_1M as usize => BANK_1M, + _ if total_bytes.is_multiple_of(BANK_4M as usize) => BANK_4M, + _ => BANK_1M, + } + } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -207,6 +229,33 @@ mod tests { assert_eq!(a3000.bank_bytes(), 1024 * 1024); } + /// The seeded control register must describe DRAM that could actually + /// carry the fitted total across the board's four banks: each machine + /// stays on its stock part while that part can cover the total. + #[test] + fn bank_geometry_tracks_the_fitted_total() { + const M: usize = 1024 * 1024; + // No RAM fitted: fall back to the stock geometry. + assert_eq!( + RamseyRevision::Rev4.bank_bytes_for(0), + RamseyRevision::Rev4.stock_bank_bytes() + ); + assert_eq!( + RamseyRevision::Rev7.bank_bytes_for(0), + RamseyRevision::Rev7.stock_bank_bytes() + ); + // A3000: stock 256Kx4 parts (1 MiB banks) up to full population, + // 1Mx4 beyond. + assert_eq!(RamseyRevision::Rev4.bank_bytes_for(2 * M), M as u32); + assert_eq!(RamseyRevision::Rev4.bank_bytes_for(4 * M), M as u32); + assert_eq!(RamseyRevision::Rev4.bank_bytes_for(16 * M), 4 * M as u32); + // A4000: stock 1Mx4 parts whenever whole 4 MiB banks fill, 256Kx4 + // for the sub-4M totals. + assert_eq!(RamseyRevision::Rev7.bank_bytes_for(4 * M), 4 * M as u32); + assert_eq!(RamseyRevision::Rev7.bank_bytes_for(16 * M), 4 * M as u32); + assert_eq!(RamseyRevision::Rev7.bank_bytes_for(2 * M), M as u32); + } + /// Refresh comes up at index 0 (154 clocks), which is what both diagnostic /// tools expect to see on a machine Kickstart has not reprogrammed. #[test] diff --git a/src/savestate.rs b/src/savestate.rs index 5b1db0d..f2f0cec 100644 --- a/src/savestate.rs +++ b/src/savestate.rs @@ -121,7 +121,10 @@ const STATE_MAGIC: &[u8; 8] = b"CLSSTATE"; // 34: the Z3660 RTG board was appended to the BoardDevice enum; a state // holding one cannot be read by a build without the variant, so the // shape change bumps the version -pub const STATE_VERSION: u32 = 34; +// 35: Memory gained the Ramsey-controlled motherboard fast RAM bank +// (mb_ram, ending at $08000000) and MachineDescriptor its size +// (mb_ram_bytes) +pub const STATE_VERSION: u32 = 35; /// Default state file name, timestamped like the screenshot/recorder names. pub fn auto_filename() -> std::path::PathBuf { @@ -232,6 +235,7 @@ mod tests { Memory { chip_ram: vec![0u8; 512 * 1024], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom, overlay: false, zorro: ZorroChain::default(), @@ -323,6 +327,7 @@ mod tests { Memory { chip_ram, slow_ram: Vec::new(), + mb_ram: Vec::new(), rom: vec![0u8; ROM_SIZE], overlay: false, zorro: ZorroChain::default(), @@ -569,6 +574,7 @@ mod tests { chip_ram_bytes: 2 * 1024 * 1024, fast_ram_bytes: 8 * 1024 * 1024, slow_ram_bytes: 0, + mb_ram_bytes: 4 * 1024 * 1024, chipset: crate::config::Chipset::Aga, video_standard: crate::chipset::agnus::VideoStandard::Ntsc, machine: Some(crate::config::MachineModel::A1200), diff --git a/src/video/launcher.rs b/src/video/launcher.rs index cb8156a..aa86997 100644 --- a/src/video/launcher.rs +++ b/src/video/launcher.rs @@ -248,6 +248,7 @@ pub enum LauncherField { ChipRam, FastRam, SlowRam, + MbRam, Z3Ram, // ROM Rom, @@ -440,10 +441,11 @@ const CPU_ROWS: [Row; 5] = [ row(F::Icache, "Instruction cache", Toggle), row(F::Dcache, "Data cache", Toggle), ]; -const MEMORY_ROWS: [Row; 4] = [ +const MEMORY_ROWS: [Row; 5] = [ row(F::ChipRam, "Chip RAM", Cycle), row(F::FastRam, "Fast RAM", Cycle), row(F::SlowRam, "Slow RAM", Cycle), + row(F::MbRam, "Motherboard RAM", Cycle), row(F::Z3Ram, "Zorro III RAM", Cycle), ]; const ROM_ROWS: [Row; 2] = [ @@ -677,6 +679,17 @@ const FAST_PRESETS: [usize; 9] = [ 8 * 1024 * 1024, ]; const SLOW_PRESETS: [usize; 3] = [0, 256 * 1024, 512 * 1024]; +/// Ramsey bank fills: 1M-4M on 256Kx4 parts, then whole 4M banks of 1Mx4. +const MB_PRESETS: [usize; 8] = [ + 0, + 1024 * 1024, + 2 * 1024 * 1024, + 3 * 1024 * 1024, + 4 * 1024 * 1024, + 8 * 1024 * 1024, + 12 * 1024 * 1024, + 16 * 1024 * 1024, +]; const Z3_PRESETS: [usize; 8] = [ 0, 16 * 1024 * 1024, @@ -778,6 +791,7 @@ pub struct MachineSetup { chip_ram: usize, fast_ram: usize, slow_ram: usize, + mb_ram: usize, z3_ram: usize, // ROM (None = bundled AROS for the boot ROM, none for extended) rom: Option, @@ -912,6 +926,7 @@ impl MachineSetup { chip_ram: cfg.chip_ram_bytes, fast_ram: cfg.fast_ram_bytes, slow_ram: cfg.slow_ram_bytes, + mb_ram: cfg.mb_ram_bytes, z3_ram: cfg.z3_ram_bytes, rom: raw.rom.as_deref().map(PathBuf::from), extended_rom: raw.extended_rom.as_deref().map(PathBuf::from), @@ -1118,6 +1133,9 @@ impl MachineSetup { if self.slow_ram != base.slow_ram_bytes { raw.memory.slow = Some(format_size(self.slow_ram)); } + if self.mb_ram != base.mb_ram_bytes { + raw.memory.motherboard = Some(format_size(self.mb_ram)); + } if self.z3_ram != base.z3_ram_bytes { raw.memory.z3 = Some(format_size(self.z3_ram)); } @@ -1388,6 +1406,7 @@ impl MachineSetup { self.chip_ram = base.chip_ram_bytes; self.fast_ram = base.fast_ram_bytes; self.slow_ram = base.slow_ram_bytes; + self.mb_ram = base.mb_ram_bytes; self.z3_ram = base.z3_ram_bytes; self.overscan = base.overscan; self.pixel_aspect = base.pixel_aspect; @@ -1456,6 +1475,21 @@ impl MachineSetup { F::Icache => reason(self.cpu.has_instruction_cache(), "needs 68020+"), F::Dcache => reason(self.cpu.has_data_cache(), "needs 68030/040"), F::Z3Ram => reason(cpu_is_32bit(self.cpu), "needs 32-bit CPU"), + // The Z3660 is a Zorro III board: same address-reach gate. + F::Rtg => reason(cpu_is_32bit(self.cpu), "needs 32-bit CPU"), + // Motherboard fast RAM hangs off Ramsey, which only the big-box + // profiles fit, and its bank ends beyond a 24-bit address bus. + F::MbRam => { + let big_box = matches!(self.model, Some(MachineModel::A3000 | MachineModel::A4000)); + reason( + big_box && cpu_is_32bit(self.cpu), + if big_box { + "needs 32-bit CPU" + } else { + "needs A3000/A4000" + }, + ) + } F::IdeMaster | F::IdeSlave => reason(self.has_ide(), "needs A600/A1200/A4000"), // The ROM and drives belong to the fitted controller; greyed with // none. The A3000's motherboard SCSI has no ROM of its own, and @@ -1698,6 +1732,7 @@ impl MachineSetup { F::ChipRam => size_label(self.chip_ram), F::FastRam => size_label(self.fast_ram), F::SlowRam => size_label(self.slow_ram), + F::MbRam => size_label(self.mb_ram), F::Z3Ram => size_label(self.z3_ram), F::FloppyDrives => self.floppy_drives.to_string(), F::FloppySpeed => crate::floppy::speed_label(self.floppy_speed), @@ -1851,13 +1886,20 @@ impl MachineSetup { self.dcache = self.cpu.has_data_cache(); self.clock_mhz = self.cpu.default_clock_mhz(); if !cpu_is_32bit(self.cpu) { + // Zorro III RAM, motherboard RAM, and the Zorro III RTG + // card all sit beyond a 24-bit bus; dropping them (rather + // than just greying their rows) keeps the emitted config + // launchable. self.z3_ram = 0; + self.mb_ram = 0; + self.rtg = RtgCard::None; } } F::Clock => self.clock_mhz = cycle_floats(&CLOCK_PRESETS, self.clock_mhz, forward), F::ChipRam => self.chip_ram = cycle_slice(&CHIP_PRESETS, self.chip_ram, forward), F::FastRam => self.fast_ram = cycle_nearest(&FAST_PRESETS, self.fast_ram, forward), F::SlowRam => self.slow_ram = cycle_nearest(&SLOW_PRESETS, self.slow_ram, forward), + F::MbRam => self.mb_ram = cycle_nearest(&MB_PRESETS, self.mb_ram, forward), F::Z3Ram => self.z3_ram = cycle_nearest(&Z3_PRESETS, self.z3_ram, forward), F::FloppyDrives => { self.floppy_drives = step_u8(self.floppy_drives, forward, 1, 4); @@ -3170,6 +3212,42 @@ mod tests { } } + /// Motherboard RAM tracks both of its hardware constraints: the big-box + /// model gate and the CPU's address reach. Downgrading the CPU to a + /// 24-bit part drops the profile-default bank (not just the row's + /// editability) so the emitted config still validates, and the greyed + /// row names whichever constraint bit. + #[test] + fn motherboard_ram_follows_model_and_cpu_gates() { + let mut s = MachineSetup::default(); + assert_eq!( + s.disabled_reason(LauncherField::MbRam), + Some("needs A3000/A4000") + ); + s.select_model(Some(MachineModel::A3000)); + assert!(s.applies(LauncherField::MbRam)); + assert_eq!(s.mb_ram, 4 * 1024 * 1024); + while s.cpu != CpuModel::M68000 { + s.cycle(LauncherField::Cpu, true); + } + assert_eq!(s.mb_ram, 0); + assert_eq!( + s.disabled_reason(LauncherField::MbRam), + Some("needs 32-bit CPU") + ); + // The profile's Zorro III RTG card is beyond a 24-bit bus too. + assert_eq!(s.rtg, RtgCard::None); + assert_eq!( + s.disabled_reason(LauncherField::Rtg), + Some("needs 32-bit CPU") + ); + // The raw config overrides the profile default back to zero, so + // this machine still launches. + assert_eq!(s.to_raw().memory.motherboard.as_deref(), Some("0")); + s.build_config() + .expect("68000 A3000 with no mb RAM validates"); + } + #[test] fn port_devices_round_trip_through_raw_against_the_profile_baseline() { let mut s = MachineSetup::default(); diff --git a/src/video/window/tests.rs b/src/video/window/tests.rs index 63e84e4..7588c97 100644 --- a/src/video/window/tests.rs +++ b/src/video/window/tests.rs @@ -1860,6 +1860,7 @@ fn test_app_with_audio(audio: Box) -> super::App { let mem = Memory { chip_ram: vec![0u8; 512 * 1024], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom, overlay: true, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/wasmboard.rs b/src/wasmboard.rs index a0b0b82..f0f0aae 100644 --- a/src/wasmboard.rs +++ b/src/wasmboard.rs @@ -711,6 +711,7 @@ mod tests { Memory { chip_ram: vec![0u8; 0x1000], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/z3660.rs b/src/z3660.rs index 010aa88..6e6a2d5 100644 --- a/src/z3660.rs +++ b/src/z3660.rs @@ -1099,6 +1099,7 @@ mod tests { Memory { chip_ram: vec![0u8; 0x100], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), @@ -1273,6 +1274,7 @@ mod exec_tests { Memory { chip_ram: vec![0u8; 0x100], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), @@ -1605,6 +1607,7 @@ mod sprite_tests { Memory { chip_ram: vec![0u8; 0x100], slow_ram: Vec::new(), + mb_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/zorro_device.rs b/src/zorro_device.rs index e93036a..feb332c 100644 --- a/src/zorro_device.rs +++ b/src/zorro_device.rs @@ -480,6 +480,7 @@ mod tests { Memory { chip_ram: vec![0u8; chip], slow_ram: vec![0u8; slow], + mb_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: ZorroChain::default(), diff --git a/tests/mb_ram.rs b/tests/mb_ram.rs new file mode 100644 index 0000000..db2f474 --- /dev/null +++ b/tests/mb_ram.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Asset-gated check that a real Kickstart sizes the Ramsey motherboard +//! fast RAM: boots the A4000 profile with the local Kickstart 3.1 A4000 +//! ROM and walks exec's memory list for the bank ending at $08000000. +//! Motherboard RAM is not autoconfig -- exec's own probe reads the Ramsey +//! control register for the DRAM geometry and pattern-tests the banks -- +//! so a listed MemHeader proves the decode, the register seeding, and the +//! Fat Gary timeout on the unfitted space all line up. + +use copperline::config::{Config, ConfigOverrides}; + +/// The integration-test asset directory (see `tests/README.md`): +/// `COPPERLINE_TEST_ASSETS`, else `test-assets/` under the repo root, +/// else the repo root itself. +fn asset(name: &str) -> Option { + let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let dir = match std::env::var_os("COPPERLINE_TEST_ASSETS") { + Some(d) => std::path::PathBuf::from(d), + None => { + let d = root.join("test-assets"); + if d.is_dir() { + d + } else { + root + } + } + }; + let path = dir.join(name); + path.is_file().then_some(path) +} + +fn peek32(bus: &copperline::bus::Bus, addr: u32) -> u32 { + (u32::from(bus.peek_word_any(addr)) << 16) | u32::from(bus.peek_word_any(addr + 2)) +} + +#[test] +#[ignore = "needs the local Kickstart 3.1 A4000 ROM (see tests/README.md)"] +fn kickstart_sizes_the_motherboard_ram_bank() { + let Some(rom) = asset("Kickstart v3.1 r40.68 (1993)(Commodore)(A4000).rom") else { + eprintln!("skipping: Kickstart 3.1 A4000 ROM not in the asset directory"); + return; + }; + let overrides = ConfigOverrides { + model: Some("A4000".into()), + ..Default::default() + }; + let raw = Config::load_raw(None, &overrides).expect("raw config"); + let cfg = Config::try_from(raw) + .expect("A4000 profile validates") + .with_rom_override(Some(rom)); + assert_eq!(cfg.mb_ram_bytes, 4 * 1024 * 1024); + let mut emu = copperline::emulator::build_machine( + &cfg, + Box::new(copperline::audio::NullSink), + false, + false, + ) + .expect("machine builds"); + // Exec sizes memory in the first moments of boot; run well past it. + for _ in 0..400 { + emu.step_frame().expect("frame"); + } + let bus = emu.bus(); + // Kickstart moves ExecBase into the best memory it found, which is the + // motherboard bank ($07C00000-$08000000 with the stock 4 MiB). + let execbase = peek32(bus, 4); + assert_eq!( + execbase & 0xFFC0_0000, + 0x07C0_0000, + "ExecBase ${execbase:08X} did not move into the motherboard bank" + ); + // Walk ExecBase->MemList for the MemHeader covering the bank. + let mut node = peek32(bus, execbase + 322); // lh_Head + let mut bank = None; + while node != 0 { + let succ = peek32(bus, node); // ln_Succ; 0 at the lh_Tail sentinel + if succ == 0 { + break; + } + let lower = peek32(bus, node + 20); // mh_Lower + let upper = peek32(bus, node + 24); // mh_Upper + if upper == 0x0800_0000 { + bank = Some(lower); + } + node = succ; + } + let lower = bank.expect("exec lists a MemHeader ending at $08000000"); + // The bank starts at $07C00000; exec claims a little for its header. + assert_eq!( + lower & 0xFFF0_0000, + 0x07C0_0000, + "mh_Lower ${lower:08X} is not the top-down 4 MiB bank" + ); +}