diff --git a/docs/debugger/window.md b/docs/debugger/window.md index 58fddf0..386c200 100644 --- a/docs/debugger/window.md +++ b/docs/debugger/window.md @@ -111,7 +111,10 @@ PageUp/PageDown by whole pages. Four buttons sit above the dump: - **Find** searches CPU-visible memory for the `$` box's hex byte pattern (`4E75`, or spaced pairs like `C0 FF EE`), starting past the previous - hit and wrapping the 24-bit space once. The view jumps to the match. + hit and wrapping the decoded memory map once. The sweep covers every RAM + bank the machine decodes -- chip, slow, Zorro II, and on a 32-bit CPU the + motherboard (`$04000000`), CPU-slot (`$08000000`), and Zorro III banks -- + plus the Kickstart and extended-ROM windows. The view jumps to the match. - **Save...** writes the `$` box's `ADDR LEN` (hex) region to a file via a save dialog -- the GUI counterpart of `COPPERLINE_DBG_RAMDUMP`. - **Writer?** reports the last instruction that wrote the word at the diff --git a/src/amigaos.rs b/src/amigaos.rs index 3c308d8..58636fd 100644 --- a/src/amigaos.rs +++ b/src/amigaos.rs @@ -99,9 +99,10 @@ pub fn task_state_name(state: u8) -> &'static str { } /// Heuristic: even and in an address range where exec structures can -/// live -- chip+Z2 RAM, slow RAM, or Zorro III space (OS 3.2+ SetPatch -/// moves ExecBase to fast RAM, which may be a Z3 board). -fn plausible_ptr(addr: u32) -> bool { +/// live -- chip+Z2 RAM, slow RAM, the big-box motherboard and CPU-slot +/// fast-RAM windows, or Zorro III space (OS 3.2+ SetPatch moves ExecBase +/// to fast RAM, which on a big-box machine is well above the 24-bit space). +pub(crate) fn plausible_ptr(addr: u32) -> bool { addr & 1 == 0 && ((0x100..0x00A0_0000).contains(&addr) // chip + Z2 fast || (0x00C0_0000..0x00D8_0000).contains(&addr) // slow @@ -110,6 +111,14 @@ fn plausible_ptr(addr: u32) -> bool { || (0x1000_0000..0x8000_0000).contains(&addr)) // Zorro III } +/// The same test for a BPTR (an address shifted right by two, as AmigaDOS +/// stores pointers). The pointed-at address must be longword aligned, so +/// the shift is exact; BPTRs past a quarter of the address space cannot +/// name a real address at all and are rejected before the shift. +fn plausible_bptr(bptr: u32) -> bool { + bptr < 0x4000_0000 && plausible_ptr(bptr << 2) +} + /// Read-only view of guest memory, built from the debugger's peek /// primitives. `peek8`/`peek32` must be side-effect-free. pub struct OsMemory<'a> { @@ -240,14 +249,14 @@ impl OsMemory<'_> { return None; } let cli = (self.peek32)(task.wrapping_add(PR_CLI)); - if cli != 0 && cli < 0x0040_0000 { + if plausible_bptr(cli) { let module = (self.peek32)((cli << 2).wrapping_add(CLI_MODULE)); if module != 0 { return Some(module); } } let array = (self.peek32)(task.wrapping_add(PR_SEGLIST)); - if array == 0 || array >= 0x0040_0000 { + if !plausible_bptr(array) { return None; } let count = (self.peek32)(array << 2); @@ -263,11 +272,8 @@ impl OsMemory<'_> { /// size longword; the payload follows the next pointer. pub fn walk_seglist(&self, mut bptr: u32) -> Vec { let mut out = Vec::new(); - while bptr != 0 && bptr < 0x0040_0000 && out.len() < 64 { + while plausible_bptr(bptr) && out.len() < 64 { let addr = bptr << 2; - if !(4..0x0100_0000).contains(&addr) { - break; - } let size = (self.peek32)(addr.wrapping_sub(4)); let next = (self.peek32)(addr); out.push(Segment { @@ -295,7 +301,7 @@ impl OsMemory<'_> { /// Read a BSTR (BPTR to a length-prefixed string): printable ASCII, /// bounded by `cap`, empty for implausible pointers. pub fn read_bstr(&self, bptr: u32, cap: usize) -> String { - if bptr == 0 || bptr >= 0x0040_0000 { + if !plausible_bptr(bptr) { return String::new(); } let addr = bptr << 2; @@ -317,7 +323,7 @@ impl OsMemory<'_> { pub fn process_command_name(&self, task: u32) -> String { if (self.peek8)(task.wrapping_add(LN_TYPE)) == NT_PROCESS { let cli = (self.peek32)(task.wrapping_add(PR_CLI)); - if cli != 0 && cli < 0x0040_0000 { + if plausible_bptr(cli) { let name_bptr = (self.peek32)((cli << 2).wrapping_add(CLI_COMMAND_NAME)); // A full AmigaDOS path fits in a 255-byte BSTR. let name = self.read_bstr(name_bptr, 255); @@ -356,8 +362,10 @@ pub fn command_basename(path: &str) -> &str { } /// A loaded hunk must sit in RAM exec could have allocated and carry a -/// believable size (the loader's size longword is bounded by the 16MB -/// chip/Z2 space a seglist can live in). +/// believable size. The 16 MiB size ceiling is a sanity bound on the +/// loader's size longword, not an address-space limit: the hunk itself may +/// sit anywhere `plausible_ptr` accepts, including the 32-bit fast-RAM +/// windows of a big-box machine. fn segment_plausible(seg: &Segment) -> bool { plausible_ptr(seg.start) && seg.size < 0x0100_0000 } @@ -890,4 +898,70 @@ mod tests { let err = os(&peek8, &peek32).exec_base().unwrap_err(); assert!(err.contains("ChkBase"), "{err}"); } + + /// Exec structures live wherever exec allocated them. On a big-box + /// machine that is the Ramsey motherboard bank ($04000000-$07FFFFFF), + /// the CPU-slot accelerator bank ($08000000-$0FFFFFFF), or a Zorro III + /// board -- all beyond the 24-bit space the small-box map fits in. + #[test] + fn plausible_pointers_cover_the_32_bit_ram_windows() { + for addr in [ + 0x0000_1000, // chip + 0x0020_0000, // Zorro II fast + 0x00C0_0000, // slow + 0x0400_0000, // motherboard (Ramsey), bottom of the window + 0x07FF_FFFE, // motherboard, top of the window + 0x0800_0000, // CPU slot, bottom + 0x0FFF_FFFE, // CPU slot, top + 0x1000_0000, // Zorro III, bottom + 0x4000_0000, // Zorro III + ] { + assert!(plausible_ptr(addr), "${addr:08X} should be plausible"); + assert!( + plausible_bptr(addr >> 2), + "BPTR ${:08X} should be plausible", + addr >> 2 + ); + } + // Still rejected: odd addresses, the zero page, the custom-register + // and ROM spaces, and anything past the Zorro III window. + for addr in [ + 0x0000_0001, + 0x0000_0080, + 0x00DF_F000, + 0x00F8_0000, + 0x8000_0000u32, + ] { + assert!(!plausible_ptr(addr), "${addr:08X} should be rejected"); + } + } + + /// A seglist walked from a hunk in motherboard RAM: the BPTR bound + /// must follow the same address windows, not a 16 MiB ceiling. + #[test] + fn walks_a_seglist_loaded_above_the_24_bit_space() { + let hunk = 0x0450_0000u32; + let mut mem = FakeMem::new(); + mem.put32(hunk - 4, 0x100); // loader size longword + mem.put32(hunk, 0); // end of list + let peek8 = |a: u32| mem.peek8(a); + let peek32 = |a: u32| mem.peek32(a); + let segs = os(&peek8, &peek32).walk_seglist(hunk >> 2); + assert_eq!(segs.len(), 1); + assert_eq!(segs[0].start, hunk + 4); + assert!(segment_plausible(&segs[0])); + } + + /// The same for a BSTR: cli_CommandName points into whatever RAM the + /// shell was loaded into. + #[test] + fn reads_a_bstr_above_the_24_bit_space() { + let bstr = 0x0900_0000u32; + let mut mem = FakeMem::new(); + mem.put8(bstr, 3); + mem.put_str(bstr + 1, "Dir"); + let peek8 = |a: u32| mem.peek8(a); + let peek32 = |a: u32| mem.peek32(a); + assert_eq!(os(&peek8, &peek32).read_bstr(bstr >> 2, 255), "Dir"); + } } diff --git a/src/bus.rs b/src/bus.rs index 227e7ec..d6e044c 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -5477,6 +5477,37 @@ impl Bus { regions } + /// The regions a debugger pattern search should sweep: every writable + /// RAM bank plus the Kickstart and extended-ROM windows, in ascending + /// address order. Sweeping the decoded map rather than a fixed + /// address span is what lets a search reach RAM above the 24-bit + /// space (motherboard, CPU-slot, and Zorro III banks) without walking + /// the gigabytes of undecoded address space between them. + /// + /// Chip RAM is offered as the whole $000000-$1FFFFF select window + /// rather than the fitted bank: Agnus decodes fewer address bits than + /// the window on the smaller parts, so the bank repeats inside it and + /// a search of CPU-visible memory sees every image. + pub fn searchable_regions(&self) -> Vec<(u32, u32)> { + let mut regions = self.writable_ram_regions(); + for (base, len) in regions.iter_mut() { + if *base == crate::memory::CHIP_RAM_BASE as u32 { + *len = crate::memory::CHIP_WINDOW_SIZE as u32; + } + } + if !self.mem.rom.is_empty() { + regions.push((crate::memory::ROM_BASE as u32, self.mem.rom.len() as u32)); + } + if !self.mem.extended_rom.is_empty() { + regions.push(( + self.mem.extended_rom_base as u32, + self.mem.extended_rom.len() as u32, + )); + } + regions.sort_by_key(|(base, _)| *base); + regions + } + /// Read a 16-bit big-endian word from whichever RAM/ROM region maps /// `addr` (chip, fast, slow, motherboard, accelerator, or ROM), for the /// debugger's memory dumps. Returns 0 for unmapped addresses. diff --git a/src/bus/tests.rs b/src/bus/tests.rs index f3a37dd..7b189b1 100644 --- a/src/bus/tests.rs +++ b/src/bus/tests.rs @@ -10897,3 +10897,38 @@ fn motherboard_ram_reaches_the_debugger_helpers() { .writable_ram_regions() .contains(&(base, 1024 * 1024_u32))); } + +/// A debugger pattern search sweeps the decoded map, so it must offer the +/// RAM banks that sit past the 24-bit space -- the Ramsey motherboard +/// bank, the CPU-slot accelerator bank, and Zorro III boards -- alongside +/// the chip window and the ROMs. Chip RAM is offered as the whole +/// $000000-$1FFFFF select window so the Agnus image repeats are searched. +#[test] +fn searchable_regions_cover_the_32_bit_ram_banks() { + let mut bus = empty_bus(); + bus.mem.fit_mb_ram(4 * 1024 * 1024); + bus.mem.fit_accel_ram(8 * 1024 * 1024); + bus.mem + .zorro + .add_board_configured_at( + crate::zorro::BoardSpec::z3_ram(16 * 1024 * 1024), + 0x1000_0000, + ) + .expect("Z3 RAM board"); + + let regions = bus.searchable_regions(); + assert!(regions.contains(&(0, crate::memory::CHIP_WINDOW_SIZE as u32))); + assert!(regions.contains(&(0x07C0_0000, 4 * 1024 * 1024))); + assert!(regions.contains(&(crate::memory::ACCEL_RAM_BASE as u32, 8 * 1024 * 1024))); + assert!( + regions + .iter() + .any(|(base, len)| *base >= 0x1000_0000 && *len == 16 * 1024 * 1024), + "Zorro III RAM board missing: {regions:08X?}" + ); + assert!(regions.contains(&(crate::memory::ROM_BASE as u32, 512 * 1024))); + assert!( + regions.windows(2).all(|w| w[0].0 <= w[1].0), + "regions must be in ascending address order: {regions:08X?}" + ); +} diff --git a/src/control/headless.rs b/src/control/headless.rs index 846663f..f4979db 100644 --- a/src/control/headless.rs +++ b/src/control/headless.rs @@ -616,7 +616,8 @@ impl Session { // Target already met (or met exactly at the last quantum)? match target { Some(RunTarget::Pc(pc)) - if self.emu.machine.pc() & 0x00FF_FFFF == pc & 0x00FF_FFFF => + if self.emu.machine.pc() & self.emu.machine.ui_addr_mask() + == pc & self.emu.machine.ui_addr_mask() => { return finish("target", format!("pc ${pc:06X}"), extra_ids, false); } @@ -638,7 +639,7 @@ impl Session { // debug_step_for_gdb keeps reverse-debug captures // happening at frame crossings. The hit itself is // seen by the checks at the top of the loop. - let mask = 0x00FF_FFFF; + let mask = self.emu.machine.ui_addr_mask(); for _ in 0..PC_POLL_CHUNK { self.emu.debug_step_for_gdb(&mut cpu_idle)?; if self.emu.machine.pc() & mask == pc & mask diff --git a/src/cpu.rs b/src/cpu.rs index c12d2a8..6c9f5c4 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1674,11 +1674,11 @@ impl M68kMachine { | u32::from(self.bus.bus.peek_word_any(addr.wrapping_add(2))) }; let execbase = peek32(4); - if execbase == 0 || execbase & 1 != 0 || execbase >= 0x0100_0000 { + if !crate::amigaos::plausible_ptr(execbase) { return None; } let task = peek32(execbase.wrapping_add(0x114)); - (task != 0 && task & 1 == 0 && task < 0x0100_0000).then_some(task) + crate::amigaos::plausible_ptr(task).then_some(task) } /// The name a task node points at, uppercased for matching. @@ -1688,7 +1688,10 @@ impl M68kMachine { | u32::from(self.bus.bus.peek_word_any(addr.wrapping_add(2))) }; let name_ptr = peek32(task.wrapping_add(10)); - if name_ptr == 0 || name_ptr >= 0x0100_0000 { + // Names may live in ROM or at odd addresses (the ROM devices' node + // names are Kickstart strings), so no plausible_ptr bound here; + // unmapped pointers read as zero bytes and end the string at once. + if name_ptr == 0 { return String::new(); } let mut name = String::new(); diff --git a/src/emulator.rs b/src/emulator.rs index 019f2a9..19a27c6 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -978,8 +978,11 @@ impl Emulator { end_pos: u64, full: bool, ) -> Result> { - const PC_MASK: u32 = 0x00FF_FFFF; - let is_bp = |pc: u32| breakpoints.contains(&(pc & PC_MASK)); + // A0-A23 on the 24-bit models, the full 32 bits on 020+, so a + // breakpoint in RAM above the 24-bit space is not aliased onto a + // different PC during replay. + let pc_mask = self.machine.ui_addr_mask(); + let is_bp = |pc: u32| breakpoints.contains(&(pc & pc_mask)); // Drain any stop left over from an earlier replay segment. let _ = self.machine.take_ui_debug_stop(); self.tt_apply_due_input(self.retired_instructions); @@ -987,7 +990,7 @@ impl Emulator { if self.retired_instructions < end_pos && is_bp(self.machine.pc()) { best = Some(( self.retired_instructions, - format!("Breakpoint at ${:06X}", self.machine.pc() & PC_MASK), + format!("Breakpoint at ${:06X}", self.machine.pc() & pc_mask), )); } let mut cpu_idle = false; @@ -1006,7 +1009,7 @@ impl Emulator { if self.retired_instructions < end_pos && is_bp(self.machine.pc()) { best = Some(( self.retired_instructions, - format!("Breakpoint at ${:06X}", self.machine.pc() & PC_MASK), + format!("Breakpoint at ${:06X}", self.machine.pc() & pc_mask), )); } if self.retired_instructions == before && !cpu_idle { @@ -1294,11 +1297,11 @@ impl Emulator { /// Run until the CPU reaches `target_pc` (masked to the bus width), up /// to `max_instructions`. Returns true when the target was hit. pub fn debug_run_to_pc(&mut self, target_pc: u32, max_instructions: usize) -> Result { - const PC_MASK: u32 = 0x00FF_FFFF; - let target = target_pc & PC_MASK; + let pc_mask = self.machine.ui_addr_mask(); + let target = target_pc & pc_mask; for _ in 0..max_instructions { self.debug_step_one_with_idle()?; - if self.machine.pc() & PC_MASK == target { + if self.machine.pc() & pc_mask == target { return Ok(true); } // A breakpoint/watch hit on the way to the target ends the diff --git a/src/video/ui.rs b/src/video/ui.rs index 30c3575..312d093 100644 --- a/src/video/ui.rs +++ b/src/video/ui.rs @@ -403,8 +403,10 @@ impl DebuggerPanel { .collect() } - /// Region spec for Save region: "ADDR LEN", both hex, length bounded - /// to the 24-bit bus. + /// Region spec for Save region: "ADDR LEN", both hex. The address is + /// taken as written -- a dump can start anywhere the CPU decodes, + /// including the motherboard, CPU-slot, and Zorro III RAM above the + /// 24-bit space -- and only the length is capped, at 16 MiB per dump. pub fn region_spec(&self) -> Option<(u32, u32)> { let mut tokens = self.entry.split_whitespace(); let addr = parse_hex_u32(tokens.next()?)?; @@ -412,7 +414,7 @@ impl DebuggerPanel { if tokens.next().is_some() || len == 0 || len > 0x0100_0000 { return None; } - Some((addr & 0x00FF_FFFF, len)) + Some((addr, len)) } pub fn push_entry_char(&mut self, ch: char) { diff --git a/src/video/window.rs b/src/video/window.rs index 302d670..27939a7 100644 --- a/src/video/window.rs +++ b/src/video/window.rs @@ -5807,7 +5807,7 @@ impl App { /// Find the entry's hex byte pattern in CPU-visible memory, starting /// past the previous hit (or the current page) and wrapping around - /// the 24-bit space once. + /// the decoded memory map once. fn debugger_mem_find(&mut self) { let Some(panel) = self.debugger_panel.as_ref() else { return; @@ -5821,27 +5821,8 @@ impl App { .map(|addr| addr.wrapping_add(1)) .unwrap_or(panel.mem_addr) & self.emu.machine.ui_addr_mask(); - const SPACE: u64 = 0x0100_0000; - const CHUNK: usize = 4096; - let mut offset = 0u64; - let mut found = None; - while offset < SPACE { - let base = ((u64::from(start) + offset) % SPACE) as u32; - // Overlap chunks by the pattern length so matches spanning a - // chunk boundary are seen. - let bytes = self - .emu - .machine - .debug_read_memory(base, CHUNK + pattern.len() - 1); - if let Some(hit) = bytes - .windows(pattern.len()) - .position(|window| window == pattern) - { - found = Some(base.wrapping_add(hit as u32) & 0x00FF_FFFF); - break; - } - offset += CHUNK as u64; - } + let regions = self.emu.bus().searchable_regions(); + let found = console::search_cpu_memory(&self.emu.machine, ®ions, &pattern, start); match found { Some(addr) => { if let Some(panel) = self.debugger_panel.as_mut() { @@ -5867,6 +5848,11 @@ impl App { self.show_osd("Save: type \"ADDR LEN\" (hex) first"); return; }; + // Through the machine's address bus, like every other debugger + // surface: the file name and the OSD then name the address the + // bytes actually came from on a 24-bit model, and a 32-bit dump + // above the 24-bit space passes through untouched on 020+. + let addr = addr & self.emu.machine.ui_addr_mask(); self.suspend_live_audio_for_host_io(); let picked = rfd::FileDialog::new() .set_title("Save memory region") diff --git a/src/video/window/console.rs b/src/video/window/console.rs index 7531ce5..637263a 100644 --- a/src/video/window/console.rs +++ b/src/video/window/console.rs @@ -99,29 +99,69 @@ fn reg_index(token: &str) -> Option { } } -/// Search CPU-visible memory for `pattern`, starting at `start` and -/// wrapping the 24-bit space once. Shared by the console FIND command -/// and the Memory tab's Find button. -pub(super) fn search_cpu_memory( +/// Search one `[from, end)` span of CPU-visible memory for `pattern`. +/// +/// Every read runs `pattern.len() - 1` bytes past its chunk, including the +/// last chunk of the span. That tail is deliberate twice over: it is what +/// lets a match straddle a chunk boundary, and at the end of a span it is +/// what lets a match straddle two banks that abut in the map (a full +/// motherboard bank ends at $08000000, exactly where the CPU-slot bank +/// begins). The tail only ever supplies trailing bytes -- the window count +/// is the chunk length, so a reported hit always starts inside +/// `[from, end)` -- and the bytes it reads are what the CPU would see +/// there, unmapped space included. +fn search_span( machine: &crate::cpu::M68kMachine, pattern: &[u8], - start: u32, + from: u32, + end: u64, ) -> Option { - const SPACE: u64 = 0x0100_0000; const CHUNK: usize = 4096; - let mut offset = 0u64; - while offset < SPACE { - let base = ((u64::from(start) + offset) % SPACE) as u32; - // Overlap chunks by the pattern length so matches spanning a - // chunk boundary are seen. - let bytes = machine.debug_read_memory(base, CHUNK + pattern.len() - 1); + let mut addr = u64::from(from); + while addr < end { + let span = (end - addr) as usize; + let bytes = machine.debug_read_memory(addr as u32, span.min(CHUNK) + pattern.len() - 1); if let Some(hit) = bytes .windows(pattern.len()) .position(|window| window == pattern) { - return Some(base.wrapping_add(hit as u32) & 0x00FF_FFFF); + return Some((addr as u32).wrapping_add(hit as u32)); + } + addr += CHUNK as u64; + } + None +} + +/// Search CPU-visible memory for `pattern`, starting at `start` and +/// wrapping the decoded map once. `regions` is the machine's +/// [`crate::bus::Bus::searchable_regions`], so RAM above the 24-bit space +/// (motherboard, CPU-slot, and Zorro III banks) is covered and the +/// undecoded gaps between the banks are skipped. Shared by the console +/// FIND command and the Memory tab's Find button. +pub(super) fn search_cpu_memory( + machine: &crate::cpu::M68kMachine, + regions: &[(u32, u32)], + pattern: &[u8], + start: u32, +) -> Option { + // Two sweeps: the map from `start` up to its top, then its bottom back + // up to `start`, so the search wraps the whole map exactly once. + for (base, len) in regions { + let end = u64::from(*base) + u64::from(*len); + let from = u64::from(start).max(u64::from(*base)); + if from < end { + if let Some(hit) = search_span(machine, pattern, from as u32, end) { + return Some(hit); + } + } + } + for (base, len) in regions { + let end = (u64::from(*base) + u64::from(*len)).min(u64::from(start)); + if u64::from(*base) < end { + if let Some(hit) = search_span(machine, pattern, *base, end) { + return Some(hit); + } } - offset += CHUNK as u64; } None } @@ -865,7 +905,13 @@ impl App { let Some(pattern) = parse_hex_pattern(pattern_tokens) else { return ConsoleOutcome::error("FIND takes hex byte pairs (e.g. 4E75)"); }; - match search_cpu_memory(&self.emu.machine, &pattern, start) { + // Through the machine's address bus, as the Memory tab's + // Find does: the sweep must start where the reads will + // land, or a START past a 24-bit bus would skip the + // whole map and silently restart from the bottom. + let start = start & self.emu.machine.ui_addr_mask(); + let regions = self.emu.bus().searchable_regions(); + match search_cpu_memory(&self.emu.machine, ®ions, &pattern, start) { Some(addr) => ConsoleOutcome::one(format!("found at ${addr:06X}")), None => ConsoleOutcome::one("pattern not found"), } @@ -1312,8 +1358,8 @@ impl App { } /// Heuristic 68k call-stack walk: scan up the stack for longwords - /// that look like return addresses (even, in the 24-bit space, and - /// immediately preceded by a JSR or BSR encoding). Heuristic by + /// that look like return addresses (even, on the CPU's address bus, + /// and immediately preceded by a JSR or BSR encoding). Heuristic by /// nature -- data words that happen to follow call opcodes can slip /// in -- but each frame shows its stack slot so it can be judged. fn console_stack_lines(&self) -> Vec { @@ -1324,8 +1370,13 @@ impl App { let peek16 = |addr: u32| bus.peek_word_any(addr); let peek32 = |addr: u32| (u32::from(peek16(addr)) << 16) | u32::from(peek16(addr.wrapping_add(2))); + // A0-A23 on the 24-bit models, the full 32 bits on 020+, so code + // running from motherboard, CPU-slot, or Zorro III RAM is walked + // rather than rejected. Unmapped words peek as 0, which matches no + // JSR/BSR encoding, so the opcode test still does the filtering. + let addr_mask = machine.ui_addr_mask(); let looks_like_return = |addr: u32| -> bool { - if addr == 0 || addr & 1 != 0 || addr >= 0x0100_0000 { + if addr == 0 || addr & 1 != 0 || addr & addr_mask != addr { return false; } // JSR (An)/-(An)+modes and BSR.B end 2 bytes before the diff --git a/src/video/window/tests.rs b/src/video/window/tests.rs index 39952cb..002d9e1 100644 --- a/src/video/window/tests.rs +++ b/src/video/window/tests.rs @@ -1902,8 +1902,18 @@ fn test_app() -> super::App { } fn test_app_with_audio(audio: Box) -> super::App { + test_app_with_audio_and_cpu(audio, crate::config::CpuModel::M68000) +} + +/// The same fixture on a chosen CPU model. Fast-RAM banks past the 24-bit +/// space are fitted by the caller through `bus_mut().mem`, since only a +/// 32-bit model reaches them. +fn test_app_with_audio_and_cpu( + audio: Box, + cpu: crate::config::CpuModel, +) -> super::App { use crate::chipset::paula::Paula; - use crate::config::{CpuModel, PacingBudget}; + use crate::config::PacingBudget; use crate::emulator::Emulator; use crate::floppy::FloppyController; use crate::memory::{Memory, ROM_BASE, ROM_SIZE}; @@ -1937,7 +1947,7 @@ fn test_app_with_audio(audio: Box) -> super::App { ); let emu = Emulator::new( bus, - CpuModel::M68000, + cpu, false, Default::default(), PacingBudget::Cycles, @@ -3452,6 +3462,16 @@ fn console_modify_search_and_transport_commands() { let out = console_run(&mut app, "FIND BEEF 50000"); assert!(out[0].contains("found at $060000"), "{out:?}"); + // A START is taken through the machine's address bus, so on this + // 24-bit fixture $2050000 sweeps from $050000 and reports the hit + // above it. Unmasked it would name no region at all and restart from + // the bottom of the map, reporting the earlier copy instead. + console_run(&mut app, "POKE 40000 BEEF"); + let out = console_run(&mut app, "FIND BEEF 2050000"); + assert!(out[0].contains("found at $060000"), "{out:?}"); + let out = console_run(&mut app, "FIND BEEF 0"); + assert!(out[0].contains("found at $040000"), "{out:?}"); + // Run to an exact beam slot; the one-shot trap reports its position. let out = console_run(&mut app, "TOSLOT 50 30"); assert!( @@ -3632,6 +3652,65 @@ fn memory_tab_find_scroll_and_bitmap_toggle() { assert!(!app.debugger_panel.as_ref().unwrap().mem_view_bits); } +/// Find sweeps the decoded memory map, so on a 32-bit CPU it reaches the +/// RAM banks past the 24-bit space -- here the CPU-slot accelerator bank at +/// $08000000. A search of a fixed 16 MiB span could never see them. +#[test] +fn memory_tab_find_reaches_ram_above_the_24_bit_space() { + let mut app = test_app_with_audio_and_cpu(Box::new(NullSink), crate::config::CpuModel::M68030); + app.emu.bus_mut().mem.fit_accel_ram(1024 * 1024); + app.open_debugger(); + if let Some(panel) = app.debugger_panel.as_mut() { + panel.tab = super::ui::DebugTab::Memory; + panel.mem_addr = 0; + } + app.emu.bus_mut().mem.accel_ram[0x4_0000..0x4_0004].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); + if let Some(panel) = app.debugger_panel.as_mut() { + panel.entry = "DEADBEEF".to_string(); + } + app.activate_ui_control(UiControl::DebugMemFind); + let panel = app.debugger_panel.as_ref().unwrap(); + assert_eq!( + panel.mem_last_find, + Some(crate::memory::ACCEL_RAM_BASE as u32 + 0x4_0000) + ); + assert_eq!( + panel.mem_addr, + crate::memory::ACCEL_RAM_BASE as u32 + 0x4_0000 + ); +} + +/// A full motherboard bank ends at $08000000, exactly where the CPU-slot +/// bank begins, so the two abut in the decoded map. The per-span reads run +/// one pattern short of a chunk past their span end precisely so a match +/// straddling that seam is still found, anchored in the bank it starts in. +#[test] +fn memory_tab_find_spans_two_abutting_ram_banks() { + let mut app = test_app_with_audio_and_cpu(Box::new(NullSink), crate::config::CpuModel::M68030); + { + let mem = &mut app.emu.bus_mut().mem; + mem.fit_mb_ram(4 * 1024 * 1024); + mem.fit_accel_ram(1024 * 1024); + assert_eq!(mem.mb_ram_base(), 0x07C0_0000); + // DE AD in the last two bytes of the motherboard bank, BE EF in + // the first two of the CPU-slot bank. + let top = mem.mb_ram.len(); + mem.mb_ram[top - 2..].copy_from_slice(&[0xDE, 0xAD]); + mem.accel_ram[..2].copy_from_slice(&[0xBE, 0xEF]); + } + app.open_debugger(); + if let Some(panel) = app.debugger_panel.as_mut() { + panel.tab = super::ui::DebugTab::Memory; + panel.mem_addr = 0; + panel.entry = "DEADBEEF".to_string(); + } + app.activate_ui_control(UiControl::DebugMemFind); + assert_eq!( + app.debugger_panel.as_ref().unwrap().mem_last_find, + Some(0x07FF_FFFE) + ); +} + #[test] fn video_tab_layer_toggles_flip_bus_masks() { let mut app = test_app();