use std::cell::{RefCell, UnsafeCell};
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
#[cfg(not(target_arch = "wasm32"))]
use std::sync::OnceLock;
use super::globals::LowMemGlobals;
const LEGACY_SOUND_BUFFER_WORDS: u32 = 370;
const LEGACY_SOUND_BUFFER_BYTES: u32 = LEGACY_SOUND_BUFFER_WORDS * 2;
const SYNTHETIC_RESERVE_BYTES: u32 = 64 * 1024;
const BOOT_ROM_SHADOW_BASE: u32 = 0x4081_0006;
const BOOT_ROM_SHADOW: [u8; 2] = 0x0372u16.to_be_bytes();
#[cfg(not(target_arch = "wasm32"))]
static FB_WRITE_TRACE_RANGE: OnceLock<Option<(u32, u32)>> = OnceLock::new();
#[cfg(not(target_arch = "wasm32"))]
static ALLOC_TRACE_MIN: OnceLock<Option<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))
})
})
}
#[cfg(target_arch = "wasm32")]
#[inline]
fn alloc_trace_min() -> Option<u32> {
None
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
fn alloc_trace_min() -> Option<u32> {
*ALLOC_TRACE_MIN.get_or_init(|| {
std::env::var("SYSTEMLESS_TRACE_ALLOC_MIN")
.ok()
.and_then(|value| {
let value = value.trim();
let parsed = if let Some(hex) = value
.strip_prefix("0x")
.or_else(|| value.strip_prefix("0X"))
{
u32::from_str_radix(hex, 16).ok()
} else {
value.parse().ok()
}?;
Some(parsed)
})
})
}
#[inline]
fn trace_alloc_event(event: &str, addr: u32, size: u32, bucket: u32) {
if let Some(min) = alloc_trace_min() {
if size >= min || bucket >= min {
eprintln!(
"[ALLOC] {} addr=${:08X} size={} bucket={}",
event, addr, size, bucket
);
}
}
}
#[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"))]
static MEM_WRITE_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"))]
#[inline]
fn mem_write_trace_range() -> Option<(u32, u32)> {
*MEM_WRITE_TRACE_RANGE.get_or_init(|| {
std::env::var("SYSTEMLESS_TRACE_MEM_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))
})
})
}
#[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
}
#[cfg(not(target_arch = "wasm32"))]
pub fn mem_write_trace_active() -> bool {
mem_write_trace_range().is_some()
}
#[cfg(target_arch = "wasm32")]
pub fn mem_write_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
);
}
}
}
#[inline]
fn maybe_log_mem_write(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_write_trace_range() {
if address >= start && address <= end {
let pc = CURRENT_PC.with(|p| *p.borrow());
eprintln!(
"[MEM-WRITE] 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 application_memory_limit(&self) -> u32 {
self.ram_size()
}
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,
addressing_32_bit: bool,
globals: LowMemGlobals,
heap_ptr: u32,
synthetic_ptr: u32,
synthetic_floor: u32,
readonly_code_ranges: Vec<(u32, u32)>,
readonly_code_span: Option<(u32, u32)>,
free_blocks: HashMap<u32, Vec<u32>>,
alloc_sizes: HashMap<u32, u32>,
reserved_heap_ranges: Vec<(u32, u32)>,
alloc_bucket_sizes: HashMap<u32, u32>,
write_probe_original: Option<HashMap<u32, u8>>,
write_probe_invalid: bool,
write_probe_overflowed: bool,
}
pub(crate) const WRITE_PROBE_MAX_ENTRIES: usize = 4096;
#[derive(Clone)]
struct SharedRam(Rc<UnsafeCell<Box<[u8]>>>);
impl SharedRam {
#[inline]
fn len(&self) -> usize {
unsafe { (&*self.0.get()).len() }
}
#[inline]
fn as_ptr(&self) -> *const u8 {
unsafe { (&*self.0.get()).as_ptr() }
}
#[inline]
fn as_mut_ptr(&self) -> *mut u8 {
unsafe { (&mut *self.0.get()).as_mut_ptr() }
}
}
#[derive(Clone)]
pub(crate) struct SharedRamRegion {
ram: SharedRam,
offset: usize,
len: usize,
}
impl std::fmt::Debug for SharedRamRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SharedRamRegion")
.field("offset", &self.offset)
.field("len", &self.len)
.finish_non_exhaustive()
}
}
impl SharedRamRegion {
pub(crate) fn len(&self) -> usize {
self.len
}
#[inline]
pub(crate) unsafe fn read(&self, offset: usize) -> Option<u8> {
(offset < self.len).then(|| {
unsafe { *self.ram.as_ptr().add(self.offset + offset) }
})
}
#[inline]
pub(crate) unsafe fn write(&self, offset: usize, value: u8) -> Option<()> {
if offset >= self.len {
return None;
}
unsafe {
*self.ram.as_mut_ptr().add(self.offset + offset) = value;
}
Some(())
}
pub(crate) fn snapshot(&self) -> Vec<u8> {
(0..self.len)
.map(|offset| {
unsafe { self.read(offset).expect("bounded shared RAM read") }
})
.collect()
}
pub(crate) fn detached_clone(&self) -> Self {
let bytes = self.snapshot();
let ram = SharedRam(Rc::new(UnsafeCell::new(bytes.into_boxed_slice())));
Self {
len: ram.len(),
ram,
offset: 0,
}
}
}
enum RamStorage {
Owned(Vec<u8>),
Shared(SharedRam),
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::Shared(v) => {
if index < v.len() {
unsafe { *v.as_ptr().add(index) }
} else {
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::Shared(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::Shared(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::Shared(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::Shared(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::Shared(v) => unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), v.as_mut_ptr().add(index), 2);
},
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::Shared(v) => unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), v.as_mut_ptr().add(index), 4);
},
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::Shared(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 copy_bytes_in_bounds(&mut self, src_index: usize, dst_index: usize, len: usize) {
match self {
RamStorage::Owned(v) => unsafe {
std::ptr::copy(
v.as_ptr().add(src_index),
v.as_mut_ptr().add(dst_index),
len,
);
},
RamStorage::Shared(v) => unsafe {
std::ptr::copy(
v.as_ptr().add(src_index),
v.as_mut_ptr().add(dst_index),
len,
);
},
RamStorage::External(ptr, _) => unsafe {
std::ptr::copy(ptr.add(src_index), ptr.add(dst_index), len);
},
}
}
#[inline]
fn copy_mapped_bytes_in_bounds(
&mut self,
src_index: usize,
dst_index: usize,
len: usize,
map: &[u8; 256],
) {
match self {
RamStorage::Owned(v) => unsafe {
let src = v.as_ptr().add(src_index);
let dst = v.as_mut_ptr().add(dst_index);
for offset in 0..len {
*dst.add(offset) = map[*src.add(offset) as usize];
}
},
RamStorage::Shared(v) => unsafe {
let src = v.as_ptr().add(src_index);
let dst = v.as_mut_ptr().add(dst_index);
for offset in 0..len {
*dst.add(offset) = map[*src.add(offset) as usize];
}
},
RamStorage::External(ptr, _) => unsafe {
let src = ptr.add(src_index);
let dst = ptr.add(dst_index);
for offset in 0..len {
*dst.add(offset) = map[*src.add(offset) as usize];
}
},
}
}
#[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::Shared(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::Shared(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::Shared(v) => {
let end = index.checked_add(len)?;
(end <= v.len())
.then(|| unsafe { std::slice::from_raw_parts(v.as_ptr().add(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::Shared(v) => {
let end = index.checked_add(len)?;
(end <= v.len()).then(|| unsafe {
std::slice::from_raw_parts_mut(v.as_mut_ptr().add(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::Shared(v) => {
if index < v.len() {
unsafe {
*v.as_mut_ptr().add(index) = value;
}
}
}
RamStorage::External(ptr, len) => {
if index < *len {
unsafe {
*ptr.add(index) = value;
}
}
}
}
}
}
impl MacMemoryBus {
#[inline]
fn boot_rom_shadow_byte(address: u32) -> Option<u8> {
let offset = address.checked_sub(BOOT_ROM_SHADOW_BASE)? as usize;
BOOT_ROM_SHADOW.get(offset).copied()
}
pub(crate) fn allocation_bucket_size(size: u32) -> u32 {
((size + 3) & !3).max(4)
}
fn can_reuse_bucket_for_request(bucket: u32, requested: u32) -> bool {
let max_bucket = if requested <= 1024 {
4096
} else {
requested.saturating_mul(2).saturating_add(4096)
};
bucket <= max_bucket
}
fn legacy_sound_base_address(ram_size: usize, screen_base: u32, screen_bytes: u32) -> u32 {
let ram_size = ram_size as u32;
let preferred = screen_base.saturating_add(screen_bytes);
if preferred >= 0x1000 && preferred + LEGACY_SOUND_BUFFER_BYTES <= ram_size {
preferred
} else if ram_size >= 0x1000 + LEGACY_SOUND_BUFFER_BYTES {
ram_size - LEGACY_SOUND_BUFFER_BYTES
} else {
0
}
}
fn init_legacy_sound_buffer(&mut self, sound_base: u32) {
if sound_base == 0 {
return;
}
for word in 0..LEGACY_SOUND_BUFFER_WORDS {
let addr = sound_base + word * 2;
self.write_byte(addr, 0x80);
self.write_byte(addr + 1, 0x00);
}
}
pub fn block_move(&mut self, src: u32, dst: u32, count: u32) {
if (count as i32) <= 0 {
return;
}
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed)
&& fb_write_trace_range().is_none()
&& self.write_probe_original.is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none() && self.write_probe_original.is_none();
let count_usize = count as usize;
let translated_src = self.range_translates_contiguously(src, count_usize);
let translated_dst = self.range_translates_contiguously(dst, count_usize);
let src_for_overlap = self.translate_guest_address(src);
let dst_for_overlap = self.translate_guest_address(dst);
let src = translated_src.unwrap_or(src);
let dst = translated_dst.unwrap_or(dst);
let src_end = (src as u64).saturating_add(count as u64);
let dst_end = (dst as u64).saturating_add(count as u64);
if fast
&& translated_src.is_some()
&& translated_dst.is_some()
&& !self.readonly_code_overlaps(dst, count)
&& 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_for_overlap > src_for_overlap
&& dst_for_overlap < src_for_overlap.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 synthetic_floor = screen_buffer_start.saturating_sub(SYNTHETIC_RESERVE_BYTES);
let mut bus = Self {
ram: RamStorage::Owned(vec![0; ram_size]),
ram_size: ram_size as u32,
addressing_32_bit: true,
globals: LowMemGlobals::new(),
heap_ptr: 0x200000, synthetic_ptr: screen_buffer_start,
synthetic_floor,
readonly_code_ranges: Vec::new(),
readonly_code_span: None,
free_blocks: HashMap::new(),
alloc_sizes: HashMap::new(),
reserved_heap_ranges: Vec::new(),
alloc_bucket_sizes: HashMap::new(),
write_probe_original: None,
write_probe_invalid: false,
write_probe_overflowed: false,
};
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 = 816;
let screen_width: u16 = 800;
let screen_height: u16 = 600;
bus.write_long(0x0824, screen_base);
bus.write_word(super::globals::addr::SCREEN_ROW, screen_row_bytes);
use super::globals::addr;
let sound_base = Self::legacy_sound_base_address(
ram_size,
screen_base,
screen_row_bytes as u32 * screen_height as u32,
);
bus.write_long(addr::SOUND_BASE, sound_base);
bus.init_legacy_sound_buffer(sound_base);
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
}
pub(crate) fn configure_screen_depth(&mut self, depth: u16) {
debug_assert!(matches!(depth, 1 | 2 | 4 | 8));
let profile = crate::machine_profile::reference_machine_profile();
let visible_row_bytes = (u32::from(profile.screen_width) * u32::from(depth)).div_ceil(8);
let row_bytes = (visible_row_bytes / 16 + 1) * 16;
self.write_word(super::globals::addr::SCREEN_ROW, row_bytes as u16);
self.write_word(super::globals::addr::SCREEN_BITS + 4, row_bytes as u16);
}
#[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
};
let synthetic_floor = screen_buffer_start.saturating_sub(SYNTHETIC_RESERVE_BYTES);
Self {
ram: RamStorage::External(ram_ptr, ram_size),
ram_size: ram_size as u32,
addressing_32_bit: true,
globals,
heap_ptr: 0x200000,
synthetic_ptr: screen_buffer_start,
synthetic_floor,
readonly_code_ranges: Vec::new(),
readonly_code_span: None,
free_blocks: HashMap::new(),
alloc_sizes: HashMap::new(),
reserved_heap_ranges: Vec::new(),
alloc_bucket_sizes: HashMap::new(),
write_probe_original: None,
write_probe_invalid: false,
write_probe_overflowed: false,
}
}
pub(crate) fn begin_write_probe(&mut self) {
self.write_probe_original = Some(HashMap::new());
self.write_probe_invalid = false;
self.write_probe_overflowed = false;
}
pub(crate) fn cancel_write_probe(&mut self) {
self.write_probe_original = None;
self.write_probe_invalid = false;
self.write_probe_overflowed = false;
}
pub(crate) fn take_write_probe_overflow(&mut self) -> bool {
std::mem::take(&mut self.write_probe_overflowed)
}
pub(crate) fn finish_write_probe_unchanged(&mut self) -> bool {
let Some(original) = self.write_probe_original.take() else {
return false;
};
let unchanged = !self.write_probe_invalid
&& original
.into_iter()
.all(|(address, value)| self.ram.get_in_bounds(address as usize) == value);
self.write_probe_invalid = false;
self.write_probe_overflowed = false;
unchanged
}
#[inline]
fn record_write_probe_byte(&mut self, address: u32) {
if self.write_probe_original.is_none() {
return;
}
if address >= self.ram_size {
self.write_probe_invalid = true;
return;
}
let original = self.ram.get_in_bounds(address as usize);
let journal = self
.write_probe_original
.as_mut()
.expect("write probe checked above");
journal.entry(address).or_insert(original);
if journal.len() > WRITE_PROBE_MAX_ENTRIES {
self.write_probe_original = None;
self.write_probe_overflowed = true;
}
}
pub fn reserve_heap(&mut self, size: u32) {
let aligned = (size + 3) & !3;
self.reserve_heap_until(0x200000 + aligned);
}
pub fn reserve_heap_until(&mut self, end_addr: u32) {
let aligned = (end_addr + 3) & !3;
self.heap_ptr = self.heap_ptr.max(aligned);
}
pub(crate) fn reserve_heap_range(&mut self, start_addr: u32, end_addr: u32) {
let start = start_addr & !3;
let end = (end_addr.saturating_add(3)) & !3;
if start >= end {
return;
}
self.reserved_heap_ranges.push((start, end));
self.reserved_heap_ranges.sort_unstable();
}
fn bump_allocation_address(&self, size: u32, alignment: u32) -> Option<(u32, u32)> {
let mut ptr = (self.heap_ptr + alignment - 1) & !(alignment - 1);
loop {
let new_ptr = ptr.checked_add(size)?;
let overlap = self
.reserved_heap_ranges
.iter()
.find(|&&(start, end)| ptr < end && new_ptr > start);
if let Some(&(_, end)) = overlap {
ptr = (end + alignment - 1) & !(alignment - 1);
continue;
}
return Some((ptr, new_ptr));
}
}
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);
trace_alloc_event("reuse-exact", addr, size, aligned);
return addr;
}
let best = self
.free_blocks
.iter()
.filter(|(&k, v)| {
k > aligned && !v.is_empty() && Self::can_reuse_bucket_for_request(k, aligned)
})
.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);
trace_alloc_event("reuse-best", addr, size, bucket);
return addr;
}
}
let Some((ptr, new_ptr)) = self.bump_allocation_address(aligned, 4) else {
return 0;
};
if new_ptr >= self.synthetic_floor {
eprintln!(
"[ALLOC] Out of memory: requesting {} bytes, heap at ${:08X}, limit ${:08X}",
size, ptr, self.synthetic_floor
);
return 0; }
self.heap_ptr = new_ptr;
self.alloc_sizes.insert(ptr, size);
trace_alloc_event("bump", ptr, size, aligned);
ptr
}
pub(crate) fn alloc_synthetic(&mut self, size: u32) -> u32 {
let aligned = Self::allocation_bucket_size(size);
let Some(ptr) = self.synthetic_ptr.checked_sub(aligned) else {
return 0;
};
if ptr < self.synthetic_floor {
eprintln!(
"[ALLOC] Out of synthetic memory: requesting {} bytes, floor at ${:08X}, synthetic at ${:08X}",
size, self.synthetic_floor, self.synthetic_ptr
);
return 0;
}
self.synthetic_ptr = ptr;
self.fill_bytes(ptr, aligned, 0);
ptr
}
pub(crate) fn protect_readonly_code(&mut self, address: u32, len: u32) {
if len != 0 {
let end = address.saturating_add(len);
self.readonly_code_ranges.push((address, end));
self.readonly_code_span = Some(match self.readonly_code_span {
Some((lo, hi)) => (lo.min(address), hi.max(end)),
None => (address, end),
});
}
}
pub(crate) fn write_readonly_code_word(&mut self, address: u32, value: u16) {
if (address as u64) + 2 <= self.ram_size as u64 {
self.ram.write_word_in_bounds(address as usize, value);
}
}
fn readonly_code_overlaps(&self, address: u32, len: u32) -> bool {
let Some((span_start, span_end)) = self.readonly_code_span else {
return false;
};
let end = (address as u64).saturating_add(len as u64);
if end <= span_start as u64 || address as u64 >= span_end as u64 {
return false;
}
self.readonly_code_ranges
.iter()
.any(|&(start, stop)| (start as u64) < end && (stop as u64) > address as u64)
}
pub fn alloc_aligned(&mut self, size: u32, alignment: u32) -> u32 {
if alignment <= 4 || !alignment.is_power_of_two() {
return self.alloc(size);
}
let aligned = Self::allocation_bucket_size(size);
if let Some(blocks) = self.free_blocks.get_mut(&aligned) {
if let Some(index) = blocks.iter().position(|addr| addr % alignment == 0) {
let addr = blocks.swap_remove(index);
self.alloc_sizes.insert(addr, size);
trace_alloc_event("reuse-exact-aligned", addr, size, aligned);
return addr;
}
}
let best = self
.free_blocks
.iter()
.filter(|(&k, v)| {
k > aligned
&& !v.is_empty()
&& Self::can_reuse_bucket_for_request(k, aligned)
&& v.iter().any(|addr| addr % alignment == 0)
})
.map(|(&k, _)| k)
.min();
if let Some(bucket) = best {
let blocks = self
.free_blocks
.get_mut(&bucket)
.expect("free bucket exists");
let index = blocks
.iter()
.position(|addr| addr % alignment == 0)
.expect("aligned free block exists");
let addr = blocks.swap_remove(index);
self.alloc_sizes.insert(addr, size);
self.alloc_bucket_sizes.insert(addr, bucket);
trace_alloc_event("reuse-best-aligned", addr, size, bucket);
return addr;
}
let Some((ptr, new_ptr)) = self.bump_allocation_address(aligned, alignment) else {
return 0;
};
if new_ptr >= self.synthetic_floor {
eprintln!(
"[ALLOC] Out of memory: requesting {} bytes aligned to {}, heap at ${:08X}, limit ${:08X}",
size, alignment, self.heap_ptr, self.synthetic_floor
);
return 0;
}
self.heap_ptr = new_ptr;
self.alloc_sizes.insert(ptr, size);
trace_alloc_event("bump-aligned", ptr, size, aligned);
ptr
}
pub fn get_alloc_size(&self, addr: u32) -> Option<u32> {
self.alloc_sizes.get(&addr).copied()
}
pub(crate) fn heap_bump_ptr(&self) -> u32 {
self.heap_ptr
}
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);
trace_alloc_event("free", addr, size, bucket);
}
}
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) => {
assert!(e <= v.len());
&v[s..e]
}
RamStorage::Shared(v) => {
assert!(e <= v.len());
unsafe { std::slice::from_raw_parts(v.as_ptr().add(s), len as usize) }
}
RamStorage::External(ptr, max_len) => {
assert!(e <= *max_len);
unsafe { std::slice::from_raw_parts(ptr.add(s), len as usize) }
}
}
}
#[inline]
pub fn copy_ram_bytes(&mut self, src: u32, dst: u32, len: u32) -> bool {
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed)
&& fb_write_trace_range().is_none()
&& self.write_probe_original.is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none() && self.write_probe_original.is_none();
let translated_src = self.range_translates_contiguously(src, len as usize);
let translated_dst = self.range_translates_contiguously(dst, len as usize);
let src = translated_src.unwrap_or(src);
let dst = translated_dst.unwrap_or(dst);
let src_end = (src as u64).saturating_add(len as u64);
let dst_end = (dst as u64).saturating_add(len as u64);
if translated_src.is_none()
|| translated_dst.is_none()
|| src_end > self.ram_size as u64
|| dst_end > self.ram_size as u64
{
return false;
}
if fast {
self.ram
.copy_bytes_in_bounds(src as usize, dst as usize, len as usize);
return true;
}
for offset in 0..len {
let byte = self.read_byte(src.wrapping_add(offset));
self.write_byte(dst.wrapping_add(offset), byte);
}
true
}
#[inline]
pub fn copy_mapped_ram_bytes(&mut self, src: u32, dst: u32, len: u32, map: &[u8; 256]) -> bool {
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed)
&& fb_write_trace_range().is_none()
&& self.write_probe_original.is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none() && self.write_probe_original.is_none();
let translated_src = self.range_translates_contiguously(src, len as usize);
let translated_dst = self.range_translates_contiguously(dst, len as usize);
let src = translated_src.unwrap_or(src);
let dst = translated_dst.unwrap_or(dst);
let src_end = (src as u64).saturating_add(len as u64);
let dst_end = (dst as u64).saturating_add(len as u64);
if translated_src.is_none()
|| translated_dst.is_none()
|| src_end > self.ram_size as u64
|| dst_end > self.ram_size as u64
{
return false;
}
if fast {
self.ram
.copy_mapped_bytes_in_bounds(src as usize, dst as usize, len as usize, map);
return true;
}
for offset in 0..len {
let byte = map[self.read_byte(src.wrapping_add(offset)) as usize];
self.write_byte(dst.wrapping_add(offset), byte);
}
true
}
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(crate) fn shared_ram_region(&mut self, address: u32, len: u32) -> Option<SharedRamRegion> {
let end = address.checked_add(len)?;
if end > self.ram_size {
return None;
}
let ram = match &mut self.ram {
RamStorage::Owned(bytes) => {
let shared = SharedRam(Rc::new(UnsafeCell::new(
std::mem::take(bytes).into_boxed_slice(),
)));
self.ram = RamStorage::Shared(shared.clone());
shared
}
RamStorage::Shared(ram) => ram.clone(),
RamStorage::External(_, _) => return None,
};
Some(SharedRamRegion {
ram,
offset: address as usize,
len: len as usize,
})
}
pub fn set_addressing_32_bit(&mut self, enabled: bool) {
self.addressing_32_bit = enabled;
}
pub fn addressing_32_bit(&self) -> bool {
self.addressing_32_bit
}
#[inline]
pub fn translate_guest_address(&self, address: u32) -> u32 {
if self.addressing_32_bit {
address
} else {
address & 0x00FF_FFFF
}
}
#[inline]
fn range_translates_contiguously(&self, address: u32, len: usize) -> Option<u32> {
let translated = self.translate_guest_address(address);
let address_space_end = if self.addressing_32_bit {
u64::from(u32::MAX) + 1
} else {
0x0100_0000
};
((translated as u64).saturating_add(len as u64) <= address_space_end).then_some(translated)
}
pub(crate) fn fast_mem_window(&mut self) -> Option<(*mut u8, u32)> {
if !self.addressing_32_bit
|| fb_write_trace_range().is_some()
|| mem_read_trace_active()
|| mem_write_trace_active()
|| watchpoint_armed()
|| self.write_probe_original.is_some()
{
return None;
}
let ptr = match &mut self.ram {
RamStorage::Owned(v) => v.as_mut_ptr(),
RamStorage::Shared(_) => return None,
RamStorage::External(ptr, _) => *ptr,
};
Some((ptr, 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 address = self.translate_guest_address(address);
let v = if address < self.ram_size {
self.ram.get_in_bounds(address as usize)
} else if let Some(value) = Self::boot_rom_shadow_byte(address) {
value
} 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 translated = self.range_translates_contiguously(address, 2);
let v = if translated.is_some_and(|address| (address as u64) + 2 <= self.ram_size as u64) {
let address = translated.unwrap();
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 translated = self.range_translates_contiguously(address, 4);
let v = if translated.is_some_and(|address| (address as u64) + 4 <= self.ram_size as u64) {
let address = translated.unwrap();
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) {
let address = self.translate_guest_address(address);
if self.readonly_code_overlaps(address, 1) {
return;
}
self.record_write_probe_byte(address);
maybe_log_mem_write(address, 1, value as u32);
let fb_trace = fb_write_trace_range();
if fb_trace.is_some() {
maybe_log_fb_write(address, value);
}
if let Some((start, end)) = fb_trace {
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) {
let translated = self.range_translates_contiguously(address, 2);
let protected_address = translated.unwrap_or(address);
if self.readonly_code_overlaps(protected_address, 2) {
return;
}
maybe_log_mem_write(address, 2, value as u32);
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed)
&& fb_write_trace_range().is_none()
&& self.write_probe_original.is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none() && self.write_probe_original.is_none();
if fast && translated.is_some_and(|address| (address as u64) + 2 <= self.ram_size as u64) {
let address = translated.unwrap();
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) {
let translated = self.range_translates_contiguously(address, 4);
let protected_address = translated.unwrap_or(address);
if self.readonly_code_overlaps(protected_address, 4) {
return;
}
maybe_log_mem_write(address, 4, value);
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed)
&& fb_write_trace_range().is_none()
&& self.write_probe_original.is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none() && self.write_probe_original.is_none();
if fast && translated.is_some_and(|address| (address as u64) + 4 <= self.ram_size as u64) {
let address = translated.unwrap();
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 translated = self.range_translates_contiguously(address, len);
let address = translated.unwrap_or(address);
let end = (address as u64).saturating_add(len as u64);
if translated.is_some() && 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 translated = self.range_translates_contiguously(address, len);
let address = translated.unwrap_or(address);
let end = (address as u64).saturating_add(len as u64);
if translated.is_some() && 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]) {
let translated = self.range_translates_contiguously(address, data.len());
let protected_address = translated.unwrap_or(address);
if self.readonly_code_overlaps(protected_address, data.len() as u32) {
return;
}
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed)
&& fb_write_trace_range().is_none()
&& self.write_probe_original.is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none() && self.write_probe_original.is_none();
let address = translated.unwrap_or(address);
let end = (address as u64).saturating_add(data.len() as u64);
if fast && translated.is_some() && 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) {
let translated = self.range_translates_contiguously(address, len as usize);
let protected_address = translated.unwrap_or(address);
if self.readonly_code_overlaps(protected_address, len) {
return;
}
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed)
&& fb_write_trace_range().is_none()
&& self.write_probe_original.is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none() && self.write_probe_original.is_none();
let address = translated.unwrap_or(address);
let end = (address as u64).saturating_add(len as u64);
if fast && translated.is_some() && 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) {
let translated = self.range_translates_contiguously(address, len as usize);
let protected_address = translated.unwrap_or(address);
if self.readonly_code_overlaps(protected_address, len) {
return;
}
#[cfg(debug_assertions)]
let fast = !WATCHPOINT_ARMED.load(Ordering::Relaxed)
&& fb_write_trace_range().is_none()
&& self.write_probe_original.is_none();
#[cfg(not(debug_assertions))]
let fast = fb_write_trace_range().is_none() && self.write_probe_original.is_none();
let address = translated.unwrap_or(address);
let end = (address as u64).saturating_add(len as u64);
if fast && translated.is_some() && 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
}
fn application_memory_limit(&self) -> u32 {
self.synthetic_floor
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn twenty_four_bit_mode_translates_scalar_and_bulk_accesses() {
let mut bus = MacMemoryBus::new(2 * 1024 * 1024);
bus.set_addressing_32_bit(false);
bus.write_byte(0x0301_0000, 0x12);
bus.write_word(0x0401_0002, 0x3456);
bus.write_long(0xA501_0004, 0x789A_BCDE);
bus.write_bytes(0x7F01_0008, &[1, 2, 3, 4]);
assert_eq!(bus.read_byte(0x0001_0000), 0x12);
assert_eq!(bus.read_word(0x0001_0002), 0x3456);
assert_eq!(bus.read_long(0x0001_0004), 0x789A_BCDE);
assert_eq!(bus.read_bytes(0x0001_0008, 4), [1, 2, 3, 4]);
assert_eq!(bus.read_long(0xEE01_0004), 0x789A_BCDE);
}
#[test]
fn twenty_four_bit_accesses_wrap_at_the_address_space_boundary() {
let mut bus = MacMemoryBus::new(0x0100_0000);
bus.set_addressing_32_bit(false);
bus.write_byte(0x00FF_FFFF, 0x12);
bus.write_byte(0, 0x34);
assert_eq!(bus.read_word(0xABFF_FFFF), 0x1234);
assert_eq!(bus.read_bytes(0xCDFF_FFFF, 2), [0x12, 0x34]);
bus.write_word(0xEFFF_FFFF, 0x5678);
assert_eq!(bus.read_byte(0x00FF_FFFF), 0x56);
assert_eq!(bus.read_byte(0), 0x78);
}
#[test]
fn thirty_two_bit_mode_preserves_tagged_addresses() {
let mut bus = MacMemoryBus::new(2 * 1024 * 1024);
bus.write_byte(0x0001_0000, 0x5A);
assert_eq!(bus.read_byte(0x0301_0000), 0);
assert_eq!(bus.read_byte(0x0001_0000), 0x5A);
assert!(bus.fast_mem_window().is_some());
bus.set_addressing_32_bit(false);
assert_eq!(bus.read_byte(0x0301_0000), 0x5A);
assert!(bus.fast_mem_window().is_none());
}
#[test]
fn new_bus_publishes_default_screen_row_bytes() {
let bus = MacMemoryBus::new(1024);
assert_eq!(bus.read_word(crate::memory::globals::addr::SCREEN_ROW), 816);
}
#[test]
fn boot_rom_shadow_exposes_witnessed_vector_zero_word() {
let bus = MacMemoryBus::new(1024);
assert_eq!(bus.read_byte(0x4081_0006), 0x03);
assert_eq!(bus.read_byte(0x4081_0007), 0x72);
assert_eq!(bus.read_word(0x4081_0006), 0x0372);
assert_eq!(bus.read_byte(0x4081_0005), 0);
assert_eq!(bus.read_byte(0x4081_0008), 0);
}
#[test]
fn boot_rom_shadow_ignores_writes() {
let mut bus = MacMemoryBus::new(1024);
bus.write_word(0x4081_0006, 0xA55A);
assert_eq!(bus.read_word(0x4081_0006), 0x0372);
}
#[test]
fn write_probe_accepts_temporary_writes_that_restore_original_bytes() {
let mut bus = MacMemoryBus::new(1024);
bus.write_long(0x100, 0x1122_3344);
bus.begin_write_probe();
assert!(bus.fast_mem_window().is_none());
bus.write_word(0x100, 0xAABB);
bus.write_byte(0x102, 0xCC);
bus.write_long(0x100, 0x1122_3344);
assert!(bus.finish_write_probe_unchanged());
assert!(bus.fast_mem_window().is_some());
}
#[test]
fn write_probe_rejects_a_changed_final_byte() {
let mut bus = MacMemoryBus::new(1024);
bus.write_long(0x100, 0x1122_3344);
bus.begin_write_probe();
bus.write_byte(0x102, 0xCC);
assert!(!bus.finish_write_probe_unchanged());
}
#[test]
fn write_probe_overflow_voids_the_journal_and_restores_fast_paths() {
let mut bus = MacMemoryBus::new(64 * 1024);
bus.begin_write_probe();
assert!(bus.fast_mem_window().is_none());
for address in 0..WRITE_PROBE_MAX_ENTRIES as u32 {
bus.write_byte(0x1000 + address, 0);
}
assert!(bus.fast_mem_window().is_none());
assert!(!bus.take_write_probe_overflow());
bus.write_byte(0x1000 + WRITE_PROBE_MAX_ENTRIES as u32, 0);
assert!(bus.fast_mem_window().is_some());
assert!(!bus.finish_write_probe_unchanged());
assert!(bus.take_write_probe_overflow());
assert!(
!bus.take_write_probe_overflow(),
"the overflow verdict is consumed once"
);
bus.begin_write_probe();
for _ in 0..(4 * WRITE_PROBE_MAX_ENTRIES) {
bus.write_long(0x2000, 0x1234_5678);
}
assert!(bus.fast_mem_window().is_none());
bus.write_long(0x2000, 0);
assert!(bus.finish_write_probe_unchanged());
assert!(!bus.take_write_probe_overflow());
}
#[test]
fn write_probe_observes_bulk_and_copy_fast_paths() {
let mut bus = MacMemoryBus::new(1024);
bus.write_bytes(0x100, &[1, 2, 3, 4]);
bus.write_bytes(0x200, &[5, 6, 7, 8]);
bus.begin_write_probe();
bus.write_bytes(0x100, &[9, 2, 3, 4]);
assert!(bus.copy_ram_bytes(0x200, 0x204, 4));
assert!(!bus.finish_write_probe_unchanged());
}
#[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 synthetic_allocations_do_not_perturb_guest_heap_addresses() {
let mut bus = MacMemoryBus::new(8 * 1024 * 1024);
let first_guest = bus.alloc(64);
let synthetic = bus.alloc_synthetic(22);
let second_guest = bus.alloc(64);
assert_eq!(second_guest, first_guest + 64);
assert!(synthetic > second_guest);
assert_eq!(bus.read_bytes(synthetic, 24), vec![0; 24]);
assert_eq!(bus.get_alloc_size(synthetic), None);
}
#[test]
fn synthetic_reservation_has_a_stable_guest_memory_boundary() {
let mut bus = MacMemoryBus::new(8 * 1024 * 1024);
let application_limit = bus.application_memory_limit();
bus.reserve_heap_until(application_limit - 4);
assert_eq!(
bus.alloc(4),
0,
"guest allocations must stop at the reservation"
);
let whole_reservation = bus.alloc_synthetic(SYNTHETIC_RESERVE_BYTES);
assert_eq!(whole_reservation, application_limit);
assert_eq!(
bus.alloc_synthetic(4),
0,
"synthetic allocations must not escape their reservation"
);
}
#[test]
fn tiny_allocations_do_not_consume_large_free_blocks() {
let mut bus = MacMemoryBus::new(8 * 1024 * 1024);
let large = bus.alloc(175_414);
assert_ne!(large, 0);
bus.free(large);
let tiny = bus.alloc(4);
assert_ne!(
tiny, large,
"tiny allocations should not consume large resource-sized free blocks"
);
let large_again = bus.alloc(175_414);
assert_eq!(
large_again, large,
"the original large block should remain available for a matching request"
);
}
#[test]
fn alloc_aligned_skips_to_requested_boundary() {
let mut bus = MacMemoryBus::new(4 * 1024 * 1024);
let skew = bus.alloc(5);
assert_eq!(skew, 0x200000);
let aligned = bus.alloc_aligned(170, 256);
assert_eq!(
aligned & 0xFF,
0,
"aligned allocation should start on the requested boundary"
);
assert_eq!(
bus.get_alloc_size(aligned),
Some(170),
"logical size remains the caller-requested size"
);
let next = bus.alloc(4);
assert_eq!(
next,
aligned + MacMemoryBus::allocation_bucket_size(170),
"only the leading alignment gap is skipped"
);
}
#[test]
fn alloc_aligned_reuses_aligned_free_blocks() {
let mut bus = MacMemoryBus::new(4 * 1024 * 1024);
let aligned = bus.alloc_aligned(170, 256);
let skewed = bus.alloc(170);
assert_eq!(aligned & 0xFF, 0);
assert_ne!(skewed & 0xFF, 0);
bus.free(skewed);
bus.free(aligned);
let reused = bus.alloc_aligned(170, 256);
assert_eq!(
reused, aligned,
"aligned allocation should prefer an aligned free block over a skewed one"
);
}
#[test]
fn reserve_heap_is_idempotent_start_of_heap_guard() {
let mut bus = MacMemoryBus::new(4 * 1024 * 1024);
bus.reserve_heap(64);
let first = bus.alloc(12);
bus.reserve_heap(64);
let second = bus.alloc(12);
assert_eq!(
first,
0x200000 + 64,
"first allocation after zone-header reservation must skip the header"
);
assert_eq!(
second,
first + 12,
"re-reserving the same zone-header range must not create a second gap"
);
}
#[test]
fn reserved_heap_range_preserves_space_before_direct_loaded_image() {
let mut bus = MacMemoryBus::new(16 * 1024 * 1024);
let image_start = 0x0080_0000;
let image_end = 0x0090_0000;
bus.reserve_heap(64);
bus.reserve_heap_range(image_start, image_end);
let lower_start = bus.alloc(image_start - (0x0020_0000 + 64));
assert_eq!(lower_start, 0x0020_0000 + 64);
let above_image = bus.alloc(4);
assert_eq!(above_image, image_end);
assert_eq!(bus.get_alloc_size(above_image), Some(4));
}
#[test]
fn new_initializes_legacy_sound_base_buffer() {
let bus = MacMemoryBus::new(4 * 1024 * 1024);
let sound_base = bus.read_long(crate::memory::globals::addr::SOUND_BASE);
assert_eq!(
sound_base, 0x003F_7880,
"SoundBase should sit just past the active framebuffer in the reserved hardware-buffer area"
);
assert!(
sound_base + LEGACY_SOUND_BUFFER_BYTES <= bus.ram_size(),
"the full 370-word sound buffer must be inside RAM"
);
assert_eq!(
bus.read_byte(sound_base),
0x80,
"legacy sound high bytes should start at neutral amplitude"
);
assert_eq!(
bus.read_byte(sound_base + 1),
0,
"legacy sound low bytes overlap disk-speed data and should start clear"
);
assert_eq!(
bus.read_byte(sound_base + LEGACY_SOUND_BUFFER_BYTES - 2),
0x80,
"last legacy sound high byte"
);
assert_eq!(
bus.read_byte(sound_base + LEGACY_SOUND_BUFFER_BYTES - 1),
0,
"last legacy sound low byte"
);
}
#[test]
fn writes_through_legacy_sound_base_do_not_corrupt_ticks() {
let mut bus = MacMemoryBus::new(4 * 1024 * 1024);
let sound_base = bus.read_long(crate::memory::globals::addr::SOUND_BASE);
bus.write_long(crate::memory::globals::addr::TICKS, 1234);
for offset in (0..LEGACY_SOUND_BUFFER_BYTES).step_by(2) {
bus.write_byte(sound_base + offset, 0x80);
}
assert_eq!(
bus.read_long(crate::memory::globals::addr::TICKS),
1234,
"SoundBase must never point at low memory; direct sound-buffer clears must not wrap Ticks"
);
assert_eq!(bus.read_byte(sound_base), 0x80);
assert_eq!(
bus.read_byte(sound_base + LEGACY_SOUND_BUFFER_BYTES - 2),
0x80
);
}
#[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 readonly_code_protection_survives_the_bounding_box_fast_path() {
let mut bus = MacMemoryBus::new(64 * 1024);
bus.write_byte(0x2000, 0x11);
bus.write_byte(0x4000, 0x22);
bus.write_byte(0x3000, 0x33); bus.protect_readonly_code(0x2000, 2);
bus.protect_readonly_code(0x4000, 2);
bus.write_byte(0x2000, 0xFF);
bus.write_word(0x4000, 0xFFFF);
bus.write_long(0x2000, 0xFFFF_FFFF);
assert_eq!(bus.read_byte(0x2000), 0x11, "protected byte is unchanged");
assert_eq!(bus.read_byte(0x4000), 0x22, "protected byte is unchanged");
bus.write_byte(0x3000, 0x44);
assert_eq!(bus.read_byte(0x3000), 0x44, "the gap stays writable");
bus.write_byte(0x0100, 0x55);
bus.write_byte(0x8000, 0x66);
assert_eq!(bus.read_byte(0x0100), 0x55, "below the span is writable");
assert_eq!(bus.read_byte(0x8000), 0x66, "above the span is writable");
}
#[test]
fn copy_ram_bytes_handles_overlap_and_bounds() {
let mut bus = MacMemoryBus::new(64 * 1024);
for i in 0..16u32 {
bus.write_byte(0x1000 + i, i as u8);
}
assert!(bus.copy_ram_bytes(0x1000, 0x1004, 8));
assert_eq!(
bus.read_bytes(0x1000, 12),
vec![0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7],
"RAM copy should match memmove semantics for overlapping ranges"
);
bus.write_byte(0x0FFF, 0xAA);
assert!(!bus.copy_ram_bytes(0x0FFF, 0xFFFF, 2));
assert_eq!(
bus.read_byte(0x0FFF),
0xAA,
"out-of-bounds copy should report failure before writing"
);
}
#[test]
fn copy_mapped_ram_bytes_applies_lookup_table() {
let mut bus = MacMemoryBus::new(64 * 1024);
bus.write_bytes(0x2000, &[1, 2, 3, 4]);
bus.write_bytes(0x3000, &[0xEE; 4]);
let mut map = [0u8; 256];
for (index, slot) in map.iter_mut().enumerate() {
*slot = 255u8.wrapping_sub(index as u8);
}
assert!(bus.copy_mapped_ram_bytes(0x2000, 0x3000, 4, &map));
assert_eq!(bus.read_bytes(0x3000, 4), vec![254, 253, 252, 251]);
assert!(!bus.copy_mapped_ram_bytes(0x2000, 0xFFFF, 2, &map));
}
#[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"
);
}
}
}