use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
#[cfg(not(target_arch = "wasm32"))]
use std::sync::OnceLock;
use super::globals::LowMemGlobals;
#[cfg(not(target_arch = "wasm32"))]
static FB_WRITE_TRACE_RANGE: OnceLock<Option<(u32, u32)>> = OnceLock::new();
#[cfg(target_arch = "wasm32")]
#[inline]
fn fb_write_trace_range() -> Option<(u32, u32)> {
None
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
fn fb_write_trace_range() -> Option<(u32, u32)> {
*FB_WRITE_TRACE_RANGE.get_or_init(|| {
std::env::var("SYSTEMLESS_TRACE_FB_WRITE_RANGE")
.ok()
.and_then(|s| {
let mut parts = s.split(':');
let start_str = parts.next()?.trim_start_matches("0x");
let end_str = parts.next()?.trim_start_matches("0x");
let start = u32::from_str_radix(start_str, 16).ok()?;
let end = u32::from_str_radix(end_str, 16).ok()?;
Some((start, end))
})
})
}
#[inline]
pub fn fb_write_trace_active() -> bool {
#[cfg(target_arch = "wasm32")]
{
return false;
}
#[cfg(not(target_arch = "wasm32"))]
fb_write_trace_range().is_some()
}
#[inline]
fn maybe_log_fb_write(address: u32, value: u8) {
if let Some((start, end)) = fb_write_trace_range() {
if address >= start && address <= end {
let pc = CURRENT_PC.with(|p| *p.borrow());
eprintln!(
"[FB-WRITE] PC=${:08X} addr=${:08X}=${:02X}",
pc, address, value
);
if pc == 0 && std::env::var_os("RUST_BACKTRACE").is_some() {
let bt = std::backtrace::Backtrace::force_capture();
eprintln!("[FB-WRITE-BT]\n{}", bt);
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
static FB_WRITE_DISASM_COUNT: OnceLock<usize> = OnceLock::new();
#[cfg(target_arch = "wasm32")]
#[inline]
fn fb_write_disasm_count() -> usize {
0
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
fn fb_write_disasm_count() -> usize {
*FB_WRITE_DISASM_COUNT.get_or_init(|| {
std::env::var("SYSTEMLESS_TRACE_FB_WRITE_DISASM")
.ok()
.and_then(|s| {
let trimmed = s.trim();
if trimmed.is_empty() {
return Some(1);
}
trimmed.parse::<usize>().ok().or(Some(1))
})
.unwrap_or(0)
})
}
#[inline]
fn fb_write_disasm_enabled() -> bool {
fb_write_disasm_count() > 0
}
#[cfg(not(target_arch = "wasm32"))]
static MEM_READ_TRACE_RANGE: OnceLock<Option<(u32, u32)>> = OnceLock::new();
#[cfg(not(target_arch = "wasm32"))]
#[inline]
fn mem_read_trace_range() -> Option<(u32, u32)> {
*MEM_READ_TRACE_RANGE.get_or_init(|| {
std::env::var("SYSTEMLESS_TRACE_MEM_READ_RANGE")
.ok()
.and_then(|s| {
let mut parts = s.split(':');
let start_str = parts.next()?.trim_start_matches("0x");
let end_str = parts.next()?.trim_start_matches("0x");
let start = u32::from_str_radix(start_str, 16).ok()?;
let end = u32::from_str_radix(end_str, 16).ok()?;
Some((start, end))
})
})
}
#[cfg(not(target_arch = "wasm32"))]
pub fn mem_read_trace_active() -> bool {
mem_read_trace_range().is_some()
}
#[cfg(target_arch = "wasm32")]
pub fn mem_read_trace_active() -> bool {
false
}
#[inline]
fn maybe_log_mem_read(address: u32, width: u8, value: u32) {
#[cfg(target_arch = "wasm32")]
{
let _ = (address, width, value);
}
#[cfg(not(target_arch = "wasm32"))]
if let Some((start, end)) = mem_read_trace_range() {
if address >= start && address <= end {
let pc = CURRENT_PC.with(|p| *p.borrow());
eprintln!(
"[MEM-READ] PC=${:08X} addr=${:08X} width={} value=${:0width$X}",
pc,
address,
width,
value,
width = (width as usize) * 2
);
}
}
}
pub static STEP_COUNTER: AtomicU32 = AtomicU32::new(0);
pub static WATCHPOINT_ARMED: AtomicBool = AtomicBool::new(false);
thread_local! {
pub static WATCH_ADDRESS: RefCell<Option<u32>> = const { RefCell::new(None) };
pub static CURRENT_PC: RefCell<u32> = const { RefCell::new(0) };
pub static CURRENT_A0: RefCell<u32> = const { RefCell::new(0) };
pub static CURRENT_A1: RefCell<u32> = const { RefCell::new(0) };
pub static CURRENT_A6: RefCell<u32> = const { RefCell::new(0) };
pub static CURRENT_A7: RefCell<u32> = const { RefCell::new(0) };
}
pub fn arm_watchpoint(addr: u32) {
WATCH_ADDRESS.with(|wa| {
*wa.borrow_mut() = Some(addr);
});
WATCHPOINT_ARMED.store(true, Ordering::Relaxed);
eprintln!("[WATCHPOINT] Armed on address ${:08X}", addr);
}
pub fn disarm_watchpoint() {
WATCH_ADDRESS.with(|wa| {
*wa.borrow_mut() = None;
});
WATCHPOINT_ARMED.store(false, Ordering::Relaxed);
}
pub fn watchpoint_armed() -> bool {
WATCHPOINT_ARMED.load(Ordering::Relaxed)
}
pub fn set_current_pc(pc: u32) {
CURRENT_PC.with(|p| {
*p.borrow_mut() = pc;
});
}
pub fn set_watch_registers(a0: u32, a1: u32, a6: u32, a7: u32) {
CURRENT_A0.with(|r| {
*r.borrow_mut() = a0;
});
CURRENT_A1.with(|r| {
*r.borrow_mut() = a1;
});
CURRENT_A6.with(|r| {
*r.borrow_mut() = a6;
});
CURRENT_A7.with(|r| {
*r.borrow_mut() = a7;
});
}
pub fn get_step() -> u32 {
STEP_COUNTER.load(Ordering::Relaxed)
}
pub fn increment_step() {
STEP_COUNTER.fetch_add(1, Ordering::Relaxed);
}
pub trait MemoryBus {
fn read_byte(&self, address: u32) -> u8;
fn read_word(&self, address: u32) -> u16;
fn read_long(&self, address: u32) -> u32;
fn write_byte(&mut self, address: u32, value: u8);
fn write_word(&mut self, address: u32, value: u16);
fn write_long(&mut self, address: u32, value: u32);
fn ram_size(&self) -> u32;
fn read_pstring(&self, address: u32) -> Vec<u8> {
let len = self.read_byte(address) as usize;
self.read_bytes(address.wrapping_add(1), len)
}
fn write_pstring(&mut self, address: u32, data: &[u8]) {
let n = data.len().min(255);
self.write_byte(address, n as u8);
self.write_bytes(address.wrapping_add(1), &data[..n]);
}
fn read_bytes(&self, address: u32, len: usize) -> Vec<u8> {
let mut result = vec![0u8; len];
self.read_bytes_into(address, &mut result);
result
}
fn read_bytes_into(&self, address: u32, dst: &mut [u8]) {
for (i, byte) in dst.iter_mut().enumerate() {
*byte = self.read_byte(address.wrapping_add(i as u32));
}
}
fn write_bytes(&mut self, address: u32, data: &[u8]) {
for (i, &byte) in data.iter().enumerate() {
self.write_byte(address.wrapping_add(i as u32), byte);
}
}
fn fill_zeros(&mut self, address: u32, len: u32) {
for i in 0..len {
self.write_byte(address.wrapping_add(i), 0);
}
}
fn fill_bytes(&mut self, address: u32, len: u32, value: u8) {
for i in 0..len {
self.write_byte(address.wrapping_add(i), value);
}
}
}
pub struct MacMemoryBus {
ram: RamStorage,
ram_size: u32,
globals: LowMemGlobals,
heap_ptr: u32,
heap_limit: u32,
free_blocks: HashMap<u32, Vec<u32>>,
alloc_sizes: HashMap<u32, u32>,
alloc_bucket_sizes: HashMap<u32, u32>,
}
enum RamStorage {
Owned(Vec<u8>),
External(*mut u8, usize),
}
impl RamStorage {
#[inline]
fn get(&self, index: usize) -> u8 {
match self {
RamStorage::Owned(v) => v.get(index).copied().unwrap_or(0),
RamStorage::External(ptr, len) => {
if index < *len {
unsafe { *ptr.add(index) }
} else {
0
}
}
}
}
#[inline]
fn get_in_bounds(&self, index: usize) -> u8 {
match self {
RamStorage::Owned(v) => unsafe { *v.as_ptr().add(index) },
RamStorage::External(ptr, _) => unsafe { *ptr.add(index) },
}
}
#[inline]
fn read_word_in_bounds(&self, index: usize) -> u16 {
match self {
RamStorage::Owned(v) => unsafe {
let ptr = v.as_ptr().add(index);
u16::from_be_bytes([*ptr, *ptr.add(1)])
},
RamStorage::External(ptr, _) => unsafe {
let ptr = ptr.add(index);
u16::from_be_bytes([*ptr, *ptr.add(1)])
},
}
}
#[inline]
fn read_long_in_bounds(&self, index: usize) -> u32 {
match self {
RamStorage::Owned(v) => unsafe {
let ptr = v.as_ptr().add(index);
u32::from_be_bytes([*ptr, *ptr.add(1), *ptr.add(2), *ptr.add(3)])
},
RamStorage::External(ptr, _) => unsafe {
let ptr = ptr.add(index);
u32::from_be_bytes([*ptr, *ptr.add(1), *ptr.add(2), *ptr.add(3)])
},
}
}
#[inline]
fn set_in_bounds(&mut self, index: usize, value: u8) {
match self {
RamStorage::Owned(v) => unsafe {
*v.as_mut_ptr().add(index) = value;
},
RamStorage::External(ptr, _) => unsafe {
*ptr.add(index) = value;
},
}
}
#[inline]
fn write_word_in_bounds(&mut self, index: usize, value: u16) {
let bytes = value.to_be_bytes();
match self {
RamStorage::Owned(v) => unsafe {
let ptr = v.as_mut_ptr().add(index);
*ptr = bytes[0];
*ptr.add(1) = bytes[1];
},
RamStorage::External(ptr, _) => unsafe {
let ptr = ptr.add(index);
*ptr = bytes[0];
*ptr.add(1) = bytes[1];
},
}
}
#[inline]
fn write_long_in_bounds(&mut self, index: usize, value: u32) {
let bytes = value.to_be_bytes();
match self {
RamStorage::Owned(v) => unsafe {
let ptr = v.as_mut_ptr().add(index);
*ptr = bytes[0];
*ptr.add(1) = bytes[1];
*ptr.add(2) = bytes[2];
*ptr.add(3) = bytes[3];
},
RamStorage::External(ptr, _) => unsafe {
let ptr = ptr.add(index);
*ptr = bytes[0];
*ptr.add(1) = bytes[1];
*ptr.add(2) = bytes[2];
*ptr.add(3) = bytes[3];
},
}
}
#[inline]
fn write_bytes_in_bounds(&mut self, index: usize, data: &[u8]) {
match self {
RamStorage::Owned(v) => unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), v.as_mut_ptr().add(index), data.len());
},
RamStorage::External(ptr, _) => unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), ptr.add(index), data.len());
},
}
}
#[inline]
fn fill_zeros_in_bounds(&mut self, index: usize, len: usize) {
match self {
RamStorage::Owned(v) => unsafe {
std::ptr::write_bytes(v.as_mut_ptr().add(index), 0, len);
},
RamStorage::External(ptr, _) => unsafe {
std::ptr::write_bytes(ptr.add(index), 0, len);
},
}
}
#[inline]
fn fill_bytes_in_bounds(&mut self, index: usize, len: usize, value: u8) {
match self {
RamStorage::Owned(v) => unsafe {
std::ptr::write_bytes(v.as_mut_ptr().add(index), value, len);
},
RamStorage::External(ptr, _) => unsafe {
std::ptr::write_bytes(ptr.add(index), value, len);
},
}
}
#[inline]
fn slice_at(&self, index: usize, len: usize) -> Option<&[u8]> {
match self {
RamStorage::Owned(v) => v.get(index..index + len),
RamStorage::External(ptr, total_len) => {
if index
.checked_add(len)
.map(|end| end <= *total_len)
.unwrap_or(false)
{
Some(unsafe { std::slice::from_raw_parts(ptr.add(index), len) })
} else {
None
}
}
}
}
#[inline]
fn slice_at_mut(&mut self, index: usize, len: usize) -> Option<&mut [u8]> {
match self {
RamStorage::Owned(v) => v.get_mut(index..index + len),
RamStorage::External(ptr, total_len) => {
if index
.checked_add(len)
.map(|end| end <= *total_len)
.unwrap_or(false)
{
Some(unsafe { std::slice::from_raw_parts_mut(ptr.add(index), len) })
} else {
None
}
}
}
}
fn set(&mut self, index: usize, value: u8) {
match self {
RamStorage::Owned(v) => {
if index < v.len() {
v[index] = value;
}
}
RamStorage::External(ptr, len) => {
if index < *len {
unsafe {
*ptr.add(index) = value;
}
}
}
}
}
}
impl MacMemoryBus {
pub(crate) fn allocation_bucket_size(size: u32) -> u32 {
((size + 3) & !3).max(4)
}
pub fn block_move(&mut self, src: u32, dst: u32, count: u32) {
if count == 0 {
return;
}
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed) && fb_write_trace_range().is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none();
let count_usize = count as usize;
let src_end = (src as u64).saturating_add(count as u64);
let dst_end = (dst as u64).saturating_add(count as u64);
if fast && src_end <= self.ram_size as u64 && dst_end <= self.ram_size as u64 {
let ram_size_usize = self.ram_size as usize;
if let Some(ram) = self.ram.slice_at_mut(0, ram_size_usize) {
let src_range = (src as usize)..(src as usize + count_usize);
ram.copy_within(src_range, dst as usize);
return;
}
}
if dst > src && dst < src.saturating_add(count) {
for i in (0..count).rev() {
let b = self.read_byte(src.wrapping_add(i));
self.write_byte(dst.wrapping_add(i), b);
}
} else {
for i in 0..count {
let b = self.read_byte(src.wrapping_add(i));
self.write_byte(dst.wrapping_add(i), b);
}
}
}
pub fn new(ram_size: usize) -> Self {
let screen_buffer_start: u32 = if ram_size >= 0x100000 {
(ram_size as u32) - 0x80000
} else if ram_size >= 0x20000 {
(ram_size as u32) - 0x10000
} else {
ram_size as u32
};
let mut bus = Self {
ram: RamStorage::Owned(vec![0; ram_size]),
ram_size: ram_size as u32,
globals: LowMemGlobals::new(),
heap_ptr: 0x200000, heap_limit: screen_buffer_start,
free_blocks: HashMap::new(),
alloc_sizes: HashMap::new(),
alloc_bucket_sizes: HashMap::new(),
};
bus.write_word(super::globals::addr::ROM85, 0x7FFF);
let screen_base: u32 = if ram_size >= 0x100000 {
(ram_size as u32) - 0x80000
} else if ram_size >= 0x20000 {
(ram_size as u32) - 0x10000
} else {
0 };
let screen_row_bytes: u16 = 800;
let screen_width: u16 = 800;
let screen_height: u16 = 600;
bus.write_long(0x0824, screen_base);
use super::globals::addr;
bus.write_long(addr::SCREEN_BITS, screen_base); bus.write_word(addr::SCREEN_BITS + 4, screen_row_bytes); bus.write_word(addr::SCREEN_BITS + 6, 0); bus.write_word(addr::SCREEN_BITS + 8, 0); bus.write_word(addr::SCREEN_BITS + 10, screen_height); bus.write_word(addr::SCREEN_BITS + 12, screen_width);
bus
}
#[allow(dead_code)]
pub unsafe fn wrap_external(ram_ptr: *mut u8, ram_size: usize, globals: LowMemGlobals) -> Self {
let screen_buffer_start: u32 = if ram_size >= 0x100000 {
(ram_size as u32) - 0x80000
} else if ram_size >= 0x20000 {
(ram_size as u32) - 0x10000
} else {
ram_size as u32
};
Self {
ram: RamStorage::External(ram_ptr, ram_size),
ram_size: ram_size as u32,
globals,
heap_ptr: 0x200000,
heap_limit: screen_buffer_start,
free_blocks: HashMap::new(),
alloc_sizes: HashMap::new(),
alloc_bucket_sizes: HashMap::new(),
}
}
pub fn reserve_heap(&mut self, size: u32) {
let aligned = (size + 3) & !3;
self.heap_ptr = self.heap_ptr.max(0x200000) + aligned;
}
pub fn alloc(&mut self, size: u32) -> u32 {
let aligned = Self::allocation_bucket_size(size);
let exact = self
.free_blocks
.get_mut(&aligned)
.and_then(|blocks| blocks.pop());
if let Some(addr) = exact {
self.alloc_sizes.insert(addr, size);
return addr;
}
let best = self
.free_blocks
.iter()
.filter(|(&k, v)| k > aligned && !v.is_empty())
.map(|(&k, _)| k)
.min();
if let Some(bucket) = best {
let recycled = self
.free_blocks
.get_mut(&bucket)
.and_then(|blocks| blocks.pop());
if let Some(addr) = recycled {
self.alloc_sizes.insert(addr, size);
self.alloc_bucket_sizes.insert(addr, bucket);
return addr;
}
}
let ptr = self.heap_ptr;
let new_ptr = ptr + aligned;
if new_ptr >= self.heap_limit {
eprintln!(
"[ALLOC] Out of memory: requesting {} bytes, heap at ${:08X}, limit ${:08X}",
size, ptr, self.heap_limit
);
return 0; }
self.heap_ptr = new_ptr;
self.alloc_sizes.insert(ptr, size);
ptr
}
pub fn get_alloc_size(&self, addr: u32) -> Option<u32> {
self.alloc_sizes.get(&addr).copied()
}
pub fn set_alloc_size(&mut self, addr: u32, new_size: u32) {
if self.alloc_sizes.contains_key(&addr) {
self.alloc_sizes.insert(addr, new_size);
}
}
pub fn free(&mut self, addr: u32) {
if addr == 0 {
return;
}
if let Some(size) = self.alloc_sizes.remove(&addr) {
let bucket = self
.alloc_bucket_sizes
.remove(&addr)
.unwrap_or_else(|| Self::allocation_bucket_size(size));
self.free_blocks.entry(bucket).or_default().push(addr);
}
}
pub fn ram_slice(&self, start: u32, len: u32) -> &[u8] {
let s = start as usize;
let e = s + len as usize;
match &self.ram {
RamStorage::Owned(v) => &v[s..e],
RamStorage::External(ptr, max_len) => {
assert!(e <= *max_len);
unsafe { std::slice::from_raw_parts(ptr.add(s), len as usize) }
}
}
}
pub fn load(&mut self, address: u32, data: &[u8]) {
for (i, &byte) in data.iter().enumerate() {
let addr = address.wrapping_add(i as u32);
if addr < self.ram_size {
self.ram.set(addr as usize, byte);
}
}
}
pub fn globals(&self) -> &LowMemGlobals {
&self.globals
}
pub fn globals_mut(&mut self) -> &mut LowMemGlobals {
&mut self.globals
}
pub fn ram_size(&self) -> u32 {
self.ram_size
}
pub fn dump_stack(&self, sp: u32, label: &str) {
eprintln!("[STACK DUMP] {} (SP=${:08X})", label, sp);
let start = sp.saturating_sub(32) & !3; let end = sp.saturating_add(32);
for addr in (start..end).step_by(4) {
let val = self.read_long(addr);
let marker = if addr == sp { " <--- SP" } else { "" };
eprintln!(" ${:08X}: ${:08X}{}", addr, val, marker);
}
}
}
impl MemoryBus for MacMemoryBus {
#[inline]
fn read_byte(&self, address: u32) -> u8 {
let v = if address < self.ram_size {
self.ram.get_in_bounds(address as usize)
} else {
tracing::warn!("Read from unmapped address ${:08X}", address);
0
};
maybe_log_mem_read(address, 1, v as u32);
v
}
#[inline]
fn read_word(&self, address: u32) -> u16 {
let v = if (address as u64) + 2 <= (self.ram_size as u64) {
self.ram.read_word_in_bounds(address as usize)
} else {
let hi = self.read_byte(address) as u16;
let lo = self.read_byte(address.wrapping_add(1)) as u16;
(hi << 8) | lo
};
maybe_log_mem_read(address, 2, v as u32);
v
}
#[inline]
fn read_long(&self, address: u32) -> u32 {
let v = if (address as u64) + 4 <= (self.ram_size as u64) {
self.ram.read_long_in_bounds(address as usize)
} else {
let hi = self.read_word(address) as u32;
let lo = self.read_word(address.wrapping_add(2)) as u32;
(hi << 16) | lo
};
maybe_log_mem_read(address, 4, v);
v
}
fn write_byte(&mut self, address: u32, value: u8) {
maybe_log_fb_write(address, value);
if let Some((start, end)) = fb_write_trace_range() {
if address >= start && address <= end && fb_write_disasm_enabled() {
let pc = CURRENT_PC.with(|p| *p.borrow());
if pc != 0 && (pc as u64 + 8) <= self.ram_size as u64 {
let read = |off: u32| self.ram.get((pc + off) as usize);
let opcode_word = ((read(0) as u16) << 8) | read(1) as u16;
let (mnemonic, _size) =
m68k::dasm::disassemble(pc, opcode_word, m68k::CpuType::M68000);
let _size = _size.clamp(2, 10);
let trap_annotation = if (opcode_word & 0xF000) == 0xA000 {
let canonical = if (opcode_word & 0x0800) != 0 {
0xA800u16 | (opcode_word & 0x03FF)
} else {
0xA000u16 | (opcode_word & 0x00FF)
};
let auto_pop = (opcode_word & 0x0800) != 0 && (opcode_word & 0x0400) != 0;
if canonical == opcode_word {
String::new()
} else if auto_pop {
format!(" (canonical=${:04X}, auto-pop)", canonical)
} else {
format!(" (canonical=${:04X})", canonical)
}
} else {
String::new()
};
eprintln!(
"[FB-WRITE-DISASM] PC=${:08X} bytes=[{:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}] {}{}",
pc,
read(0), read(1), read(2), read(3),
read(4), read(5), read(6), read(7),
mnemonic,
trap_annotation,
);
let extra = fb_write_disasm_count().saturating_sub(1);
if extra > 0 {
let mut cur = pc.wrapping_add(_size);
for _ in 0..extra {
if (cur as u64 + 2) > self.ram_size as u64 {
break;
}
let op = ((self.ram.get(cur as usize) as u16) << 8)
| self.ram.get(cur as usize + 1) as u16;
let (m, sz) = m68k::dasm::disassemble(cur, op, m68k::CpuType::M68000);
let ann = if (op & 0xF000) == 0xA000 {
let canonical = if (op & 0x0800) != 0 {
0xA800u16 | (op & 0x03FF)
} else {
0xA000u16 | (op & 0x00FF)
};
let auto_pop = (op & 0x0800) != 0 && (op & 0x0400) != 0;
if canonical == op {
String::new()
} else if auto_pop {
format!(" (canonical=${:04X}, auto-pop)", canonical)
} else {
format!(" (canonical=${:04X})", canonical)
}
} else {
String::new()
};
eprintln!(
"[FB-WRITE-DISASM] +{:08X} {}{}",
cur, m, ann
);
cur = cur.wrapping_add(sz.clamp(2, 10));
}
}
}
}
}
#[cfg(debug_assertions)]
if WATCHPOINT_ARMED.load(Ordering::Relaxed) {
WATCH_ADDRESS.with(|wa| {
if let Some(watch_addr) = *wa.borrow() {
if address >= watch_addr && address < watch_addr + 4 {
let step = STEP_COUNTER.load(Ordering::Relaxed);
let pc = CURRENT_PC.with(|p| *p.borrow());
let a0 = CURRENT_A0.with(|r| *r.borrow());
let a1 = CURRENT_A1.with(|r| *r.borrow());
let a6 = CURRENT_A6.with(|r| *r.borrow());
let a7 = CURRENT_A7.with(|r| *r.borrow());
let rw = |off: usize| -> u16 {
let a = pc as usize + off;
if a + 1 < self.ram_size as usize {
((self.ram.get(a) as u16) << 8) | self.ram.get(a + 1) as u16
} else {
0
}
};
let op0 = rw(0);
let op1 = rw(2);
let op2 = rw(4);
eprintln!(
"WATCHPOINT at Step {} PC=${:08X} [{:04X} {:04X} {:04X}] A0=${:08X} A1=${:08X} A6=${:08X} A7=${:08X} Write ${:08X}=${:02X}",
step, pc, op0, op1, op2, a0, a1, a6, a7, address, value
);
}
}
});
}
if address < self.ram_size {
self.ram.set_in_bounds(address as usize, value);
} else {
tracing::warn!(
"Write to unmapped address ${:08X} = ${:02X}",
address,
value
);
}
}
#[inline]
fn write_word(&mut self, address: u32, value: u16) {
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed) && fb_write_trace_range().is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none();
if fast && (address as u64) + 2 <= (self.ram_size as u64) {
self.ram.write_word_in_bounds(address as usize, value);
return;
}
self.write_byte(address, (value >> 8) as u8);
self.write_byte(address.wrapping_add(1), value as u8);
}
#[inline]
fn write_long(&mut self, address: u32, value: u32) {
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed) && fb_write_trace_range().is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none();
if fast && (address as u64) + 4 <= (self.ram_size as u64) {
self.ram.write_long_in_bounds(address as usize, value);
return;
}
self.write_word(address, (value >> 16) as u16);
self.write_word(address.wrapping_add(2), value as u16);
}
#[inline]
fn read_bytes(&self, address: u32, len: usize) -> Vec<u8> {
let end = (address as u64).saturating_add(len as u64);
if end <= self.ram_size as u64 {
if let Some(slice) = self.ram.slice_at(address as usize, len) {
return slice.to_vec();
}
}
let mut result = Vec::with_capacity(len);
for i in 0..len {
result.push(self.read_byte(address.wrapping_add(i as u32)));
}
result
}
#[inline]
fn read_bytes_into(&self, address: u32, dst: &mut [u8]) {
let len = dst.len();
let end = (address as u64).saturating_add(len as u64);
if end <= self.ram_size as u64 {
if let Some(slice) = self.ram.slice_at(address as usize, len) {
dst.copy_from_slice(slice);
return;
}
}
for (i, byte) in dst.iter_mut().enumerate() {
*byte = self.read_byte(address.wrapping_add(i as u32));
}
}
#[inline]
fn write_bytes(&mut self, address: u32, data: &[u8]) {
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed) && fb_write_trace_range().is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none();
let end = (address as u64).saturating_add(data.len() as u64);
if fast && end <= self.ram_size as u64 {
self.ram.write_bytes_in_bounds(address as usize, data);
return;
}
for (i, &byte) in data.iter().enumerate() {
self.write_byte(address.wrapping_add(i as u32), byte);
}
}
#[inline]
fn fill_zeros(&mut self, address: u32, len: u32) {
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed) && fb_write_trace_range().is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none();
let end = (address as u64).saturating_add(len as u64);
if fast && end <= self.ram_size as u64 {
self.ram
.fill_zeros_in_bounds(address as usize, len as usize);
return;
}
for i in 0..len {
self.write_byte(address.wrapping_add(i), 0);
}
}
#[inline]
fn fill_bytes(&mut self, address: u32, len: u32, value: u8) {
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed) && fb_write_trace_range().is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none();
let end = (address as u64).saturating_add(len as u64);
if fast && end <= self.ram_size as u64 {
self.ram
.fill_bytes_in_bounds(address as usize, len as usize, value);
return;
}
for i in 0..len {
self.write_byte(address.wrapping_add(i), value);
}
}
fn ram_size(&self) -> u32 {
self.ram_size
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_big_endian_word() {
let mut bus = MacMemoryBus::new(1024);
bus.write_word(0x100, 0x1234);
assert_eq!(bus.read_byte(0x100), 0x12); assert_eq!(bus.read_byte(0x101), 0x34);
assert_eq!(bus.read_word(0x100), 0x1234);
}
#[test]
fn test_big_endian_long() {
let mut bus = MacMemoryBus::new(1024);
bus.write_long(0x100, 0x12345678);
assert_eq!(bus.read_byte(0x100), 0x12);
assert_eq!(bus.read_byte(0x101), 0x34);
assert_eq!(bus.read_byte(0x102), 0x56);
assert_eq!(bus.read_byte(0x103), 0x78);
assert_eq!(bus.read_long(0x100), 0x12345678);
}
#[test]
fn test_pascal_string() {
let mut bus = MacMemoryBus::new(1024);
bus.write_pstring(0x100, b"Hello");
assert_eq!(bus.read_byte(0x100), 5); assert_eq!(bus.read_pstring(0x100), b"Hello".to_vec());
}
#[test]
fn zero_size_allocations_get_unique_slots() {
let mut bus = MacMemoryBus::new(4 * 1024 * 1024);
let zero = bus.alloc(0);
let next = bus.alloc(4);
assert_ne!(zero, 0);
assert_ne!(
zero, next,
"zero-size allocations must not alias the following allocation"
);
assert_eq!(
bus.get_alloc_size(zero),
Some(0),
"the logical allocation size should remain zero"
);
assert_eq!(bus.get_alloc_size(next), Some(4));
bus.free(zero);
let reused = bus.alloc(1);
assert_eq!(
reused, zero,
"the minimum bucket for a freed zero-size allocation should be reusable"
);
}
#[test]
fn write_bytes_fast_path_matches_byte_loop() {
let mut bus = MacMemoryBus::new(64 * 1024);
bus.write_byte(0x0FFF, 0xCC);
bus.write_byte(0x13E8, 0xCC);
let payload: Vec<u8> = (0..1000).map(|i| ((i * 37) & 0xFF) as u8).collect();
bus.write_bytes(0x1000, &payload);
assert_eq!(bus.read_bytes(0x1000, 1000), payload);
assert_eq!(
bus.read_byte(0x0FFF),
0xCC,
"byte before write_bytes window"
);
assert_eq!(bus.read_byte(0x13E8), 0xCC, "byte after write_bytes window");
}
#[test]
fn read_pstring_handles_zero_and_max_lengths() {
let mut bus = MacMemoryBus::new(8 * 1024);
bus.write_byte(0x100, 0);
assert_eq!(bus.read_pstring(0x100), Vec::<u8>::new());
bus.write_pstring(0x200, &vec![0x77u8; 255]);
assert_eq!(bus.read_pstring(0x200), vec![0x77u8; 255]);
}
#[test]
fn write_pstring_clamps_to_255_bytes() {
let mut bus = MacMemoryBus::new(8 * 1024);
let huge = vec![0x33u8; 1000];
bus.write_pstring(0x100, &huge);
assert_eq!(bus.read_byte(0x100), 255);
assert_eq!(bus.read_pstring(0x100).len(), 255);
assert_eq!(
bus.read_byte(0x100 + 256),
0,
"byte after the clamped 255-byte payload must be untouched"
);
}
#[test]
fn read_bytes_into_matches_read_bytes() {
let mut bus = MacMemoryBus::new(64 * 1024);
for i in 0..1024u32 {
bus.write_byte(0x1000 + i, ((i.wrapping_mul(13)) & 0xFF) as u8);
}
let baseline = bus.read_bytes(0x1000, 619);
let mut into = vec![0u8; 619];
bus.read_bytes_into(0x1000, &mut into);
assert_eq!(
baseline, into,
"read_bytes_into fast path must return identical bytes to read_bytes"
);
let baseline_straddle = bus.read_bytes(0xFFF0, 32);
let mut into_straddle = vec![0u8; 32];
bus.read_bytes_into(0xFFF0, &mut into_straddle);
assert_eq!(
baseline_straddle, into_straddle,
"read_bytes_into must match read_bytes even on the boundary fallback"
);
let mut empty: [u8; 0] = [];
bus.read_bytes_into(0x1234, &mut empty);
}
#[test]
fn fill_zeros_clears_target_bytes_only() {
let mut bus = MacMemoryBus::new(64 * 1024);
for i in 0..1024u32 {
bus.write_byte(0x1000 + i, 0xAA);
}
bus.fill_zeros(0x1100, 100);
for i in 0..0x100u32 {
assert_eq!(bus.read_byte(0x1000 + i), 0xAA, "before window untouched");
}
for i in 0..100u32 {
assert_eq!(bus.read_byte(0x1100 + i), 0, "fill_zeros target zero");
}
for i in 0..100u32 {
assert_eq!(bus.read_byte(0x1164 + i), 0xAA, "after window untouched");
}
bus.fill_zeros(0x1000, 0);
assert_eq!(bus.read_byte(0x1000), 0xAA);
for i in 0u32..16 {
bus.write_byte(0xFFF0 + i, 0xCC);
}
bus.fill_zeros(0xFFF0, 32); for i in 0u32..16 {
assert_eq!(
bus.read_byte(0xFFF0 + i),
0,
"in-RAM tail of straddling fill_zeros"
);
}
}
}