fix(memory): ignore emulated writes to mapped BIOS

This commit is contained in:
0xrsydn 2026-10-07 03:31:33 +07:00
commit a0516de748
16 changed files with 462 additions and 34 deletions

View file

@ -357,7 +357,8 @@ fn dma_and_irq_each_consume_a_budget_step_and_dma_errors_fail() {
assert_eq!(stats.irq_entries, 1); assert_eq!(stats.irq_entries, 1);
assert_eq!(stats.instructions, 0); assert_eq!(stats.instructions, 0);
let bus = machine.memory_mut(); let bus = machine.memory_mut();
bus.write32(DMA_BASE + 4, ROM_START).unwrap(); // DMA0 masks cartridge addresses to BIOS; use an actually unmapped destination.
bus.write32(DMA_BASE + 4, 0x0000_4000).unwrap();
bus.write32(DMA_BASE + 8, 0x8000_0001).unwrap(); bus.write32(DMA_BASE + 8, 0x8000_0001).unwrap();
let (stats, outcome) = run_to_checkpoint(&mut machine, &config); let (stats, outcome) = run_to_checkpoint(&mut machine, &config);
assert_eq!(stats.steps, 0); assert_eq!(stats.steps, 0);

View file

@ -322,7 +322,8 @@ fn dma_errors_return_a_diagnostic_without_consuming_a_step() {
let mut machine = boot(&[0xeaff_fffe]); let mut machine = boot(&[0xeaff_fffe]);
let bus = machine.memory_mut(); let bus = machine.memory_mut();
bus.write32(DMA_BASE, 0x0200_0000).unwrap(); bus.write32(DMA_BASE, 0x0200_0000).unwrap();
bus.write32(DMA_BASE + 4, ROM_START).unwrap(); // DMA0 masks cartridge addresses to BIOS; use an actually unmapped destination.
bus.write32(DMA_BASE + 4, 0x0000_4000).unwrap();
bus.write32(DMA_BASE + 8, 0x8000_0001).unwrap(); bus.write32(DMA_BASE + 8, 0x8000_0001).unwrap();
let report = run_bounded(&mut machine, 100); let report = run_bounded(&mut machine, 100);
assert!(matches!( assert!(matches!(

View file

@ -266,7 +266,8 @@ fn dma_and_irq_consume_slice_budget_and_dma_failure_preserves_progress() {
.unwrap(); .unwrap();
assert_eq!(session.stats.irq_entries, 1); assert_eq!(session.stats.irq_entries, 1);
let bus = session.machine.memory_mut(); let bus = session.machine.memory_mut();
bus.write32(DMA_BASE + 4, ROM_START).unwrap(); // DMA0 masks cartridge addresses to BIOS; use an actually unmapped destination.
bus.write32(DMA_BASE + 4, 0x0000_4000).unwrap();
bus.write32(DMA_BASE + 8, 0x8000_0001).unwrap(); bus.write32(DMA_BASE + 8, 0x8000_0001).unwrap();
let cycles = session.machine.cycles(); let cycles = session.machine.cycles();
assert!(session.update(Buttons::default()).is_err()); assert!(session.update(Buttons::default()).is_err());

View file

@ -32,6 +32,8 @@ mod arm_pipeline_tests;
#[cfg(test)] #[cfg(test)]
mod bios_access_tests; mod bios_access_tests;
#[cfg(test)] #[cfg(test)]
mod bios_write_tests;
#[cfg(test)]
mod compare_psr_tests; mod compare_psr_tests;
#[cfg(test)] #[cfg(test)]
mod dma_resume_tests; mod dma_resume_tests;

View file

@ -0,0 +1,276 @@
//! Original BIOS-bus write probes. Images stay immutable; bus transfers still complete.
use super::*;
use crate::{
dma::{DmaError, DMA_BASE},
io::{IF, TIMER_BASE},
machine::{Machine, MachineError, StepKind},
memory::{BIOS_SIZE, ROM_START},
};
const DATA: u32 = 0x9234_abcd;
const RAM: u32 = 0x0200_0100;
fn setup(pc: u32, instruction: u32, thumb: bool) -> (Cpu, Memory, Vec<u8>) {
let mut image = vec![0x55; BIOS_SIZE];
let rom = [instruction, 0xe1a0_0000, 0xe1a0_0000]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
if pc < BIOS_SIZE as u32 {
image[pc as usize..pc as usize + 4].copy_from_slice(&instruction.to_le_bytes());
}
let mut memory = Memory::with_bios(rom, image.clone()).unwrap();
if pc == RAM {
memory.write32(pc, instruction).unwrap();
memory.write32(pc + 4, 0xe1a0_0000).unwrap();
memory.write32(pc + 8, 0xe1a0_0000).unwrap();
}
let mut cpu = Cpu::new(pc);
if thumb {
cpu.instruction_set = InstructionSet::Thumb;
}
cpu.registers[1] = DATA;
(cpu, memory, image)
}
fn unchanged(memory: &Memory, image: &[u8]) {
for (address, expected) in image.iter().enumerate() {
assert_eq!(memory.read8(address as u32).unwrap(), *expected);
}
}
#[test]
fn bios_stores_ignore_data_in_both_states_all_modes_widths_and_address_lanes() {
for (arm, thumb) in [
(0xe5c0_1000, 0x7001),
(0xe1c0_10b0, 0x8001),
(0xe580_1000, 0x6001),
] {
for is_thumb in [false, true] {
for pc in [0x100, RAM, ROM_START] {
for mode in [
Mode::User,
Mode::System,
Mode::Supervisor,
Mode::Irq,
Mode::Fiq,
Mode::Abort,
Mode::Undefined,
] {
for address in [0, 1, 2, 3, 0x3ffc, 0x3ffd, 0x3ffe, 0x3fff] {
let (mut cpu, mut memory, image) =
setup(pc, if is_thumb { thumb } else { arm }, is_thumb);
cpu.apply_status(
0xb000_00c0 | (u32::from(is_thumb) << 5) | mode as u32,
mode,
);
cpu.registers[0] = address;
let mut expected = cpu.clone();
expected.registers[15] += if is_thumb { 2 } else { 4 };
let timing = cpu.step_timed(&mut memory).unwrap();
assert_cpu_arch_eq!(cpu, expected);
assert_eq!(timing.data_cycles, 1);
assert_eq!(memory.cycles(), 0); // CPU-only stepping never clocks devices.
unchanged(&memory, &image);
assert_eq!(memory.write8(0, 1), Err(MemoryError::ReadOnly(0)));
}
}
}
}
}
}
#[test]
fn ignored_block_stores_write_back_but_a_later_unmapped_word_rolls_back_the_instruction() {
for thumb in [false, true] {
for base in [0x3ff8, 0x3ffc] {
let (mut cpu, mut memory, image) =
setup(ROM_START, if thumb { 0xc006 } else { 0xe8a0_0006 }, thumb);
cpu.registers[0] = base;
cpu.registers[2] = !DATA;
let before = cpu.clone();
let result = cpu.step_timed(&mut memory);
if base == 0x3ff8 {
assert_eq!(result.unwrap().data_cycles, 2);
assert_eq!(cpu.registers[0], base + 8);
assert_eq!(cpu.pc(), ROM_START + if thumb { 2 } else { 4 });
} else {
assert_eq!(result, Err(CpuError::Memory(MemoryError::Unmapped(0x4000))));
assert_eq!(cpu, before);
assert_eq!(cpu.step_timed(&mut memory), result);
}
unchanged(&memory, &image);
}
}
}
#[test]
fn swap_keeps_protected_read_lanes_and_does_not_latch_the_ignored_write_value() {
for byte in [false, true] {
for lane in 0..4 {
let instruction = if byte { 0xe140_2091_u32 } else { 0xe100_2091 }; // SWPB/SWP r2,r1,[r0]
let rom = [instruction, 0xe590_3000, 0xe1a0_0000, 0xe1a0_0000]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
// Enter BIOS using a synthetic image to establish known protected read history.
let mut image = vec![0x55; BIOS_SIZE];
image[0x100..0x104].copy_from_slice(&0xe12f_ff14_u32.to_le_bytes()); // BX r4
image[0x108..0x10c].copy_from_slice(&0x8765_4321_u32.to_le_bytes());
let mut memory = Memory::with_bios(rom, image.clone()).unwrap();
let mut cpu = Cpu::new(0x100);
cpu.registers[4] = ROM_START;
cpu.step(&mut memory).unwrap();
cpu.registers[0] = lane;
cpu.registers[1] = DATA;
assert_eq!(cpu.step_timed(&mut memory).unwrap().data_cycles, 2);
let rotated = 0x8765_4321_u32.rotate_right(lane * 8);
assert_eq!(cpu.registers[2], if byte { rotated & 255 } else { rotated });
cpu.step(&mut memory).unwrap(); // Protected LDR r3,[r0] retains the old BIOS fetch.
assert_eq!(cpu.registers[3], rotated);
unchanged(&memory, &image);
}
}
}
#[test]
fn swap_with_unknown_bios_read_history_still_fails_before_the_write() {
let (mut cpu, mut memory, image) = setup(ROM_START, 0xe100_2091, false);
cpu.registers[0] = 0;
let before = cpu.clone();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(0)))
);
assert_eq!(cpu, before);
unchanged(&memory, &image);
}
#[test]
fn ignored_cpu_stores_still_advance_timers_and_preserve_strict_host_diagnostics() {
let (mut cpu, mut memory, image) = setup(ROM_START, 0xe480_1004, false); // STR r1,[r0],#4
cpu.registers[0] = 0x200;
memory.write32(TIMER_BASE, 0x0080_0000).unwrap();
let mut machine = Machine::new(cpu, memory);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], 0x204);
assert_eq!(machine.last_timing().data_cycles, 1);
assert_eq!(u64::from(machine.last_timing().total()), machine.cycles());
assert_eq!(
u64::from(machine.memory().read16(TIMER_BASE).unwrap()),
machine.cycles()
);
unchanged(machine.memory(), &image);
for address in [0, 0x3ffc] {
assert_eq!(
machine.memory_mut().write8(address, 0),
Err(MemoryError::ReadOnly(address))
);
assert_eq!(
machine.memory_mut().write16(address, 0),
Err(MemoryError::ReadOnly(address))
);
assert_eq!(
machine.memory_mut().write32(address, 0),
Err(MemoryError::ReadOnly(address))
);
}
}
#[test]
fn unmapped_bios_and_cartridge_destinations_remain_retryable_diagnostics() {
for (address, mapped_bios) in [
(0, false),
(0x4000, true),
(0x0100_0000, true),
(ROM_START, true),
] {
let (mut cpu, mut memory, _) = setup(ROM_START, 0xe480_1004, false);
if !mapped_bios {
memory = Memory::new(
[0xe480_1004_u32, 0xe1a0_0000, 0xe1a0_0000]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect(),
)
.unwrap();
}
cpu.registers[0] = address;
let before = cpu.clone();
let mut machine = Machine::new(cpu, memory);
let error = machine.step().unwrap_err();
assert_eq!(
error,
MachineError::Cpu(CpuError::Memory(if address == ROM_START {
MemoryError::ReadOnly(address)
} else {
MemoryError::Unmapped(address)
}))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.step(), Err(error));
}
}
#[test]
fn dma_ignored_writes_pay_cycles_complete_and_raise_the_normal_completion_irq() {
// DMA0's destination mask turns ROM_START into BIOS address zero.
for (word, destination) in [(false, 0), (true, 0), (false, ROM_START), (true, ROM_START)] {
let (cpu, mut memory, image) = setup(ROM_START, 0xeaff_fffe, false);
memory.write32(RAM, DATA).unwrap();
memory.write32(DMA_BASE, RAM).unwrap();
memory.write32(DMA_BASE + 4, destination).unwrap();
memory
.write32(DMA_BASE + 8, if word { 0xc400_0001 } else { 0xc000_0001 })
.unwrap();
memory.write32(TIMER_BASE, 0x0080_0000).unwrap();
let before = cpu.clone();
let mut machine = Machine::new(cpu, memory);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), if word { 9 } else { 6 }); // Startup + EWRAM read + BIOS write.
assert_eq!(
u64::from(machine.memory().read16(TIMER_BASE).unwrap()),
machine.cycles()
);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x100);
assert_eq!(machine.memory().read16(DMA_BASE + 10).unwrap() & 0x8000, 0);
unchanged(machine.memory(), &image);
// A blocked source now reuses this completed unit's data, not BIOS bytes.
machine.memory_mut().write32(DMA_BASE, 0).unwrap();
machine.memory_mut().write32(DMA_BASE + 4, RAM + 8).unwrap();
machine
.memory_mut()
.write32(DMA_BASE + 8, 0x8400_0001)
.unwrap();
machine.step().unwrap();
assert_eq!(
machine.memory().read32(RAM + 8).unwrap(),
if word { DATA } else { 0xabcd_abcd }
);
}
}
#[test]
fn dma_bios_boundary_keeps_completed_units_and_retries_the_unmapped_destination() {
let (cpu, mut memory, image) = setup(ROM_START, 0xeaff_fffe, false);
memory.write32(RAM, DATA).unwrap();
memory.write32(DMA_BASE, RAM).unwrap();
memory.write32(DMA_BASE + 4, 0x3ffc).unwrap();
memory.write32(DMA_BASE + 8, 0xc400_0002).unwrap();
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap();
let cycles = machine.cycles();
let timing = machine.last_timing();
let error = MachineError::Dma(DmaError::Memory {
channel: 0,
error: MemoryError::Unmapped(0x4000),
});
assert_eq!(machine.step(), Err(error.clone()));
assert_eq!(machine.step(), Err(error));
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
unchanged(machine.memory(), &image);
}

View file

@ -86,6 +86,13 @@ fn vram_index(address: u32) -> usize {
} }
} }
/// Host setup rejects read-only destinations; CPU and DMA use bus semantics.
#[derive(Clone, Copy, PartialEq, Eq)]
enum WriteAccess {
Host,
Emulated,
}
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct CpuAccess { struct CpuAccess {
pc: u32, pc: u32,
@ -373,8 +380,10 @@ impl Memory {
let map_error = |error| DmaError::Memory { channel, error }; let map_error = |error| DmaError::Memory { channel, error };
// Validate before reading or committing channel data. Failed units are atomic. // Validate before reading or committing channel data. Failed units are atomic.
match transfer.width { match transfer.width {
AccessWidth::Halfword => self.write_index::<2>(transfer.destination), AccessWidth::Halfword => {
AccessWidth::Word => self.write_index::<4>(transfer.destination), self.write_index::<2>(transfer.destination, WriteAccess::Emulated)
}
AccessWidth::Word => self.write_index::<4>(transfer.destination, WriteAccess::Emulated),
AccessWidth::Byte => unreachable!("DMA is never byte-wide"), AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
} }
.map_err(map_error)?; .map_err(map_error)?;
@ -419,8 +428,16 @@ impl Memory {
}; };
self.advance_timer_step(timing.total()); self.advance_timer_step(timing.total());
match transfer.width { match transfer.width {
AccessWidth::Halfword => self.write16(transfer.destination, value as u16), AccessWidth::Halfword => self.write_aligned(
AccessWidth::Word => self.write32(transfer.destination, value), transfer.destination,
(value as u16).to_le_bytes(),
WriteAccess::Emulated,
),
AccessWidth::Word => self.write_aligned(
transfer.destination,
value.to_le_bytes(),
WriteAccess::Emulated,
),
AccessWidth::Byte => unreachable!("DMA is never byte-wide"), AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
}?; }?;
Ok::<_, MemoryError>((data_latch, value)) Ok::<_, MemoryError>((data_latch, value))
@ -722,15 +739,23 @@ impl Memory {
} }
pub fn write8(&mut self, address: u32, value: u8) -> Result<(), MemoryError> { pub fn write8(&mut self, address: u32, value: u8) -> Result<(), MemoryError> {
self.write_aligned(address, [value]) self.write_aligned(address, [value], self.write_access())
} }
pub fn write16(&mut self, address: u32, value: u16) -> Result<(), MemoryError> { pub fn write16(&mut self, address: u32, value: u16) -> Result<(), MemoryError> {
self.write_aligned(address, value.to_le_bytes()) self.write_aligned(address, value.to_le_bytes(), self.write_access())
} }
pub fn write32(&mut self, address: u32, value: u32) -> Result<(), MemoryError> { pub fn write32(&mut self, address: u32, value: u32) -> Result<(), MemoryError> {
self.write_aligned(address, value.to_le_bytes()) self.write_aligned(address, value.to_le_bytes(), self.write_access())
}
fn write_access(&self) -> WriteAccess {
if self.cpu_access.is_some() {
WriteAccess::Emulated
} else {
WriteAccess::Host
}
} }
// Validate the complete access before any RAM or I/O side effects. // Validate the complete access before any RAM or I/O side effects.
@ -739,8 +764,9 @@ impl Memory {
&mut self, &mut self,
address: u32, address: u32,
bytes: [u8; N], bytes: [u8; N],
access: WriteAccess,
) -> Result<(), MemoryError> { ) -> Result<(), MemoryError> {
let index = self.write_index::<N>(address)?; let index = self.write_index::<N>(address, access)?;
Self::validate_io_values(&mut self.audio_state(), address, &bytes)?; Self::validate_io_values(&mut self.audio_state(), address, &bytes)?;
self.record_access( self.record_access(
address, address,
@ -751,6 +777,11 @@ impl Memory {
_ => unreachable!("unsupported bus width"), _ => unreachable!("unsupported bus width"),
}, },
); );
if address < BIOS_SIZE as u32 {
// Only validated CPU/DMA writes reach here. Pay the bus cost but
// never modify the mapped ROM image or drive BIOS read history.
return Ok(());
}
if address >> 24 == 0x03 { if address >> 24 == 0x03 {
let (width, value) = match N { let (width, value) = match N {
1 => (AccessWidth::Byte, u32::from(bytes[0])), 1 => (AccessWidth::Byte, u32::from(bytes[0])),
@ -840,14 +871,22 @@ impl Memory {
Ok(()) Ok(())
} }
fn write_index<const N: usize>(&self, address: u32) -> Result<usize, MemoryError> { fn write_index<const N: usize>(
&self,
address: u32,
access: WriteAccess,
) -> Result<usize, MemoryError> {
if address & (N as u32 - 1) != 0 { if address & (N as u32 - 1) != 0 {
return Err(MemoryError::Unaligned(address)); return Err(MemoryError::Unaligned(address));
} }
match address >> 24 { match address >> 24 {
0x00 if self.bios.is_some() && address < BIOS_SIZE as u32 => { 0x00 if self.bios.is_some() && address < BIOS_SIZE as u32 => {
if access == WriteAccess::Emulated {
Ok(0) // Mapped BIOS ignores emulated writes; host setup stays strict.
} else {
Err(MemoryError::ReadOnly(address)) Err(MemoryError::ReadOnly(address))
} }
}
0x02 => Ok((address & 0x3ffff) as usize), 0x02 => Ok((address & 0x3ffff) as usize),
0x03 => Ok((address & 0x7fff) as usize), 0x03 => Ok((address & 0x7fff) as usize),
0x05 => Ok((address & 0x3ff) as usize), 0x05 => Ok((address & 0x3ff) as usize),
@ -897,7 +936,7 @@ impl Memory {
pub(crate) fn write_words(&mut self, writes: &[(u32, u32)]) -> Result<(), MemoryError> { pub(crate) fn write_words(&mut self, writes: &[(u32, u32)]) -> Result<(), MemoryError> {
let mut audio = self.audio_state(); let mut audio = self.audio_state();
for &(address, value) in writes { for &(address, value) in writes {
self.write_index::<4>(address)?; self.write_index::<4>(address, self.write_access())?;
Self::validate_io_values(&mut audio, address, &value.to_le_bytes())?; Self::validate_io_values(&mut audio, address, &value.to_le_bytes())?;
} }
for &(address, value) in writes { for &(address, value) in writes {

View file

@ -22,6 +22,8 @@ mod arithmetic;
mod bit_unpack; mod bit_unpack;
#[path = "bios/boot_video.rs"] #[path = "bios/boot_video.rs"]
mod boot_video; mod boot_video;
#[path = "bios/copy_bus.rs"]
mod copy_bus;
#[path = "bios/differential.rs"] #[path = "bios/differential.rs"]
mod differential; mod differential;
#[path = "bios/huffman.rs"] #[path = "bios/huffman.rs"]

View file

@ -0,0 +1,51 @@
//! Copy services execute real bus accesses, including ignored BIOS destinations.
use super::*;
#[test]
fn copy_and_fill_to_bios_return_without_modifying_firmware() {
for thumb in [false, true] {
for (service, width) in [(0x0b, 0), (0x0b, 1 << 26), (0x0c, 0)] {
for fill in [0, 1 << 24] {
let (mut machine, return_pc) = call(service, thumb, [SOURCE, 0, 28 | width | fill]);
for index in 0..32 {
machine
.memory_mut()
.write32(SOURCE + index * 4, 0x1234_abcd ^ index)
.unwrap();
}
let before = machine.cpu().clone();
let cycles = machine.cycles();
finish(&mut machine, return_pc);
assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(machine.cpu().cpsr(), before.cpsr());
assert!(machine.cycles() > cycles);
for (address, expected) in bios::image().into_iter().enumerate() {
assert_eq!(machine.memory().read8(address as u32).unwrap(), expected);
}
assert_eq!(
machine.memory_mut().write16(0, 0),
Err(MemoryError::ReadOnly(0))
);
}
}
}
}
#[test]
fn ignored_bios_destination_does_not_bypass_invalid_source_reads() {
for service in [0x0b, 0x0c] {
let (mut machine, _) = call(service, true, [0x0e00_0000, 0, 1]);
let error = (0..200)
.find_map(|_| machine.step().err())
.expect("source read must still fail");
assert_eq!(
error,
MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped(0x0e00_0000)))
);
let cpu = machine.cpu().clone();
let cycles = machine.cycles();
assert_eq!(machine.step(), Err(error));
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.cycles(), cycles);
}
}

View file

@ -436,14 +436,14 @@ fn dma_reset_disables_channels_and_clears_write_only_address_and_count_latches()
); );
m.memory_mut().write16(base + 10, 0).unwrap(); m.memory_mut().write16(base + 10, 0).unwrap();
m.memory_mut().write32(base, SOURCE).unwrap(); m.memory_mut().write32(base, SOURCE).unwrap();
m.memory_mut().write32(SOURCE, MARKER).unwrap();
m.memory_mut().write32(DEST, !MARKER).unwrap();
let vector = m.memory().read32(0).unwrap();
m.memory_mut().write16(base + 10, 0x8000).unwrap(); m.memory_mut().write16(base + 10, 0x8000).unwrap();
assert_eq!( assert_eq!(m.step().unwrap(), StepKind::Dma { channel });
m.step(), // The reset destination is BIOS: discard the transfer, not into stale DEST.
Err(MachineError::Dma(DmaError::Memory { assert_eq!(m.memory().read32(0).unwrap(), vector);
channel, assert_eq!(m.memory().read32(DEST).unwrap(), !MARKER);
error: MemoryError::ReadOnly(0)
}))
);
m.memory_mut().write16(base + 10, 0).unwrap(); m.memory_mut().write16(base + 10, 0).unwrap();
m.memory_mut().write32(base + 4, DEST).unwrap(); m.memory_mut().write32(base + 4, DEST).unwrap();
m.memory_mut().write32(SOURCE, MARKER).unwrap(); m.memory_mut().write32(SOURCE, MARKER).unwrap();

View file

@ -33,6 +33,7 @@ The repository is a Cargo workspace with three crates:
| `crates/gba-core/src/cpu/exception.rs` | Reset state, exception entry, and interrupt sampling | | `crates/gba-core/src/cpu/exception.rs` | Reset state, exception entry, and interrupt sampling |
| `crates/gba-core/src/cpu/load_alias_tests.rs` | Original single-load alias regressions for indexing, banks, widths, alignment, I/O, timing, and diagnostics | | `crates/gba-core/src/cpu/load_alias_tests.rs` | Original single-load alias regressions for indexing, banks, widths, alignment, I/O, timing, and diagnostics |
| `crates/gba-core/src/cpu/bios_access_tests.rs` | Original CPU BIOS protection regressions for retained history, modes, widths, exception returns, and diagnostics | | `crates/gba-core/src/cpu/bios_access_tests.rs` | Original CPU BIOS protection regressions for retained history, modes, widths, exception returns, and diagnostics |
| `crates/gba-core/src/cpu/bios_write_tests.rs` | Ignored CPU/DMA BIOS writes, immutable images, swaps, timing, completion, and strict unsupported boundaries |
| `crates/gba-core/src/cpu/thumb_bios_access_tests.rs` | Thumb BIOS aligned-word snapshots, transitions, load lanes, boundaries, diagnostics, and nominal timing | | `crates/gba-core/src/cpu/thumb_bios_access_tests.rs` | Thumb BIOS aligned-word snapshots, transitions, load lanes, boundaries, diagnostics, and nominal timing |
| `crates/gba-core/src/cpu/thumb_open_bus_tests.rs` | Region-dependent Thumb open bus, mirrors, widths, transfers, source boundaries, isolation, and nominal timing | | `crates/gba-core/src/cpu/thumb_open_bus_tests.rs` | Region-dependent Thumb open bus, mirrors, widths, transfers, source boundaries, isolation, and nominal timing |
| `crates/gba-core/src/cpu/iwram_open_bus_tests.rs` | Persistent IWRAM lanes, cold fills, access widths, isolation, errors, refills, IRQ preservation, and nominal timing | | `crates/gba-core/src/cpu/iwram_open_bus_tests.rs` | Persistent IWRAM lanes, cold fills, access widths, isolation, errors, refills, IRQ preservation, and nominal timing |
@ -96,6 +97,7 @@ The repository is a Cargo workspace with three crates:
| `crates/gba-demos/src/lib.rs` | Demo modules, original instruction/exception bytes, and raw input-test ROM | | `crates/gba-demos/src/lib.rs` | Demo modules, original instruction/exception bytes, and raw input-test ROM |
| `crates/gba-core/src/bios.rs` | Original ARM BIOS image builder, minimal boot, IRQ dispatch, waits, and memory services | | `crates/gba-core/src/bios.rs` | Original ARM BIOS image builder, minimal boot, IRQ dispatch, waits, and memory services |
| `crates/gba-core/tests/bios/boot_video.rs` | Firmware identity scales, bitmap/affine startup rendering, capture, and raw-memory isolation | | `crates/gba-core/tests/bios/boot_video.rs` | Firmware identity scales, bitmap/affine startup rendering, capture, and raw-memory isolation |
| `crates/gba-core/tests/bios/copy_bus.rs` | Original copy/fill services with ignored BIOS destinations and retained source diagnostics |
| `crates/gba-core/src/bios/reset.rs` | Emitted ARM SoftReset and selective RegisterRamReset; CPU banks, RAM clearing, and supported device-register resets | | `crates/gba-core/src/bios/reset.rs` | Emitted ARM SoftReset and selective RegisterRamReset; CPU banks, RAM clearing, and supported device-register resets |
| `crates/gba-core/src/bios/arithmetic.rs` | Emitted ARM division and integer-square-root routines | | `crates/gba-core/src/bios/arithmetic.rs` | Emitted ARM division and integer-square-root routines |
| `crates/gba-core/src/bios/affine.rs` | Emitted ARM background/sprite matrix services, range checks, and generated sine table | | `crates/gba-core/src/bios/affine.rs` | Emitted ARM background/sprite matrix services, range checks, and generated sine table |

View file

@ -134,7 +134,8 @@ See the [root-cause research](../research/bios-readback.md) for evidence and the
Host/debug reads and ROM-suite memory assertions still inspect raw mapped bytes and do not initialize or change the history. Host/debug reads and ROM-suite memory assertions still inspect raw mapped bytes and do not initialize or change the history.
Instruction fetches remain strict mapped reads outside the CPU data-access context. Instruction fetches remain strict mapped reads outside the CPU data-access context.
BIOS writes remain read-only diagnostics. DMA BIOS sources never read image bytes. CPU and DMA writes to the mapped BIOS are ignored, as described below. Host writes remain read-only diagnostics.
DMA BIOS sources never read image bytes.
They reuse known [channel data](dma.md#retained-channel-data), or report an unsupported-source diagnostic when channel data is unknown. They reuse known [channel data](dma.md#retained-channel-data), or report an unsupported-source diagnostic when channel data is unknown.
Missing BIOS images remain unmapped. The unused-memory ARM open-bus snapshot is separate from this retained BIOS value. Missing BIOS images remain unmapped. The unused-memory ARM open-bus snapshot is separate from this retained BIOS value.
@ -148,6 +149,38 @@ Original regressions cover synthetic images, boot, exception returns, access wid
The [public BIOS ROM](../public-bios-tests.md) passes its unchanged protected-read assertions in debug and release builds. The [public BIOS ROM](../public-bios-tests.md) passes its unchanged protected-read assertions in debug and release builds.
This result covers these firmware boundaries, not full BIOS service compatibility. This result covers these firmware boundaries, not full BIOS service compatibility.
## Writes to the mapped BIOS
CPU stores to `0x00000000..0x00003fff` complete without changing the supplied BIOS image.
This applies to ARM and Thumb stores, all processor modes, block stores, and the write phase of swaps.
The rule depends on the destination, not the executing address or a particular BIOS service.
A swap must still complete its protected read; unknown read history remains diagnostic.
Ignored writes retain nominal data-access costs, instruction writeback, and normal machine clock advancement.
The write value does not replace BIOS read history. Instructions executed inside BIOS still update history through their ordinary fetches.
DMA writes follow the same discard rule, with source reads, channel data, address progression, completion IRQs, and nominal timing intact.
Host/debug `Memory::write*` calls remain strict and report `ReadOnly` for the mapped BIOS.
The bounded rule requires a supplied BIOS image and stops at `0x00003fff`.
Missing BIOS images, unmapped destinations beyond that boundary, and cartridge writes retain their existing diagnostics.
Whole-instruction validation still rejects a block store with a later unsupported destination before committing CPU state.
DMA retains earlier completed units when a later unit fails.
This is not a general model of ignored writes across unused address ranges.
`CpuSet` and `CpuFastSet` need no service-specific change: their original ARM stores obey these bus rules.
Tests use original programs and images to check immutable firmware, widths, lanes, modes, swaps, clocks, DMA, and failed-step retry.
The local Emerald caller supplies destination zero itself; the copy routine does not corrupt that argument.
See [the startup investigation](../research/emerald-reset.md).
### Write-behavior references
- [GBATEK memory map](https://mgba-emu.github.io/gbatek/): the 16 KiB BIOS system-ROM region and bus widths.
- [GBATEK memory-copy services](https://problemkaputt.de/gbatek-bios-memory-copy.htm): CpuSet/CpuFastSet arguments and executed copy operations.
- [mGBA memory implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/memory.c): unsupported BIOS store cases log and continue without writing the image.
- [ares BIOS implementation at 728105b2](https://github.com/ares-emulator/ares/blob/728105b28ed6f1429841d5b2f5a38794d01d5c39/ares/gba/system/bios.cpp): the BIOS write handler discards writes separately from protected reads.
These are documentation and emulator comparisons, not new physical-hardware measurements. No reference implementation was copied.
## Stop ## Stop
`Stop` (`SWI 0x03`) executes an original ARM byte store of `0x80` to HALTCNT. `Stop` (`SWI 0x03`) executes an original ARM byte store of `0x80` to HALTCNT.

View file

@ -146,7 +146,8 @@ Limits remain explicit:
- Unused/write-only I/O reads, general DMA-to-CPU bus handoff, and disabled-RAM reads are not modeled here. - Unused/write-only I/O reads, general DMA-to-CPU bus handoff, and disabled-RAM reads are not modeled here.
[DMA channel data](dma.md#retained-channel-data) is separate and never overrides a CPU snapshot. [DMA channel data](dma.md#retained-channel-data) is separate and never overrides a CPU snapshot.
- Missing BIOS, truncated ROM, unsupported I/O, and save-memory accesses retain their existing diagnostics. - Missing BIOS, truncated ROM, unsupported I/O, and save-memory accesses retain their existing diagnostics.
- Unused-memory writes remain diagnostics rather than ignored hardware writes. Swaps cannot silently discard their write. - Unused-memory writes remain diagnostics rather than ignored hardware writes. Swaps to those ranges still fail.
- Writes to the mapped BIOS are a separate [ignored-write subset](bios.md#writes-to-the-mapped-bios), including swap write phases.
- ARM/Thumb instruction buffering is persistent, but these data snapshots remain a separate bounded model. - ARM/Thumb instruction buffering is persistent, but these data snapshots remain a separate bounded model.
Thumb IWRAM bus-history samples do not share storage with retained instructions. Thumb IWRAM bus-history samples do not share storage with retained instructions.
DMA updates IWRAM lanes at instruction boundaries, not during CPU accesses. DMA updates IWRAM lanes at instruction boundaries, not during CPU accesses.

View file

@ -64,6 +64,9 @@ Timer-driven requests, repeat, and completion IRQs are described in [Direct Soun
Other special-mode pairings, video capture, Game Pak DRQ, and prohibited source mode 3 return `DmaError::UnsupportedControl`. Other special-mode pairings, video capture, Game Pak DRQ, and prohibited source mode 3 return `DmaError::UnsupportedControl`.
DMA reads below work RAM reuse known channel data as described below. They never expose BIOS bytes. DMA reads below work RAM reuse known channel data as described below. They never expose BIOS bytes.
DMA writes to DMA registers return `DmaError::RegisterDestination`; self-modifying transfers are not supported. DMA writes to DMA registers return `DmaError::RegisterDestination`; self-modifying transfers are not supported.
Writes into the mapped 16 KiB BIOS [complete without changing its image](bios.md#writes-to-the-mapped-bios).
Source reads, channel data, address progression, timing, and completion IRQs still apply to these ignored writes.
DMA0–2 mask destination bit 27, so a programmed cartridge address can instead select the BIOS region.
Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules. Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules.
Other unmapped reads and cartridge writes remain diagnostics, not general DMA open-bus behavior. Other unmapped reads and cartridge writes remain diagnostics, not general DMA open-bus behavior.
A failed unit changes no memory, device state, or clock. Earlier units remain committed; the failed unit remains pending. A failed unit changes no memory, device state, or clock. Earlier units remain committed; the failed unit remains pending.

View file

@ -38,13 +38,23 @@ RCNT bit 15 was clear, so bit 8 did not enable general-purpose input/output (GPI
GBATEK documents bits 8 and 14 as writable but unused in normal mode. GBATEK documents bits 8 and 14 as writable but unused in normal mode.
The serial model now retains these inactive bits. Effective GPIO interrupt enable and Joybus selection still return explicit errors. The serial model now retains these inactive bits. Effective GPIO interrupt enable and Joybus selection still return explicit errors.
Both runs pass that write and now stop in the original BIOS replacement: That correction reached `read-only memory at 0x00000000` inside the original BIOS replacement, PC `0x00000378`.
A bounded local trace found a Thumb `CpuSet` call with source `0x02022a74`, destination zero, and count `0x1c` halfwords.
The caller supplied zero before entering the service. The failing original BIOS instruction was a halfword store.
The copy routine did not corrupt the destination. Its stores encountered an overly strict memory-bus diagnostic.
The [mapped-BIOS write correction](../hardware/bios.md#writes-to-the-mapped-bios) now discards CPU/DMA writes without modifying firmware.
The original copy service still executes all its reads and writes. No ROM patch or service-specific bypass was added.
Host writes remain strict. Reference-emulator comparisons and original tests support this bounded bus behavior.
The trace stayed local; no game code or assets were added to the repository.
Both runs pass the copy and now stop at an unsupported serial-transfer start:
```text ```text
Steps: 398019; instructions: 396289; IRQ entries: 2; DMA units: 1728; HALT idle: 0 Steps: 426406; instructions: 424636; IRQ entries: 2; DMA units: 1768; HALT idle: 0
Nominal cycles: 1334271 Nominal cycles: 1416670
PC=0x00000378 PC=0x082e6e5c
read-only memory at 0x00000000 unsupported serial transfer start: write 0x84 at 0x04000128
``` ```
The native run captures five startup frames. These do not establish a working title screen or gameplay. The native run captures five startup frames. These do not establish a working title screen or gameplay.
@ -59,10 +69,10 @@ Workspace debug/release tests, strict Clippy, public ARM/Thumb/memory/BIOS check
Native ROM-window tests and graphics smoke modes pass on Darwin arm64. Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
The pinned Pong debug/release reports match. The pinned Pong debug/release reports match.
The next investigation is the BIOS replacement's attempted write to address zero at PC `0x00000378`. The next investigation is the serial-transfer start at SIOCNT (`0x04000128`).
Trace the service arguments and original firmware routine before deciding whether the caller or service behavior is wrong. Check RCNT mode, clock selection, disconnected-input behavior, and completion/IRQ timing before implementing a transfer model.
The diagnostic alone does not establish the root cause. Do not make BIOS memory writable to bypass it. Do not invent a connected partner or immediate completion to suppress the diagnostic.
The corrected RCNT write does not establish a need for active link transfers or GPIO interrupts. The corrected inactive RCNT write does not establish a need for GPIO interrupts.
Noise now has deterministic counter clocks and shares tested length/envelope logic with the independent pulse channels. Noise now has deterministic counter clocks and shares tested length/envelope logic with the independent pulse channels.
Sweep applies only to channel 1. Direct Sound has FIFO clocks and nominal DMA requests. Sweep applies only to channel 1. Direct Sound has FIFO clocks and nominal DMA requests.

View file

@ -56,6 +56,7 @@ Update this file when a feature lands or a limit is removed.
- The unchanged original ARM cancellation probe matches both published read interval totals and its unadjusted timer samples. - The unchanged original ARM cancellation probe matches both published read interval totals and its unadjusted timer samples.
- Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing. - Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing.
- Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words. - Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words.
- CPU/DMA writes to the mapped BIOS complete without changing its image; host writes retain read-only diagnostics.
- Optional original BIOS replacement: minimal boot, SoftReset, selective RegisterRamReset, IRQ dispatch, interrupt waits, memory copy/fill, integer/fixed-point arithmetic, affine matrices, bit unpacking, LZ77/run-length/Huffman decompression, and differential filters. - Optional original BIOS replacement: minimal boot, SoftReset, selective RegisterRamReset, IRQ dispatch, interrupt waits, memory copy/fill, integer/fixed-point arithmetic, affine matrices, bit unpacking, LZ77/run-length/Huffman decompression, and differential filters.
- All 16 ARM data-processing operations, with immediate and shifted-register operands. - All 16 ARM data-processing operations, with immediate and shifted-register operands.
- Logical operations: `AND`, `EOR`, `TST`, `TEQ`, `ORR`, `MOV`, `BIC`, and `MVN`. - Logical operations: `AND`, `EOR`, `TST`, `TEQ`, `ORR`, `MOV`, `BIC`, and `MVN`.
@ -153,8 +154,9 @@ ROM windows have no demo-specific startup checks. Input is sampled between bound
Audio, saves, and external BIOS loading remain unavailable. ROM loading does not establish commercial-game compatibility. Audio, saves, and external BIOS loading remain unavailable. ROM loading does not establish commercial-game compatibility.
A local Emerald run now completes its all-device reset and master sound enable. A local Emerald run now completes its all-device reset and master sound enable.
It also passes the pulse/noise triggers and normal-mode RCNT configuration. It also passes the pulse/noise triggers and normal-mode RCNT configuration.
It next stops on an attempted write to address zero in the BIOS replacement at PC `0x00000378`. It also completes a CpuSet call to address zero through the corrected ignored BIOS-write behavior.
The cause remains under investigation. No title-screen, gameplay, or audible-game compatibility is established. It next stops at an unsupported serial-transfer start (`0x04000128`, PC `0x082e6e5c`).
No title-screen, gameplay, or audible-game compatibility is established.
See [the startup result](research/emerald-reset.md) for the bounded run and next investigation. See [the startup result](research/emerald-reset.md) for the bounded run and next investigation.
See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status. See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status.
The [ROM suite runner](rom-tests.md) has original regressions and a [pinned public ARM baseline](public-arm-tests.md). The [ROM suite runner](rom-tests.md) has original regressions and a [pinned public ARM baseline](public-arm-tests.md).

View file

@ -309,6 +309,10 @@ Status tests cover all modes, flag combinations, interrupt masks, field selectio
Exception tests cover vectors, ARM/Thumb return addresses, nested interrupts, mask priority, and return-state restoration. Exception tests cover vectors, ARM/Thumb return addresses, nested interrupts, mask priority, and return-state restoration.
User-bank transfers and failed returns are checked for unintended changes to active and hidden registers. User-bank transfers and failed returns are checked for unintended changes to active and hidden registers.
Integration tests execute original vector handlers and check optional BIOS mapping boundaries and write protection. Integration tests execute original vector handlers and check optional BIOS mapping boundaries and write protection.
Mapped-BIOS write tests distinguish ignored CPU/DMA transfers from strict host writes without changing the supplied image.
They cover ARM/Thumb widths, lanes, modes, block stores, swaps, timing, DMA completion, and failed boundary transfers.
Original CpuSet/CpuFastSet callers verify ignored BIOS destinations without bypassing source reads or the service instructions.
Desktop DMA-error fixtures use an unmapped destination because DMA0 masks cartridge destination addresses into the BIOS region.
Original BIOS-access regressions check retained ARM PC+8 and aligned Thumb PC+4 words for ARM/Thumb callers. Original BIOS-access regressions check retained ARM PC+8 and aligned Thumb PC+4 words for ARM/Thumb callers.
They cover all processor modes, load widths, lanes, rotation, both Thumb code alignments, and distinct halfwords. They cover all processor modes, load widths, lanes, rotation, both Thumb code alignments, and distinct halfwords.
They verify direct reads while executing inside BIOS, re-entry, SWI/IRQ returns, host-inspection isolation, and original-boot provenance. They verify direct reads while executing inside BIOS, re-entry, SWI/IRQ returns, host-inspection isolation, and original-boot provenance.