fix(memory): ignore emulated writes to mapped BIOS
This commit is contained in:
parent
86f8663904
commit
a0516de748
16 changed files with 462 additions and 34 deletions
|
|
@ -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.instructions, 0);
|
||||
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();
|
||||
let (stats, outcome) = run_to_checkpoint(&mut machine, &config);
|
||||
assert_eq!(stats.steps, 0);
|
||||
|
|
|
|||
|
|
@ -322,7 +322,8 @@ fn dma_errors_return_a_diagnostic_without_consuming_a_step() {
|
|||
let mut machine = boot(&[0xeaff_fffe]);
|
||||
let bus = machine.memory_mut();
|
||||
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();
|
||||
let report = run_bounded(&mut machine, 100);
|
||||
assert!(matches!(
|
||||
|
|
|
|||
|
|
@ -266,7 +266,8 @@ fn dma_and_irq_consume_slice_budget_and_dma_failure_preserves_progress() {
|
|||
.unwrap();
|
||||
assert_eq!(session.stats.irq_entries, 1);
|
||||
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();
|
||||
let cycles = session.machine.cycles();
|
||||
assert!(session.update(Buttons::default()).is_err());
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ mod arm_pipeline_tests;
|
|||
#[cfg(test)]
|
||||
mod bios_access_tests;
|
||||
#[cfg(test)]
|
||||
mod bios_write_tests;
|
||||
#[cfg(test)]
|
||||
mod compare_psr_tests;
|
||||
#[cfg(test)]
|
||||
mod dma_resume_tests;
|
||||
|
|
|
|||
276
crates/gba-core/src/cpu/bios_write_tests.rs
Normal file
276
crates/gba-core/src/cpu/bios_write_tests.rs
Normal 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);
|
||||
}
|
||||
|
|
@ -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)]
|
||||
struct CpuAccess {
|
||||
pc: u32,
|
||||
|
|
@ -373,8 +380,10 @@ impl Memory {
|
|||
let map_error = |error| DmaError::Memory { channel, error };
|
||||
// Validate before reading or committing channel data. Failed units are atomic.
|
||||
match transfer.width {
|
||||
AccessWidth::Halfword => self.write_index::<2>(transfer.destination),
|
||||
AccessWidth::Word => self.write_index::<4>(transfer.destination),
|
||||
AccessWidth::Halfword => {
|
||||
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"),
|
||||
}
|
||||
.map_err(map_error)?;
|
||||
|
|
@ -419,8 +428,16 @@ impl Memory {
|
|||
};
|
||||
self.advance_timer_step(timing.total());
|
||||
match transfer.width {
|
||||
AccessWidth::Halfword => self.write16(transfer.destination, value as u16),
|
||||
AccessWidth::Word => self.write32(transfer.destination, value),
|
||||
AccessWidth::Halfword => self.write_aligned(
|
||||
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"),
|
||||
}?;
|
||||
Ok::<_, MemoryError>((data_latch, value))
|
||||
|
|
@ -722,15 +739,23 @@ impl Memory {
|
|||
}
|
||||
|
||||
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> {
|
||||
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> {
|
||||
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.
|
||||
|
|
@ -739,8 +764,9 @@ impl Memory {
|
|||
&mut self,
|
||||
address: u32,
|
||||
bytes: [u8; N],
|
||||
access: WriteAccess,
|
||||
) -> 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.record_access(
|
||||
address,
|
||||
|
|
@ -751,6 +777,11 @@ impl Memory {
|
|||
_ => 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 {
|
||||
let (width, value) = match N {
|
||||
1 => (AccessWidth::Byte, u32::from(bytes[0])),
|
||||
|
|
@ -840,14 +871,22 @@ impl Memory {
|
|||
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 {
|
||||
return Err(MemoryError::Unaligned(address));
|
||||
}
|
||||
match address >> 24 {
|
||||
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))
|
||||
}
|
||||
}
|
||||
0x02 => Ok((address & 0x3ffff) as usize),
|
||||
0x03 => Ok((address & 0x7fff) 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> {
|
||||
let mut audio = self.audio_state();
|
||||
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())?;
|
||||
}
|
||||
for &(address, value) in writes {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ mod arithmetic;
|
|||
mod bit_unpack;
|
||||
#[path = "bios/boot_video.rs"]
|
||||
mod boot_video;
|
||||
#[path = "bios/copy_bus.rs"]
|
||||
mod copy_bus;
|
||||
#[path = "bios/differential.rs"]
|
||||
mod differential;
|
||||
#[path = "bios/huffman.rs"]
|
||||
|
|
|
|||
51
crates/gba-core/tests/bios/copy_bus.rs
Normal file
51
crates/gba-core/tests/bios/copy_bus.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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().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();
|
||||
assert_eq!(
|
||||
m.step(),
|
||||
Err(MachineError::Dma(DmaError::Memory {
|
||||
channel,
|
||||
error: MemoryError::ReadOnly(0)
|
||||
}))
|
||||
);
|
||||
assert_eq!(m.step().unwrap(), StepKind::Dma { channel });
|
||||
// The reset destination is BIOS: discard the transfer, not into stale DEST.
|
||||
assert_eq!(m.memory().read32(0).unwrap(), vector);
|
||||
assert_eq!(m.memory().read32(DEST).unwrap(), !MARKER);
|
||||
m.memory_mut().write16(base + 10, 0).unwrap();
|
||||
m.memory_mut().write32(base + 4, DEST).unwrap();
|
||||
m.memory_mut().write32(SOURCE, MARKER).unwrap();
|
||||
|
|
|
|||
|
|
@ -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/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_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_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 |
|
||||
|
|
@ -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-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/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/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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
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` (`SWI 0x03`) executes an original ARM byte store of `0x80` to HALTCNT.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
[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.
|
||||
- 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.
|
||||
Thumb IWRAM bus-history samples do not share storage with retained instructions.
|
||||
DMA updates IWRAM lanes at instruction boundaries, not during CPU accesses.
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
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.
|
||||
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 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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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
|
||||
Steps: 398019; instructions: 396289; IRQ entries: 2; DMA units: 1728; HALT idle: 0
|
||||
Nominal cycles: 1334271
|
||||
PC=0x00000378
|
||||
read-only memory at 0x00000000
|
||||
Steps: 426406; instructions: 424636; IRQ entries: 2; DMA units: 1768; HALT idle: 0
|
||||
Nominal cycles: 1416670
|
||||
PC=0x082e6e5c
|
||||
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.
|
||||
|
|
@ -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.
|
||||
The pinned Pong debug/release reports match.
|
||||
|
||||
The next investigation is the BIOS replacement's attempted write to address zero at PC `0x00000378`.
|
||||
Trace the service arguments and original firmware routine before deciding whether the caller or service behavior is wrong.
|
||||
The diagnostic alone does not establish the root cause. Do not make BIOS memory writable to bypass it.
|
||||
The corrected RCNT write does not establish a need for active link transfers or GPIO interrupts.
|
||||
The next investigation is the serial-transfer start at SIOCNT (`0x04000128`).
|
||||
Check RCNT mode, clock selection, disconnected-input behavior, and completion/IRQ timing before implementing a transfer model.
|
||||
Do not invent a connected partner or immediate completion to suppress the diagnostic.
|
||||
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.
|
||||
Sweep applies only to channel 1. Direct Sound has FIFO clocks and nominal DMA requests.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
- 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.
|
||||
- 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.
|
||||
- All 16 ARM data-processing operations, with immediate and shifted-register operands.
|
||||
- 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.
|
||||
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 next stops on an attempted write to address zero in the BIOS replacement at PC `0x00000378`.
|
||||
The cause remains under investigation. No title-screen, gameplay, or audible-game compatibility is established.
|
||||
It also completes a CpuSet call to address zero through the corrected ignored BIOS-write behavior.
|
||||
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 [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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue