diff --git a/src/cpu.rs b/src/cpu.rs index f2fcf46..b203a1e 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -279,7 +279,7 @@ impl M68kMachine { dbg_crash_on: crate::envcfg::flag("COPPERLINE_DIAG_CRASH"), dbg_crash_dumped: false, dbg: crate::debugger::Debugger::from_env(), - ui_breaks: crate::debugger::InteractiveBreaks::default(), + ui_breaks: crate::debugger::InteractiveBreaks::new(address_mask_for_model(cpu_model)), ui_stop: None, ui_last_this_task: None, ui_pc_history: [0; UI_PC_HISTORY_CAP], @@ -1241,6 +1241,13 @@ impl M68kMachine { &self.ui_breaks } + /// The CPU model's address-bus mask, for debugger surfaces that + /// normalize or display CPU addresses (A0-A23 on 24-bit models, + /// full 32 bits on 020+). + pub fn ui_addr_mask(&self) -> u32 { + self.cpu.address_mask + } + /// Toggle a PC breakpoint carrying an optional condition and ignore count. /// Returns true when the breakpoint is now set. pub fn ui_set_breakpoint( @@ -1277,7 +1284,7 @@ impl M68kMachine { addr: u32, filter: Option, ) -> bool { - let addr = addr & crate::debugger::UI_ADDR_MASK & !1; + let addr = addr & self.cpu.address_mask & !1; let current = self.bus.bus.peek_word_any(addr); let added = self.ui_breaks.toggle_watch(addr, current, filter); let addrs: Vec = self.ui_breaks.watches.iter().map(|w| w.addr).collect(); @@ -1443,7 +1450,7 @@ impl M68kMachine { /// is the instruction that just retired. fn ui_check_breaks_after_step(&mut self) { use crate::debugger::DebugStop; - let pc = self.cpu.pc & crate::debugger::UI_ADDR_MASK; + let pc = self.cpu.pc & self.cpu.address_mask; // Exception catchpoints: the core records every exception entry // (trap, fault, or interrupt) as it loads the handler vector; // drain it here so a hit stops at the handler's first @@ -1467,7 +1474,7 @@ impl M68kMachine { if self.ui_stop.is_some() { return; } - let writer_pc = self.cpu.ppc & crate::debugger::UI_ADDR_MASK; + let writer_pc = self.cpu.ppc & self.cpu.address_mask; for i in 0..self.ui_breaks.watches.len() { let addr = self.ui_breaks.watches[i].addr; let new = self.bus.bus.peek_word_any(addr); @@ -1548,7 +1555,7 @@ impl M68kMachine { // a caught vector; stop before the handler's first // instruction executes. if self.ui_breaks.armed() { - let pc = self.cpu.pc & crate::debugger::UI_ADDR_MASK; + let pc = self.cpu.pc & self.cpu.address_mask; if let Some(vector) = self.cpu.last_exception_vector.take() { let vector = vector.min(u32::from(u16::MAX)) as u16; if self.ui_breaks.catches.contains(&vector) { diff --git a/src/debugger.rs b/src/debugger.rs index 6ff15c6..1d0a7fc 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -61,8 +61,9 @@ pub struct Watch { pub len: u32, } -/// The 68000 address-bus width the interactive debugger compares PCs and -/// watch addresses through (A0-A23). +/// The 68000/EC020 address-bus mask (A0-A23). Debugger surfaces compare +/// and display through the machine's model mask (`ui_addr_mask()`); this +/// constant remains for tests and 24-bit callers. pub const UI_ADDR_MASK: u32 = 0x00FF_FFFF; /// An interactive memory watchpoint: a 16-bit word and the value it held @@ -687,8 +688,11 @@ pub struct Breakpoint { /// The debugger window's breakpoint/watchpoint set. Owned by the CPU /// machine so it stays armed while the window is closed; `armed` is the /// single per-instruction gate the hot loop checks. -#[derive(Default)] pub struct InteractiveBreaks { + /// Address-bus mask breakpoint/watch addresses and PCs are compared + /// through: A0-A23 on 24-bit models, full 32 bits on 020+ (set from + /// the CPU model at machine construction). + pub addr_mask: u32, pub breakpoints: Vec, pub watches: Vec, /// Watched custom-register word offsets into $DFF000 ($000-$1FE). @@ -706,6 +710,20 @@ pub struct InteractiveBreaks { } impl InteractiveBreaks { + /// An empty break set comparing addresses through `addr_mask` (the + /// owning machine's address-bus mask; see `address_mask_for_model`). + pub fn new(addr_mask: u32) -> Self { + Self { + addr_mask, + breakpoints: Vec::new(), + watches: Vec::new(), + reg_watches: Vec::new(), + catches: Vec::new(), + task_catch: None, + armed: false, + } + } + pub fn armed(&self) -> bool { self.armed } @@ -721,7 +739,7 @@ impl InteractiveBreaks { /// Whether any breakpoint is set at `pc`, ignoring its condition. Used for /// display (marking the address) and the reverse-debug scan. pub fn is_breakpoint(&self, pc: u32) -> bool { - let pc = pc & UI_ADDR_MASK; + let pc = pc & self.addr_mask; self.breakpoints.iter().any(|bp| bp.addr == pc) } @@ -734,7 +752,7 @@ impl InteractiveBreaks { cond: Option, ignore: u32, ) -> bool { - let addr = addr & UI_ADDR_MASK; + let addr = addr & self.addr_mask; let added = match self.breakpoints.iter().position(|bp| bp.addr == addr) { Some(pos) => { self.breakpoints.remove(pos); @@ -759,7 +777,7 @@ impl InteractiveBreaks { /// ignore count has been exhausted -- each qualifying hit before that is /// counted and skipped. pub fn breakpoint_stops(&mut self, pc: u32, ctx: &dyn BreakContext) -> bool { - let pc = pc & UI_ADDR_MASK; + let pc = pc & self.addr_mask; let Some(bp) = self.breakpoints.iter_mut().find(|bp| bp.addr == pc) else { return false; }; @@ -1118,7 +1136,7 @@ mod tests { #[test] fn interactive_breakpoints_toggle_mask_and_arm() { - let mut breaks = InteractiveBreaks::default(); + let mut breaks = InteractiveBreaks::new(UI_ADDR_MASK); assert!(!breaks.armed()); // Adding masks the address to the 68000 bus width. @@ -1132,6 +1150,16 @@ mod tests { assert!(!breaks.is_breakpoint(0x00C0_33C2)); } + #[test] + fn full_mask_keeps_z3_breakpoints_distinct_from_chip_aliases() { + // On a 32-bit CPU a Zorro III breakpoint must not fire at the + // chip-RAM address it would alias through a 24-bit mask. + let mut breaks = InteractiveBreaks::new(0xFFFF_FFFF); + assert!(breaks.toggle_breakpoint_full(0x4000_1000, None, 0)); + assert!(breaks.is_breakpoint(0x4000_1000)); + assert!(!breaks.is_breakpoint(0x0000_1000)); + } + /// Fixed register/memory snapshot for exercising condition evaluation. #[derive(Default)] struct FakeCtx { @@ -1162,7 +1190,7 @@ mod tests { #[test] fn conditional_breakpoint_stops_only_when_condition_holds() { - let mut breaks = InteractiveBreaks::default(); + let mut breaks = InteractiveBreaks::new(UI_ADDR_MASK); breaks.toggle_breakpoint_full( 0x1000, Some(BreakCond { @@ -1186,7 +1214,7 @@ mod tests { #[test] fn ignore_count_skips_the_first_qualifying_hits() { - let mut breaks = InteractiveBreaks::default(); + let mut breaks = InteractiveBreaks::new(UI_ADDR_MASK); // Stop on the 4th qualifying hit (ignore the first 3). breaks.toggle_breakpoint_full(0x1000, None, 3); let ctx = FakeCtx::default(); @@ -1200,7 +1228,7 @@ mod tests { #[test] fn bit_test_condition_uses_memory_word() { - let mut breaks = InteractiveBreaks::default(); + let mut breaks = InteractiveBreaks::new(UI_ADDR_MASK); breaks.toggle_breakpoint_full( 0x40, Some(BreakCond { @@ -1219,7 +1247,7 @@ mod tests { #[test] fn interactive_watches_record_baselines_and_clear() { - let mut breaks = InteractiveBreaks::default(); + let mut breaks = InteractiveBreaks::new(UI_ADDR_MASK); assert!(breaks.toggle_watch(0x1000, 0xABCD, None)); assert_eq!(breaks.watches[0].last, 0xABCD); // The register watch normalizes a full $DFFxxx address to the diff --git a/src/gdbstub.rs b/src/gdbstub.rs index 1c2e9ae..23fe005 100644 --- a/src/gdbstub.rs +++ b/src/gdbstub.rs @@ -7,7 +7,7 @@ //! memory-mapped devices; Amiga custom-chip state is exposed through `monitor` //! commands so inspection remains side-effect-free. -use crate::debugger::{custom_reg_name, UI_ADDR_MASK}; +use crate::debugger::custom_reg_name; use crate::emulator::Emulator; use crate::timetravel::ReverseOutcome; use anyhow::{anyhow, bail, Context, Result}; @@ -391,7 +391,7 @@ impl Session { fn add_breakpoint(&mut self, packet: &str) -> Result { let (addr, _) = parse_z_packet(packet)?; - let addr = addr & UI_ADDR_MASK; + let addr = addr & self.emu.machine.ui_addr_mask(); if !self.breakpoints.contains(&addr) { self.breakpoints.push(addr); } @@ -400,7 +400,7 @@ impl Session { fn remove_breakpoint(&mut self, packet: &str) -> Result { let (addr, _) = parse_z_packet(packet)?; - let addr = addr & UI_ADDR_MASK; + let addr = addr & self.emu.machine.ui_addr_mask(); self.breakpoints.retain(|&candidate| candidate != addr); Ok("OK".to_string()) } @@ -485,7 +485,7 @@ impl Session { if self.emu.bus_mut().take_ui_copper_hit().is_some() { return Ok(Some(StopReason::CopperBreak)); } - let pc = self.emu.machine.pc() & UI_ADDR_MASK; + let pc = self.emu.machine.pc() & self.emu.machine.ui_addr_mask(); if self.breakpoints.contains(&pc) { return Ok(Some(StopReason::Breakpoint)); } diff --git a/src/video/window.rs b/src/video/window.rs index 6cdafc2..7a0f8ba 100644 --- a/src/video/window.rs +++ b/src/video/window.rs @@ -3206,7 +3206,7 @@ impl App { let mut panel = ui::DebuggerPanel::new(); // Start the memory view at the current program counter's // neighbourhood; it is usually what you came to look at. - panel.mem_addr = self.emu.machine.pc() & 0x00FF_FFF0; + panel.mem_addr = self.emu.machine.pc() & self.emu.machine.ui_addr_mask() & !0xF; self.debugger_panel = Some(panel); self.emu.machine.ui_set_pc_history_enabled(true); // Arm reverse debugging so the < Step / < Run controls work. A @@ -4453,7 +4453,7 @@ impl App { self.reported_double_fault = true; let message = format!( "CPU halted: double fault at pc ${:06X} (bus/address error during exception)", - self.emu.machine.pc() & 0x00FF_FFFF + self.emu.machine.pc() & self.emu.machine.ui_addr_mask() ); warn!("{message}"); self.last_debug_stop = Some(message.clone()); @@ -4498,7 +4498,7 @@ impl App { let set = self.emu.machine.ui_set_breakpoint(addr, cond, ignore); let mut msg = format!( "Breakpoint ${:06X} {}", - addr & 0x00FF_FFFF, + addr & self.emu.machine.ui_addr_mask(), if set { "set" } else { "removed" } ); if set { @@ -4517,7 +4517,7 @@ impl App { let Some(addr) = self.debugger_entry_addr("Watch") else { return; }; - let addr = addr & 0x00FF_FFFE; + let addr = addr & self.emu.machine.ui_addr_mask() & !1; let set = self.emu.machine.ui_toggle_watch(addr); self.show_osd(format!( "Watchpoint ${addr:06X} {}", @@ -4564,7 +4564,7 @@ impl App { panel.mem_addr.wrapping_sub(delta) } else { panel.mem_addr.wrapping_add(delta) - } & 0x00FF_FFFF; + } & self.emu.machine.ui_addr_mask(); if !panel.mem_view_bits { panel.mem_addr &= !0xF; } @@ -4587,7 +4587,7 @@ impl App { panel.mem_addr.wrapping_sub(delta) } else { panel.mem_addr.wrapping_add(delta) - } & 0x00FF_FFFF; + } & self.emu.machine.ui_addr_mask(); self.request_redraw(); } } @@ -4608,7 +4608,7 @@ impl App { .mem_last_find .map(|addr| addr.wrapping_add(1)) .unwrap_or(panel.mem_addr) - & 0x00FF_FFFF; + & self.emu.machine.ui_addr_mask(); const SPACE: u64 = 0x0100_0000; const CHUNK: usize = 4096; let mut offset = 0u64; @@ -4689,7 +4689,7 @@ impl App { self.show_osd("Writer: type a hex address first"); return; }; - let addr = addr & 0x00FF_FFFE; + let addr = addr & self.emu.machine.ui_addr_mask() & !1; let before = self.emu.retired_instructions(); match self.emu.tt_last_writer(addr, before) { Ok(ReverseOutcome::Found(rec)) => { @@ -4698,7 +4698,7 @@ impl App { rec.addr, rec.old, rec.new, - rec.pc & 0x00FF_FFFF, + rec.pc & self.emu.machine.ui_addr_mask(), rec.frame ); info!("last-writer {message}"); @@ -5367,7 +5367,7 @@ impl App { if panel.mem_view_bits { let stride = panel.mem_bitmap_stride.max(1) as usize; let rows = ui::mem_bitmap_rows(); - let base = panel.mem_addr & 0x00FF_FFFF; + let base = panel.mem_addr & machine.ui_addr_mask(); lines.push(ui::DbgLine::plain(format!( "bitplane at ${base:06X}, stride {stride} bytes ({} px), {rows} rows", stride * 8 @@ -5383,9 +5383,9 @@ impl App { "$ box: jump / \"ADDR VALUE\" poke / \"ADDR LEN\" save / hex bytes find", )); lines.push(ui::DbgLine::plain("")); - let base = panel.mem_addr & 0x00FF_FFF0; + let base = panel.mem_addr & machine.ui_addr_mask() & !0xF; for row in 0..16u32 { - let addr = base.wrapping_add(row * 16) & 0x00FF_FFFF; + let addr = base.wrapping_add(row * 16) & machine.ui_addr_mask(); let mut bytes = [0u8; 16]; for word in 0..8u32 { let value = bus.peek_word_any(addr.wrapping_add(word * 2)); diff --git a/src/video/window/console.rs b/src/video/window/console.rs index 9f5e899..77bcfad 100644 --- a/src/video/window/console.rs +++ b/src/video/window/console.rs @@ -378,7 +378,7 @@ impl App { let set = self.emu.machine.ui_set_breakpoint(addr, cond, ignore); ConsoleOutcome::one(format!( "breakpoint ${:06X} {}", - addr & 0x00FF_FFFF, + addr & self.emu.machine.ui_addr_mask(), if set { "set" } else { "removed" } )) } @@ -398,7 +398,7 @@ impl App { let set = self.emu.machine.ui_toggle_watch_filtered(addr, filter); ConsoleOutcome::one(format!( "watchpoint ${:06X}{} {}", - addr & 0x00FF_FFFE, + addr & self.emu.machine.ui_addr_mask() & !1, filter .map(|f| format!(" ({} writes only)", f.label())) .unwrap_or_default(), @@ -482,7 +482,10 @@ impl App { // exec's Alert() lives at LVO -108; the jump-table // entry itself executes, so a PC breakpoint there // fires on every alert with D7 = the guru code. - vec![format!("{:06X}", base.wrapping_sub(108) & 0x00FF_FFFF)] + vec![format!( + "{:06X}", + base.wrapping_sub(108) & self.emu.machine.ui_addr_mask() + )] }); let Some(addr) = lvo .first() @@ -805,7 +808,7 @@ impl App { let Some(addr) = args.first().and_then(|t| hex32(t)) else { return ConsoleOutcome::error("usage: WRITER ADDR (hex, word)"); }; - let addr = addr & 0x00FF_FFFE; + let addr = addr & self.emu.machine.ui_addr_mask() & !1; let before = self.emu.retired_instructions(); let outcome = match self.emu.tt_last_writer(addr, before) { Ok(crate::timetravel::ReverseOutcome::Found(rec)) => { @@ -814,7 +817,7 @@ impl App { rec.addr, rec.old, rec.new, - rec.pc & 0x00FF_FFFF, + rec.pc & self.emu.machine.ui_addr_mask(), rec.frame )) } @@ -1284,8 +1287,8 @@ impl App { let cpu_type = machine.cpu_type(); let mut lines = vec![format!( "#0 pc ${:06X} sp ${:06X}", - machine.pc() & 0x00FF_FFFF, - sp & 0x00FF_FFFF + machine.pc() & machine.ui_addr_mask(), + sp & machine.ui_addr_mask() )]; let mut frame = 1usize; for slot in 0..SLOTS { @@ -1293,7 +1296,7 @@ impl App { break; } let slot_addr = sp.wrapping_add(slot * 4); - let value = peek32(slot_addr) & 0x00FF_FFFF; + let value = peek32(slot_addr) & machine.ui_addr_mask(); if !looks_like_return(value) { continue; }