diff --git a/copperline.example.toml b/copperline.example.toml index f321f98..f00556b 100644 --- a/copperline.example.toml +++ b/copperline.example.toml @@ -191,9 +191,19 @@ slow = "512K" # 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. +# 8M/12M/16M. On the A4000 (Ramsey-07), sizes beyond 16M fill the +# $04000000-$06FFFFFF motherboard RAM expansion space in 4M steps up +# to 64M. Set to 0 to remove it. # motherboard = "16M" +# CPU-slot (accelerator) fast RAM: 32-bit local RAM starting at $08000000 +# and growing upward through the coprocessor-slot space, up to 128M +# (ending at $10000000, where Zorro III space begins). Sized by +# Kickstart's own probe rather than autoconfig; the big-box Kickstarts +# scan for it, elsewhere guest software has to AddMem it. Needs a CPU +# with a 32-bit address bus. Any whole number of megabytes. +# accelerator = "64M" + # 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 c678dd4..c9e4999 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -43,7 +43,8 @@ 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` | +| `--motherboard SIZE` | `[memory] motherboard` | Ramsey RAM (A3000/A4000): `0`, `1M`..`4M`, `8M`, `12M`, `16M`; A4000 up to `64M` | +| `--accelerator SIZE` | `[memory] accelerator` | CPU-slot RAM at `$08000000` (32-bit CPUs): `0` to `128M` | | `--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` | @@ -180,7 +181,10 @@ drives), and setting the flag explicitly wins in either direction. The ROM file itself is never modified. Both profiles fit their stock 4M of Ramsey-controlled motherboard fast RAM; -`[memory] motherboard` resizes it up to 16M (see the `[memory]` section). +`[memory] motherboard` resizes it up to 16M, and on the A4000 up to 64M +via the motherboard RAM expansion space (see the `[memory]` section). +`[memory] accelerator` adds CPU-slot RAM at `$08000000` on any 32-bit +machine. `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 @@ -315,7 +319,8 @@ clock_mhz = 14.0 # optional; defaults to the model's stock speed 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 +motherboard = "0" # Ramsey motherboard RAM (A3000/A4000): up to 16M (A4000: 64M) +accelerator = "0" # CPU-slot RAM at $08000000 (32-bit CPUs): up to 128M z3 = "0" # Zorro III RAM (needs a 32-bit CPU): 64K..1G, power of two ``` @@ -335,7 +340,16 @@ plain byte counts, and must be multiples of 4 KiB. 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. + 1M steps, or 8M, 12M, 16M. On the A4000 (Ramsey-07), sizes beyond 16M + keep growing downward into the `$04000000`-`$06FFFFFF` motherboard RAM + expansion space, in 4M steps up to 64M (which reaches `$04000000`). + Set `motherboard = "0"` to remove it. +- **Accelerator RAM** is CPU-slot local memory: it starts at `$08000000` + and grows upward through the coprocessor-slot expansion space, up to + 128M (ending at `$10000000`, where Zorro III space begins). This is the + RAM an accelerator/CPU board carries, so it needs a 32-bit CPU but no + particular machine profile; any whole number of megabytes fits. + Kickstart sizes it with its own probe, like the motherboard bank. - **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/internals/peripherals.md b/docs/internals/peripherals.md index 2801dae..dcf7772 100644 --- a/docs/internals/peripherals.md +++ b/docs/internals/peripherals.md @@ -45,6 +45,17 @@ geometry bits are seeded to describe DRAM parts matching the fitted RAM size so sizing probes agree with the RAM that answers. Only the register file lives in `ramsey.rs`; the RAM bank itself is `memory::Memory::mb_ram`. +Beyond Ramsey's four banks the big-box memory map reserves +`$04000000`-`$06FFFFFF` for motherboard RAM expansion; on the A4000 +profile the bank keeps growing downward through it, up to 64 MiB at +`$04000000`, still sized by Kickstart's top-down probe (the control +register keeps describing the fully populated 1Mx4 geometry -- it has +no way to say more). The complementary `[memory] accelerator` bank +models CPU-slot local RAM: it starts at `$08000000` and grows upward +through the coprocessor-slot space, up to 128 MiB at `$10000000` where +Zorro III space begins, gated only on a 32-bit CPU +(`memory::Memory::accel_ram`). + ## Gayle IDE (`gayle.rs`) A600/A1200 machines get the Gayle gate array: the ID register at diff --git a/src/a2065.rs b/src/a2065.rs index 232cc13..284a571 100644 --- a/src/a2065.rs +++ b/src/a2065.rs @@ -556,6 +556,7 @@ mod tests { chip_ram: vec![0u8; 0x100], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/a2091.rs b/src/a2091.rs index 9a5f6be..0e8e334 100644 --- a/src/a2091.rs +++ b/src/a2091.rs @@ -482,6 +482,7 @@ mod tests { chip_ram: vec![0u8; 512 * 1024], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: ZorroChain::default(), diff --git a/src/a4091.rs b/src/a4091.rs index c1fa6ef..ec2d516 100644 --- a/src/a4091.rs +++ b/src/a4091.rs @@ -1009,6 +1009,7 @@ mod tests { chip_ram: vec![0u8; 512 * 1024], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: ZorroChain::default(), diff --git a/src/bus.rs b/src/bus.rs index 60be527..3db0087 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, motherboard fast RAM, and configured Zorro RAM - /// boards, as (base, length) pairs. + /// slow/ranger RAM, motherboard fast RAM, accelerator (CPU-slot) 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() { @@ -5349,15 +5349,21 @@ impl Bus { if !self.mem.mb_ram.is_empty() { regions.push((self.mem.mb_ram_base() as u32, self.mem.mb_ram.len() as u32)); } + if !self.mem.accel_ram.is_empty() { + regions.push(( + crate::memory::ACCEL_RAM_BASE as u32, + self.mem.accel_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, motherboard, or ROM), for the debugger's - /// memory dumps. Returns 0 for unmapped addresses. + /// `addr` (chip, fast, slow, motherboard, accelerator, 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}; + use crate::memory::{ACCEL_RAM_BASE, CHIP_RAM_BASE, ROM_BASE, SLOW_RAM_BASE}; if let Some((board, off)) = self.mem.zorro.region_at(addr, 2) { let ram = self.mem.zorro.board_ram(board); return ((ram[off] as u16) << 8) | ram[off + 1] as u16; @@ -5372,10 +5378,11 @@ impl Bus { .unwrap_or(0); } let a = addr as usize; - let regions: [(usize, &[u8]); 5] = [ + let regions: [(usize, &[u8]); 6] = [ (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), + (ACCEL_RAM_BASE as usize, &self.mem.accel_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 63bed8e..4714aa8 100644 --- a/src/bus/tests.rs +++ b/src/bus/tests.rs @@ -216,6 +216,7 @@ fn empty_bus_with_chip_ram(chip_ram_bytes: usize) -> Bus { chip_ram: vec![0; chip_ram_bytes], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom: vec![0; 512 * 1024], overlay: true, zorro: crate::zorro::ZorroChain::default(), @@ -236,6 +237,7 @@ fn empty_bus_with_collect_audio() -> (Bus, SharedFrames) { chip_ram: vec![0; 512 * 1024], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom: vec![0; 512 * 1024], overlay: true, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/cdtv.rs b/src/cdtv.rs index 5020bde..61ed02c 100644 --- a/src/cdtv.rs +++ b/src/cdtv.rs @@ -1340,6 +1340,7 @@ mod tests { chip_ram: vec![0u8; chip_bytes], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/config.rs b/src/config.rs index 76d71d3..e49971f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -63,8 +63,16 @@ pub struct Config { /// 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. + /// A3000/A4000 profiles fit 4 MiB by default. Ramsey's four banks stop + /// at 16 MiB; larger totals (up to 64 MiB, Ramsey-07/A4000 only) fill + /// the $04000000-$06FFFFFF motherboard RAM expansion space below them. pub mb_ram_bytes: usize, + /// CPU-slot (accelerator) fast RAM (`[memory] accelerator`): 32-bit + /// local RAM in the big-box coprocessor-slot space, starting at + /// $08000000 and growing upward (up to 128 MiB, ending where Zorro III + /// space begins), sized by Kickstart's own probe rather than autoconfig. + /// Needs a CPU with a 32-bit address bus. + pub accel_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, @@ -886,6 +894,9 @@ pub struct MachineDescriptor { /// Ramsey-controlled motherboard fast RAM (A3000/A4000). #[serde(default)] pub mb_ram_bytes: usize, + /// CPU-slot (accelerator) fast RAM at $08000000. + #[serde(default)] + pub accel_ram_bytes: usize, pub chipset: Chipset, pub video_standard: VideoStandard, pub machine: Option, @@ -907,6 +918,7 @@ impl Default for MachineDescriptor { fast_ram_bytes: 0, slow_ram_bytes: 0, mb_ram_bytes: 0, + accel_ram_bytes: 0, chipset: Chipset::Ocs, video_standard: VideoStandard::Pal, machine: None, @@ -937,7 +949,7 @@ impl MachineDescriptor { None => String::new(), }; format!( - "{profile} / {:?} / {:?} / {:?} / chip {}K fast {}K slow {}K mb {}K / ROM {}{ext}", + "{profile} / {:?} / {:?} / {:?} / chip {}K fast {}K slow {}K mb {}K accel {}K / ROM {}{ext}", self.cpu, self.chipset, self.video_standard, @@ -945,6 +957,7 @@ impl MachineDescriptor { self.fast_ram_bytes / 1024, self.slow_ram_bytes / 1024, self.mb_ram_bytes / 1024, + self.accel_ram_bytes / 1024, self.rom.label(), ) } @@ -997,6 +1010,13 @@ impl MachineDescriptor { other.mb_ram_bytes / 1024 )); } + if self.accel_ram_bytes != other.accel_ram_bytes { + diffs.push(format!( + "accelerator RAM {}K -> {}K", + self.accel_ram_bytes / 1024, + other.accel_ram_bytes / 1024 + )); + } if self.rom != other.rom { diffs.push(format!("ROM {} -> {}", self.rom.label(), other.rom.label())); } @@ -1097,6 +1117,7 @@ impl Default for Config { fast_ram_bytes: 0, slow_ram_bytes: A500_TRAPDOOR_RAM_BYTES, mb_ram_bytes: 0, + accel_ram_bytes: 0, z3_ram_bytes: 0, zorro_boards: Vec::new(), wasm_boards: Vec::new(), @@ -1189,6 +1210,7 @@ impl Config { fast_ram_bytes: self.fast_ram_bytes, slow_ram_bytes: self.slow_ram_bytes, mb_ram_bytes: self.mb_ram_bytes, + accel_ram_bytes: self.accel_ram_bytes, chipset: self.chipset, video_standard: self.video_standard, machine: self.machine, @@ -1281,6 +1303,9 @@ pub struct ConfigOverrides { /// Ramsey motherboard fast RAM size (`--motherboard`). Same parser as /// `[memory] motherboard`. pub motherboard: Option, + /// CPU-slot accelerator fast RAM size (`--accelerator`). Same parser as + /// `[memory] accelerator`. + pub accelerator: 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`. @@ -1344,6 +1369,7 @@ impl ConfigOverrides { && self.fast.is_none() && self.slow.is_none() && self.motherboard.is_none() + && self.accelerator.is_none() && self.floppy_drives.is_none() && self.floppy_speed.is_none() && self.joystick.is_none() @@ -1394,6 +1420,9 @@ impl ConfigOverrides { if let Some(motherboard) = &self.motherboard { raw.memory.motherboard = Some(motherboard.clone()); } + if let Some(accelerator) = &self.accelerator { + raw.memory.accelerator = Some(accelerator.clone()); + } if let Some(drives) = self.floppy_drives { raw.floppy.drives = Some(drives); } @@ -1877,9 +1906,15 @@ pub(crate) struct RawMemory { #[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. + /// Ramsey (A3000/A4000 profiles) and a 32-bit CPU. Sizes beyond 16M + /// (up to 64M) fill the motherboard RAM expansion space and need the + /// A4000's Ramsey-07. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) motherboard: Option, + /// CPU-slot (accelerator) fast RAM size at $08000000 (e.g. "64M", up + /// to 128M); 32-bit CPUs only. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) accelerator: Option, /// Zorro III autoconfig RAM size (e.g. "16M"); 32-bit CPUs only. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) z3: Option, @@ -2170,6 +2205,10 @@ impl TryFrom for Config { None => defaults.mb_ram_bytes, Some(s) => parse_size(s, "motherboard RAM")?, }; + let accel_ram_bytes = match raw.memory.accelerator.as_deref() { + None => defaults.accel_ram_bytes, + Some(s) => parse_size(s, "accelerator RAM")?, + }; let z3_ram_bytes = match raw.memory.z3.as_deref() { None => defaults.z3_ram_bytes, Some(s) => parse_size(s, "Zorro III RAM")?, @@ -2470,6 +2509,7 @@ impl TryFrom for Config { 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_accel_ram(accel_ram_bytes, 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 @@ -2545,6 +2585,7 @@ impl TryFrom for Config { fast_ram_bytes, slow_ram_bytes, mb_ram_bytes, + accel_ram_bytes, z3_ram_bytes, zorro_boards, wasm_boards, @@ -3129,8 +3170,11 @@ fn validate_fast_ram(fast: usize, chip: usize) -> Result<()> { /// 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. +/// 1M-4M in 1M steps or 4M/8M/12M/16M. Beyond the four banks the big-box +/// memory map reserves $04000000-$06FFFFFF for motherboard RAM expansion; +/// filling it (whole 4M steps up to 64M) is an A4000/Ramsey-07 option, +/// sized by the same top-down Kickstart probe. 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; @@ -3151,18 +3195,61 @@ fn validate_mb_ram(mb: usize, mem_controller: MemController, cpu: CpuModel) -> R cpu ); } + if mb > 4 * BANK_4M { + if mem_controller.ramsey_revision() != Some(crate::ramsey::RamseyRevision::Rev7) { + bail!( + "motherboard RAM beyond 16M fills the $04000000-$06FFFFFF \ + expansion space, an A4000 option (needs \ + [machine] mem_controller = \"ramsey-07\")" + ); + } + if !mb.is_multiple_of(BANK_4M) || mb > crate::memory::MB_RAM_MAX { + bail!( + "motherboard RAM {} bytes does not fill the expansion space \ + in whole 4M banks (20M-64M in 4M steps)", + mb + ); + } + return Ok(()); + } 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)", + (1M-4M in 1M steps, or 8M, 12M, 16M; the A4000 extends \ + in 4M steps to 64M)", mb ); } Ok(()) } +/// CPU-slot (accelerator) fast RAM occupies $08000000-$0FFFFFFF, which only +/// a 32-bit address bus reaches. The bank is whatever DRAM the CPU board +/// carries, so any whole number of megabytes up to the 128M slot space fits. +fn validate_accel_ram(accel: usize, cpu: CpuModel) -> Result<()> { + const MB: usize = 1024 * 1024; + if accel == 0 { + return Ok(()); + } + if !cpu_has_32bit_bus(cpu) { + bail!( + "accelerator RAM sits at $08000000-$0FFFFFFF, beyond a 24-bit \ + address bus: {:?} cannot reach it (needs a 68020/68030/68040/68060)", + cpu + ); + } + if !accel.is_multiple_of(MB) || accel > crate::memory::ACCEL_RAM_MAX { + bail!( + "accelerator RAM {} bytes is not a whole number of megabytes \ + up to the 128M CPU-slot space", + accel + ); + } + Ok(()) +} + fn cpu_has_32bit_bus(cpu: CpuModel) -> bool { matches!( cpu, @@ -3352,6 +3439,9 @@ pub fn about_machine_lines(cfg: &Config) -> Vec { if cfg.mb_ram_bytes > 0 { ram.push_str(&format!(", {}K motherboard", cfg.mb_ram_bytes / 1024)); } + if cfg.accel_ram_bytes > 0 { + ram.push_str(&format!(", {}K accelerator", cfg.accel_ram_bytes / 1024)); + } if cfg.z3_ram_bytes > 0 { ram.push_str(&format!(", {}K Z3", cfg.z3_ram_bytes / 1024)); } @@ -3611,6 +3701,7 @@ mod tests { fast: Some("8M".to_string()), slow: None, motherboard: None, + accelerator: None, z3: None, }, chipset: RawChipset { @@ -4544,6 +4635,102 @@ mod tests { Ok(()) } + /// Beyond Ramsey's four banks the A4000 fills the $04000000-$06FFFFFF + /// motherboard RAM expansion space in whole 4M banks up to 64M; the + /// A3000's Ramsey-04 does not, and partial banks are refused. + #[test] + fn motherboard_ram_expansion_space_is_an_a4000_option() -> Result<()> { + let cfg = parse_config( + r#" + [machine] + profile = "A4000" + [memory] + motherboard = "64M" + "#, + )?; + assert_eq!(cfg.mb_ram_bytes, 64 * 1024 * 1024); + let cfg = parse_config( + r#" + [machine] + profile = "A4000" + [memory] + motherboard = "20M" + "#, + )?; + assert_eq!(cfg.mb_ram_bytes, 20 * 1024 * 1024); + + // The A3000 stops at Ramsey's own 16M. + let err = parse_config( + r#" + [machine] + profile = "A3000" + [memory] + motherboard = "32M" + "#, + ) + .unwrap_err(); + assert!(err.to_string().contains("A4000 option"), "{err:#}"); + + // Partial expansion banks and totals past the window are refused. + for size in ["18M", "65M", "128M"] { + let err = parse_config(&format!( + "[machine]\nprofile = \"A4000\"\n[memory]\nmotherboard = \"{size}\"" + )) + .unwrap_err(); + assert!(err.to_string().contains("expansion space"), "{err:#}"); + } + Ok(()) + } + + /// Accelerator (CPU-slot) RAM at $08000000 needs only a 32-bit address + /// bus: any megabyte total up to the 128M slot space, on any machine. + #[test] + fn accelerator_ram_gates_on_the_cpu_bus() -> Result<()> { + let cfg = parse_config( + r#" + [cpu] + model = "68030" + [memory] + accelerator = "128M" + "#, + )?; + assert_eq!(cfg.accel_ram_bytes, 128 * 1024 * 1024); + // Not tied to the big-box profiles: an accelerated A1200 counts. + let cfg = parse_config( + r#" + [machine] + profile = "A1200" + [cpu] + model = "68030" + [memory] + accelerator = "64M" + "#, + )?; + assert_eq!(cfg.accel_ram_bytes, 64 * 1024 * 1024); + + // The stock A1200 EC020 has a 24-bit bus. + let err = parse_config( + r#" + [machine] + profile = "A1200" + [memory] + accelerator = "64M" + "#, + ) + .unwrap_err(); + assert!(err.to_string().contains("24-bit"), "{err:#}"); + + // Sub-megabyte and beyond-the-slot totals are refused. + for size in ["512K", "129M"] { + let err = parse_config(&format!( + "[cpu]\nmodel = \"68030\"\n[memory]\naccelerator = \"{size}\"" + )) + .unwrap_err(); + assert!(err.to_string().contains("CPU-slot space"), "{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 7503764..d73143c 100644 --- a/src/control.rs +++ b/src/control.rs @@ -183,6 +183,7 @@ pub(crate) fn test_emulator() -> crate::emulator::Emulator { chip_ram, slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom, overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/cpu.rs b/src/cpu.rs index cb8f82a..575bce8 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -6,8 +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, CHIP_WINDOW_SIZE, ROM_BASE, SLOW_RAM_BASE, - WCS_BASE, + ACCEL_RAM_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}; @@ -2219,7 +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, motherboard RAM, ROM, WCS, extended ROM -- is only +/// Zorro RAM, slow RAM, motherboard RAM, accelerator 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 @@ -2230,6 +2231,7 @@ enum PlainMemRegion { ZorroRam(usize, usize), SlowRam(usize), MbRam(usize), + AccelRam(usize), Rom(usize), Wcs(usize), ExtendedRom(usize), @@ -2261,6 +2263,9 @@ impl CpuBus { ) { return Some(PlainMemRegion::MbRam(off)); } + if let Some(off) = region_offset(self.bus.mem.accel_ram.len(), ACCEL_RAM_BASE, addr, size) { + return Some(PlainMemRegion::AccelRam(off)); + } if let Some(off) = region_offset(self.bus.mem.rom.len(), ROM_BASE, addr, size) { return Some(PlainMemRegion::Rom(off)); } @@ -2413,6 +2418,7 @@ impl CpuBus { 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::AccelRam(off)) => self.bus.mem.accel_ram[off], Some(PlainMemRegion::Wcs(off)) => self.bus.mem.wcs[off], Some(PlainMemRegion::ExtendedRom(off)) => self.bus.mem.extended_rom[off], None => 0xFF, @@ -2442,6 +2448,11 @@ impl CpuBus { self.invalidate_debug_write(addr, 1); true } + Some(PlainMemRegion::AccelRam(off)) => { + self.bus.mem.accel_ram[off] = value; + self.invalidate_debug_write(addr, 1); + true + } // Overlay ROM/ROM/WCS/extended ROM are not debugger-writable. _ => false, } @@ -2576,6 +2587,7 @@ impl CpuBus { 1, ) .is_some() + || region_offset(self.bus.mem.accel_ram.len(), ACCEL_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(), @@ -2586,9 +2598,9 @@ impl CpuBus { .is_some() } - /// 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. + /// What the data cache may hold: expansion RAM, motherboard RAM, + /// accelerator 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; @@ -2601,6 +2613,7 @@ impl CpuBus { 1, ) .is_some() + || region_offset(self.bus.mem.accel_ram.len(), ACCEL_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(), @@ -2635,6 +2648,12 @@ impl CpuBus { self.bus.cpu_external_access(Self::access_words(size)); return read_be(&self.bus.mem.mb_ram, off, size); } + Some(PlainMemRegion::AccelRam(off)) => { + // CPU-slot (accelerator) fast RAM: local RAM on the CPU + // board, off the chip bus, at uncontended external speed. + self.bus.cpu_external_access(Self::access_words(size)); + return read_be(&self.bus.mem.accel_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 @@ -2865,6 +2884,11 @@ impl CpuBus { write_be(&mut self.bus.mem.mb_ram, off, size, value); return; } + Some(PlainMemRegion::AccelRam(off)) => { + self.bus.cpu_external_access(Self::access_words(size)); + write_be(&mut self.bus.mem.accel_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 @@ -3395,6 +3419,7 @@ mod tests { chip_ram, slow_ram: vec![0; 64 * 1024], mb_ram: Vec::new(), + accel_ram: Vec::new(), rom, overlay: false, zorro, @@ -3488,6 +3513,43 @@ mod tests { Ok(()) } + /// CPU-slot (accelerator) RAM decodes from the bottom of the + /// coprocessor-slot space at $08000000 for a 32-bit CPU: stores land in + /// the bank and read back through the same decode. Like the motherboard + /// bank, it 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 accelerator_ram_decodes_at_the_bottom_of_the_cpu_slot_space() -> Result<()> { + let program = [ + 0x23FCu16, 0xCAFE, 0xBABE, 0x0800, 0x0000, // move.l #$CAFEBABE,$08000000 + 0x2039, 0x0800, 0x0000, // move.l $08000000,d0 + 0x23C0, 0x0000, 0x1000, // move.l d0,$1000 + 0x60FE, // bra.s * + ]; + + let mut bus = test_bus_with_pc(0x00F8_0100); + bus.mem.fit_accel_ram(2 * 1024 * 1024); + 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; + assert_eq!(&mem.accel_ram[..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(0x0800_0000), 0xCAFE); + + let mut bus = test_bus_with_pc(0x00F8_0100); + bus.mem.fit_accel_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; + assert_eq!(&mem.accel_ram[..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"); @@ -3756,6 +3818,7 @@ mod tests { chip_ram: vec![0; 512 * 1024], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom, overlay: true, zorro: ZorroChain::default(), diff --git a/src/emulator.rs b/src/emulator.rs index 009936f..10e7246 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -1985,6 +1985,15 @@ pub fn build_machine( crate::memory::MB_RAM_TOP - 1 ); } + if cfg.accel_ram_bytes > 0 { + mem.fit_accel_ram(cfg.accel_ram_bytes); + info!( + "cpu slot: {}K accelerator fast RAM at {:#010X}-{:#010X}", + cfg.accel_ram_bytes / 1024, + crate::memory::ACCEL_RAM_BASE, + crate::memory::ACCEL_RAM_BASE + cfg.accel_ram_bytes as u64 - 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 @@ -2563,6 +2572,7 @@ mod tests { chip_ram: vec![0u8; 512 * 1024], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom, overlay: false, zorro: crate::zorro::ZorroChain::default(), @@ -2619,6 +2629,7 @@ mod tests { chip_ram, slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom, overlay: false, zorro: crate::zorro::ZorroChain::default(), @@ -2675,6 +2686,7 @@ mod tests { chip_ram, slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom, overlay: false, zorro: crate::zorro::ZorroChain::default(), @@ -2838,6 +2850,7 @@ mod tests { chip_ram, slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom, overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/filesys.rs b/src/filesys.rs index c672c9d..6880c9b 100644 --- a/src/filesys.rs +++ b/src/filesys.rs @@ -2258,6 +2258,7 @@ mod tests { chip_ram: vec![0u8; 0x1_0000], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/gdbstub.rs b/src/gdbstub.rs index b5d71c7..5563f67 100644 --- a/src/gdbstub.rs +++ b/src/gdbstub.rs @@ -1219,6 +1219,7 @@ mod tests { chip_ram, slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom, overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/main.rs b/src/main.rs index 747ecef..b8e3f65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -397,6 +397,12 @@ where anyhow!("--motherboard requires a size (e.g. 0, 4M, 16M)") })?); } + "--accelerator" => { + overrides.accelerator = + Some(args.next().ok_or_else(|| { + anyhow!("--accelerator requires a size (e.g. 0, 32M, 128M)") + })?); + } "--floppy-drives" | "--fdd-drives" => { let value = args .next() @@ -904,7 +910,10 @@ 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 \ + --motherboard SIZE Ramsey motherboard fast RAM (A3000/A4000), e.g. 0, 4M,\n \ + \x20 16M; the A4000 extends to 64M\n \ + --accelerator SIZE CPU-slot accelerator fast RAM at $08000000 (32-bit\n \ + \x20 CPUs), e.g. 0, 32M, 128M\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 \ @@ -1644,6 +1653,7 @@ fn build_placeholder_machine() -> Result { chip_ram: vec![0u8; 512 * 1024], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom, overlay: true, zorro: copperline::zorro::ZorroChain::default(), diff --git a/src/memory.rs b/src/memory.rs index 6b82b41..bb6a51e 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -28,10 +28,26 @@ pub const SLOW_RAM_BASE: u64 = 0x00C0_0000; /// 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; +/// Ramsey's own four banks of 1Mx4 parts stop at 16 MiB ($07000000-$07FFFFFF), +/// but the big-box memory map reserves the whole $04000000-$07FFFFFF window +/// for motherboard RAM expansion, and Kickstart's top-down sizing probe walks +/// all of it. Fitting more than [`MB_RAM_RAMSEY_MAX`] models an expanded +/// motherboard decode filling that window (A4000/Ramsey-07 only; config +/// validation enforces the gate). The cap keeps the [`Memory::mb_ram_base`] +/// subtraction from ever underflowing. +pub const MB_RAM_MAX: usize = 64 * 1024 * 1024; +/// The most motherboard RAM Ramsey itself can drive: four 4 MiB banks of +/// 1Mx4 parts. Totals beyond this spill into the expansion window below +/// $07000000 (see [`MB_RAM_MAX`]). +pub const MB_RAM_RAMSEY_MAX: usize = 16 * 1024 * 1024; +/// Base of the CPU-slot (accelerator) fast RAM: the $08000000-$0FFFFFFF +/// coprocessor-slot expansion space of the big-box memory map, where +/// accelerator boards carry their local RAM. The bank starts here and grows +/// upward; Kickstart's sizing probe scans the space bottom-up. +pub const ACCEL_RAM_BASE: u64 = 0x0800_0000; +/// The whole coprocessor-slot space: $08000000 up to $10000000, where the +/// Zorro III expansion space begins. +pub const ACCEL_RAM_MAX: usize = 128 * 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 @@ -95,6 +111,11 @@ pub struct Memory { /// 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, + /// CPU-slot (accelerator) fast RAM: 32-bit local RAM in the big-box + /// coprocessor-slot space, starting at [`ACCEL_RAM_BASE`] and growing + /// upward. Empty when no accelerator RAM is fitted; fitted via + /// [`Memory::fit_accel_ram`]. + pub accel_ram: Vec, pub rom: Vec, pub overlay: bool, /// Zorro expansion boards (autoconfig chain plus their RAM windows). @@ -184,6 +205,7 @@ impl Memory { chip_ram: vec![0u8; chip_ram_bytes], slow_ram: vec![0u8; slow_ram_bytes], mb_ram: Vec::new(), + accel_ram: Vec::new(), rom, overlay: true, zorro, @@ -202,7 +224,7 @@ impl Memory { 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" + "motherboard RAM {bytes} bytes exceeds the {MB_RAM_MAX}-byte expansion window" ); self.mb_ram = vec![0u8; bytes]; } @@ -213,6 +235,19 @@ impl Memory { MB_RAM_TOP - self.mb_ram.len() as u64 } + /// Fit `bytes` of CPU-slot (accelerator) fast RAM (see + /// [`Memory::accel_ram`]). Zero removes the bank. Panics beyond + /// [`ACCEL_RAM_MAX`]: config validation rejects such sizes long before + /// this, and enforcing the bound at the only mutation point keeps the + /// bank inside the coprocessor-slot space. + pub fn fit_accel_ram(&mut self, bytes: usize) { + assert!( + bytes <= ACCEL_RAM_MAX, + "accelerator RAM {bytes} bytes exceeds the {ACCEL_RAM_MAX}-byte CPU-slot space" + ); + self.accel_ram = vec![0u8; bytes]; + } + /// 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<()> { @@ -245,6 +280,7 @@ impl Memory { self.chip_ram.fill(0); self.slow_ram.fill(0); self.mb_ram.fill(0); + self.accel_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. @@ -314,15 +350,44 @@ mod tests { 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. + /// The only mutation point enforces the 64 MiB expansion-window 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() { + #[should_panic(expected = "expansion window")] + fn mb_ram_beyond_the_expansion_window_is_refused() { let mut mem = Memory::placeholder(1024, 0, ZorroChain::default()); mem.fit_mb_ram(MB_RAM_MAX + 1); } + /// A full 64 MiB motherboard fit reaches the bottom of the expansion + /// window at $04000000. + #[test] + fn mb_ram_expansion_window_reaches_04000000() { + let mut mem = Memory::placeholder(1024, 0, ZorroChain::default()); + mem.fit_mb_ram(MB_RAM_MAX); + assert_eq!(mem.mb_ram_base(), 0x0400_0000); + } + + #[test] + fn accel_ram_starts_at_its_base_and_clears_on_power_on() { + let mut mem = Memory::placeholder(1024, 0, ZorroChain::default()); + mem.fit_accel_ram(16 * 1024 * 1024); + assert_eq!(mem.accel_ram.len(), 16 * 1024 * 1024); + mem.accel_ram[0] = 0xAA; + mem.power_on_reset(); + assert_eq!(mem.accel_ram[0], 0); + assert_eq!(mem.accel_ram.len(), 16 * 1024 * 1024); + } + + /// The CPU-slot space ends at $10000000 where Zorro III begins; the only + /// mutation point refuses a bank that would cross it. + #[test] + #[should_panic(expected = "CPU-slot space")] + fn accel_ram_beyond_the_cpu_slot_space_is_refused() { + let mut mem = Memory::placeholder(1024, 0, ZorroChain::default()); + mem.fit_accel_ram(ACCEL_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 0f59719..ad6b00d 100644 --- a/src/ramsey.rs +++ b/src/ramsey.rs @@ -93,7 +93,10 @@ impl RamseyRevision { /// (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. + /// geometry. Totals beyond the four banks (motherboard expansion RAM + /// below $07000000) keep the fully-populated 1Mx4 description: the + /// control register has no way to describe the expansion decode, and + /// on real hardware it would not go through Ramsey's geometry either. pub fn bank_bytes_for(self, total_bytes: usize) -> u32 { const BANK_1M: u32 = 1024 * 1024; const BANK_4M: u32 = 4 * 1024 * 1024; @@ -254,6 +257,9 @@ mod tests { 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); + // Expansion totals below $07000000 keep the fully-populated + // 1Mx4 description; the control register cannot say more. + assert_eq!(RamseyRevision::Rev7.bank_bytes_for(64 * M), 4 * M as u32); } /// Refresh comes up at index 0 (154 clocks), which is what both diagnostic diff --git a/src/savestate.rs b/src/savestate.rs index f2f0cec..4e79753 100644 --- a/src/savestate.rs +++ b/src/savestate.rs @@ -124,7 +124,10 @@ const STATE_MAGIC: &[u8; 8] = b"CLSSTATE"; // 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; +// 36: Memory gained the CPU-slot accelerator fast RAM bank (accel_ram, +// starting at $08000000) and MachineDescriptor its size +// (accel_ram_bytes) +pub const STATE_VERSION: u32 = 36; /// Default state file name, timestamped like the screenshot/recorder names. pub fn auto_filename() -> std::path::PathBuf { @@ -236,6 +239,7 @@ mod tests { chip_ram: vec![0u8; 512 * 1024], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom, overlay: false, zorro: ZorroChain::default(), @@ -328,6 +332,7 @@ mod tests { chip_ram, slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom: vec![0u8; ROM_SIZE], overlay: false, zorro: ZorroChain::default(), @@ -575,6 +580,7 @@ mod tests { fast_ram_bytes: 8 * 1024 * 1024, slow_ram_bytes: 0, mb_ram_bytes: 4 * 1024 * 1024, + accel_ram_bytes: 32 * 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 aa86997..c33c541 100644 --- a/src/video/launcher.rs +++ b/src/video/launcher.rs @@ -249,6 +249,7 @@ pub enum LauncherField { FastRam, SlowRam, MbRam, + AccelRam, Z3Ram, // ROM Rom, @@ -441,11 +442,12 @@ const CPU_ROWS: [Row; 5] = [ row(F::Icache, "Instruction cache", Toggle), row(F::Dcache, "Data cache", Toggle), ]; -const MEMORY_ROWS: [Row; 5] = [ +const MEMORY_ROWS: [Row; 6] = [ 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::AccelRam, "Accelerator RAM", Cycle), row(F::Z3Ram, "Zorro III RAM", Cycle), ]; const ROM_ROWS: [Row; 2] = [ @@ -690,6 +692,29 @@ const MB_PRESETS: [usize; 8] = [ 12 * 1024 * 1024, 16 * 1024 * 1024, ]; +/// The A4000 additionally fills the $04000000-$06FFFFFF motherboard RAM +/// expansion space beyond Ramsey's four banks. +const MB_PRESETS_A4000: [usize; 10] = [ + 0, + 1024 * 1024, + 2 * 1024 * 1024, + 3 * 1024 * 1024, + 4 * 1024 * 1024, + 8 * 1024 * 1024, + 12 * 1024 * 1024, + 16 * 1024 * 1024, + 32 * 1024 * 1024, + 64 * 1024 * 1024, +]; +/// CPU-slot accelerator RAM at $08000000: whatever the CPU board carries, +/// up to the whole 128M coprocessor-slot space. +const ACCEL_PRESETS: [usize; 5] = [ + 0, + 16 * 1024 * 1024, + 32 * 1024 * 1024, + 64 * 1024 * 1024, + 128 * 1024 * 1024, +]; const Z3_PRESETS: [usize; 8] = [ 0, 16 * 1024 * 1024, @@ -792,6 +817,7 @@ pub struct MachineSetup { fast_ram: usize, slow_ram: usize, mb_ram: usize, + accel_ram: usize, z3_ram: usize, // ROM (None = bundled AROS for the boot ROM, none for extended) rom: Option, @@ -927,6 +953,7 @@ impl MachineSetup { fast_ram: cfg.fast_ram_bytes, slow_ram: cfg.slow_ram_bytes, mb_ram: cfg.mb_ram_bytes, + accel_ram: cfg.accel_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), @@ -1136,6 +1163,9 @@ impl MachineSetup { if self.mb_ram != base.mb_ram_bytes { raw.memory.motherboard = Some(format_size(self.mb_ram)); } + if self.accel_ram != base.accel_ram_bytes { + raw.memory.accelerator = Some(format_size(self.accel_ram)); + } if self.z3_ram != base.z3_ram_bytes { raw.memory.z3 = Some(format_size(self.z3_ram)); } @@ -1407,6 +1437,7 @@ impl MachineSetup { self.fast_ram = base.fast_ram_bytes; self.slow_ram = base.slow_ram_bytes; self.mb_ram = base.mb_ram_bytes; + self.accel_ram = base.accel_ram_bytes; self.z3_ram = base.z3_ram_bytes; self.overscan = base.overscan; self.pixel_aspect = base.pixel_aspect; @@ -1475,6 +1506,8 @@ 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 CPU-slot space at $08000000 is beyond a 24-bit bus too. + F::AccelRam => 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 @@ -1733,6 +1766,7 @@ impl MachineSetup { F::FastRam => size_label(self.fast_ram), F::SlowRam => size_label(self.slow_ram), F::MbRam => size_label(self.mb_ram), + F::AccelRam => size_label(self.accel_ram), F::Z3Ram => size_label(self.z3_ram), F::FloppyDrives => self.floppy_drives.to_string(), F::FloppySpeed => crate::floppy::speed_label(self.floppy_speed), @@ -1886,12 +1920,13 @@ 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. + // Zorro III RAM, motherboard RAM, accelerator 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.accel_ram = 0; self.rtg = RtgCard::None; } } @@ -1899,7 +1934,17 @@ impl MachineSetup { 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::MbRam => { + // Only the A4000's Ramsey-07 extends past its four banks + // into the $04000000-$06FFFFFF expansion space. + let presets: &[usize] = if self.model == Some(MachineModel::A4000) { + &MB_PRESETS_A4000 + } else { + &MB_PRESETS + }; + self.mb_ram = cycle_nearest(presets, self.mb_ram, forward); + } + F::AccelRam => self.accel_ram = cycle_nearest(&ACCEL_PRESETS, self.accel_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); @@ -3248,6 +3293,61 @@ mod tests { .expect("68000 A3000 with no mb RAM validates"); } + /// Only the A4000 cycles past Ramsey's 16M four-bank maximum into the + /// $04000000-$06FFFFFF expansion presets; the A3000 wraps back to zero. + #[test] + fn motherboard_ram_expansion_presets_are_a4000_only() { + let mut s = MachineSetup::default(); + s.select_model(Some(MachineModel::A4000)); + while s.mb_ram != 16 * 1024 * 1024 { + s.cycle(LauncherField::MbRam, true); + } + s.cycle(LauncherField::MbRam, true); + assert_eq!(s.mb_ram, 32 * 1024 * 1024); + s.cycle(LauncherField::MbRam, true); + assert_eq!(s.mb_ram, 64 * 1024 * 1024); + s.build_config().expect("64M A4000 motherboard validates"); + + let mut s = MachineSetup::default(); + s.select_model(Some(MachineModel::A3000)); + while s.mb_ram != 16 * 1024 * 1024 { + s.cycle(LauncherField::MbRam, true); + } + s.cycle(LauncherField::MbRam, true); + assert_eq!(s.mb_ram, 0); + } + + /// Accelerator RAM follows only the CPU's address reach: any 32-bit + /// machine can carry it, and downgrading the CPU to a 24-bit part drops + /// the bank so the emitted config still validates. + #[test] + fn accelerator_ram_follows_the_cpu_gate() { + let mut s = MachineSetup::default(); + // The default machine is a 68000 A500: greyed out. + assert_eq!( + s.disabled_reason(LauncherField::AccelRam), + Some("needs 32-bit CPU") + ); + s.select_model(Some(MachineModel::A1200)); + while !cpu_is_32bit(s.cpu) { + s.cycle(LauncherField::Cpu, true); + } + assert!(s.applies(LauncherField::AccelRam)); + s.cycle(LauncherField::AccelRam, true); + assert_eq!(s.accel_ram, 16 * 1024 * 1024); + assert_eq!(s.to_raw().memory.accelerator.as_deref(), Some("16M")); + s.build_config() + .expect("32-bit A1200 with accelerator RAM validates"); + while s.cpu != CpuModel::M68EC020 { + s.cycle(LauncherField::Cpu, true); + } + assert_eq!(s.accel_ram, 0); + assert_eq!( + s.disabled_reason(LauncherField::AccelRam), + Some("needs 32-bit CPU") + ); + } + #[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 7588c97..d654ad4 100644 --- a/src/video/window/tests.rs +++ b/src/video/window/tests.rs @@ -1861,6 +1861,7 @@ fn test_app_with_audio(audio: Box) -> super::App { chip_ram: vec![0u8; 512 * 1024], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom, overlay: true, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/wasmboard.rs b/src/wasmboard.rs index f0f0aae..3a8fbae 100644 --- a/src/wasmboard.rs +++ b/src/wasmboard.rs @@ -712,6 +712,7 @@ mod tests { chip_ram: vec![0u8; 0x1000], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), diff --git a/src/z3660.rs b/src/z3660.rs index 58a0b66..dd5d8a4 100644 --- a/src/z3660.rs +++ b/src/z3660.rs @@ -1100,6 +1100,7 @@ mod tests { chip_ram: vec![0u8; 0x100], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), @@ -1275,6 +1276,7 @@ mod exec_tests { chip_ram: vec![0u8; 0x100], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: crate::zorro::ZorroChain::default(), @@ -1608,6 +1610,7 @@ mod sprite_tests { chip_ram: vec![0u8; 0x100], slow_ram: Vec::new(), mb_ram: Vec::new(), + accel_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 feb332c..417b5fd 100644 --- a/src/zorro_device.rs +++ b/src/zorro_device.rs @@ -481,6 +481,7 @@ mod tests { chip_ram: vec![0u8; chip], slow_ram: vec![0u8; slow], mb_ram: Vec::new(), + accel_ram: Vec::new(), rom: Vec::new(), overlay: false, zorro: ZorroChain::default(), diff --git a/tests/mb_ram.rs b/tests/mb_ram.rs index db2f474..6f6753d 100644 --- a/tests/mb_ram.rs +++ b/tests/mb_ram.rs @@ -93,3 +93,103 @@ fn kickstart_sizes_the_motherboard_ram_bank() { "mh_Lower ${lower:08X} is not the top-down 4 MiB bank" ); } + +/// Boot a machine and collect exec's memory list as (mh_Lower, mh_Upper) +/// pairs once Kickstart has finished sizing memory. +fn boot_and_walk_memlist(cfg: &copperline::config::Config) -> Vec<(u32, u32)> { + 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(); + let execbase = peek32(bus, 4); + let mut headers = Vec::new(); + let mut node = peek32(bus, execbase + 322); // lh_Head + while node != 0 { + let succ = peek32(bus, node); // ln_Succ; 0 at the lh_Tail sentinel + if succ == 0 { + break; + } + headers.push((peek32(bus, node + 20), peek32(bus, node + 24))); + node = succ; + } + headers +} + +/// The A4000 motherboard RAM expansion space: with 64 MiB fitted the bank +/// grows down from $08000000 all the way to $04000000, and Kickstart's +/// top-down probe must size the whole window, not stop at Ramsey's own +/// four-bank 16 MiB. +#[test] +#[ignore = "needs the local Kickstart 3.1 A4000 ROM (see tests/README.md)"] +fn kickstart_sizes_the_expanded_motherboard_ram() { + 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()), + motherboard: Some("64M".into()), + ..Default::default() + }; + let raw = Config::load_raw(None, &overrides).expect("raw config"); + let cfg = Config::try_from(raw) + .expect("A4000 with 64M motherboard RAM validates") + .with_rom_override(Some(rom)); + assert_eq!(cfg.mb_ram_bytes, 64 * 1024 * 1024); + let headers = boot_and_walk_memlist(&cfg); + let (lower, upper) = headers + .iter() + .copied() + .find(|&(_, upper)| upper == 0x0800_0000) + .unwrap_or_else(|| panic!("no MemHeader ending at $08000000 in {headers:X?}")); + assert_eq!(upper, 0x0800_0000); + // Exec claims a little of the bottom of the bank for its header. + assert_eq!( + lower & 0xFFF0_0000, + 0x0400_0000, + "mh_Lower ${lower:08X}: the probe did not reach the bottom of the \ + expansion window at $04000000" + ); +} + +/// CPU-slot (accelerator) RAM at $08000000: Kickstart's bottom-up probe of +/// the coprocessor-slot space must find the fitted bank and add it to the +/// memory list. +#[test] +#[ignore = "needs the local Kickstart 3.1 A4000 ROM (see tests/README.md)"] +fn kickstart_sizes_the_cpu_slot_ram() { + 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()), + accelerator: Some("64M".into()), + ..Default::default() + }; + let raw = Config::load_raw(None, &overrides).expect("raw config"); + let cfg = Config::try_from(raw) + .expect("A4000 with 64M accelerator RAM validates") + .with_rom_override(Some(rom)); + assert_eq!(cfg.accel_ram_bytes, 64 * 1024 * 1024); + let headers = boot_and_walk_memlist(&cfg); + let (lower, upper) = headers + .iter() + .copied() + .find(|&(lower, _)| (0x0800_0000..0x0810_0000).contains(&lower)) + .unwrap_or_else(|| panic!("no MemHeader starting at $08000000 in {headers:X?}")); + // Exec claims a little of the bottom of the bank for its header. + assert_eq!( + upper, 0x0C00_0000, + "mh_Upper ${upper:08X}: the probe did not size the whole 64 MiB \ + CPU-slot bank from ${lower:08X}" + ); +}