From a0516de748a10468993a16dd9979f4c898b83230 Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Wed, 7 Oct 2026 03:31:33 +0700 Subject: [PATCH] fix(memory): ignore emulated writes to mapped BIOS --- crates/desktop/src/cartridge/suite/tests.rs | 3 +- crates/desktop/src/cartridge/tests.rs | 3 +- crates/desktop/src/cartridge/window/tests.rs | 3 +- crates/gba-core/src/cpu.rs | 2 + crates/gba-core/src/cpu/bios_write_tests.rs | 276 +++++++++++++++++++ crates/gba-core/src/memory.rs | 61 +++- crates/gba-core/tests/bios.rs | 2 + crates/gba-core/tests/bios/copy_bus.rs | 51 ++++ crates/gba-core/tests/bios/ram_reset.rs | 14 +- docs/architecture.md | 2 + docs/hardware/bios.md | 35 ++- docs/hardware/cpu.md | 3 +- docs/hardware/dma.md | 3 + docs/research/emerald-reset.md | 28 +- docs/status.md | 6 +- docs/testing.md | 4 + 16 files changed, 462 insertions(+), 34 deletions(-) create mode 100644 crates/gba-core/src/cpu/bios_write_tests.rs create mode 100644 crates/gba-core/tests/bios/copy_bus.rs diff --git a/crates/desktop/src/cartridge/suite/tests.rs b/crates/desktop/src/cartridge/suite/tests.rs index b521902..bda9a67 100644 --- a/crates/desktop/src/cartridge/suite/tests.rs +++ b/crates/desktop/src/cartridge/suite/tests.rs @@ -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); diff --git a/crates/desktop/src/cartridge/tests.rs b/crates/desktop/src/cartridge/tests.rs index ca7b807..5058059 100644 --- a/crates/desktop/src/cartridge/tests.rs +++ b/crates/desktop/src/cartridge/tests.rs @@ -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!( diff --git a/crates/desktop/src/cartridge/window/tests.rs b/crates/desktop/src/cartridge/window/tests.rs index c5d5b74..89c6823 100644 --- a/crates/desktop/src/cartridge/window/tests.rs +++ b/crates/desktop/src/cartridge/window/tests.rs @@ -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()); diff --git a/crates/gba-core/src/cpu.rs b/crates/gba-core/src/cpu.rs index bc164bc..66b1f65 100644 --- a/crates/gba-core/src/cpu.rs +++ b/crates/gba-core/src/cpu.rs @@ -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; diff --git a/crates/gba-core/src/cpu/bios_write_tests.rs b/crates/gba-core/src/cpu/bios_write_tests.rs new file mode 100644 index 0000000..c4ef0b9 --- /dev/null +++ b/crates/gba-core/src/cpu/bios_write_tests.rs @@ -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) { + 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); +} diff --git a/crates/gba-core/src/memory.rs b/crates/gba-core/src/memory.rs index 9b49552..6a2a4d7 100644 --- a/crates/gba-core/src/memory.rs +++ b/crates/gba-core/src/memory.rs @@ -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::(address)?; + let index = self.write_index::(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,13 +871,21 @@ impl Memory { Ok(()) } - fn write_index(&self, address: u32) -> Result { + fn write_index( + &self, + address: u32, + access: WriteAccess, + ) -> Result { 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 => { - Err(MemoryError::ReadOnly(address)) + 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), @@ -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 { diff --git a/crates/gba-core/tests/bios.rs b/crates/gba-core/tests/bios.rs index 7601817..43b5f9c 100644 --- a/crates/gba-core/tests/bios.rs +++ b/crates/gba-core/tests/bios.rs @@ -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"] diff --git a/crates/gba-core/tests/bios/copy_bus.rs b/crates/gba-core/tests/bios/copy_bus.rs new file mode 100644 index 0000000..abe8107 --- /dev/null +++ b/crates/gba-core/tests/bios/copy_bus.rs @@ -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); + } +} diff --git a/crates/gba-core/tests/bios/ram_reset.rs b/crates/gba-core/tests/bios/ram_reset.rs index bad093b..d4363a8 100644 --- a/crates/gba-core/tests/bios/ram_reset.rs +++ b/crates/gba-core/tests/bios/ram_reset.rs @@ -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(); diff --git a/docs/architecture.md b/docs/architecture.md index 9ac1394..39dcb0b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 | diff --git a/docs/hardware/bios.md b/docs/hardware/bios.md index 2c99e02..3d343ec 100644 --- a/docs/hardware/bios.md +++ b/docs/hardware/bios.md @@ -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. diff --git a/docs/hardware/cpu.md b/docs/hardware/cpu.md index 7bcf817..abbb65d 100644 --- a/docs/hardware/cpu.md +++ b/docs/hardware/cpu.md @@ -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. diff --git a/docs/hardware/dma.md b/docs/hardware/dma.md index b475e18..4950501 100644 --- a/docs/hardware/dma.md +++ b/docs/hardware/dma.md @@ -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. diff --git a/docs/research/emerald-reset.md b/docs/research/emerald-reset.md index 863f760..71e2430 100644 --- a/docs/research/emerald-reset.md +++ b/docs/research/emerald-reset.md @@ -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. diff --git a/docs/status.md b/docs/status.md index 9c4a52d..d78d003 100644 --- a/docs/status.md +++ b/docs/status.md @@ -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). diff --git a/docs/testing.md b/docs/testing.md index 3e86232..1aa35e8 100644 --- a/docs/testing.md +++ b/docs/testing.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.