feat(memory): protect CPU BIOS reads with retained ARM prefetch

Retain the supplied image PC+8 snapshot after successful ARM BIOS instructions. CPU data reads from outside BIOS use its byte lanes; reads inside BIOS and host inspection remain mapped reads. Keep nominal timing and diagnostic atomicity. Unknown history, Thumb BIOS execution, and missing lookahead remain explicit limits rather than guessed values.

Add original regressions for ARM/Thumb callers, all CPU modes, widths and rotation, synthetic SWI/IRQ returns, original boot provenance, context isolation, history invalidation, and device progress. No Nintendo firmware or opcode constants are used to synthesize results.

Darwin arm64 validation: 861 workspace tests and 792 core/demo tests in debug and release; 39 Python tests; native ROM windows; all ten demo smoke tests; fmt, clippy, rustdoc, and docs links. Public ARM, Thumb, and memory ROMs still pass. The public BIOS ROM still fails firmware-specific test 1, now reading its actual replacement-BIOS prefetch rather than raw vector bytes.
This commit is contained in:
0xrsydn 2026-10-05 21:42:04 +07:00
commit c31a703db4
11 changed files with 399 additions and 16 deletions

View file

@ -13,6 +13,8 @@ pub use exception::Exception;
pub use status::Mode; pub use status::Mode;
mod transfer; mod transfer;
#[cfg(test)]
mod bios_access_tests;
#[cfg(test)] #[cfg(test)]
mod compare_psr_tests; mod compare_psr_tests;
#[cfg(test)] #[cfg(test)]
@ -206,7 +208,7 @@ impl Cpu {
// Always clear the access context, including on diagnostic errors. // Always clear the access context, including on diagnostic errors.
memory.begin_cpu_access(self.pc(), self.instruction_set); memory.begin_cpu_access(self.pc(), self.instruction_set);
let result = self.execute_instruction(instruction, memory); let result = self.execute_instruction(instruction, memory);
memory.end_cpu_access(); memory.end_cpu_access(result.is_ok());
result result
} }

View file

@ -0,0 +1,309 @@
//! Original synthetic BIOS images; no Nintendo firmware or expected firmware constants.
use super::*;
use crate::{
io::TIMER_BASE,
machine::{Machine, StepKind},
memory::{BIOS_SIZE, ROM_START},
timing::StepTiming,
};
const ENTRY: u32 = 0x100;
const DATA: u32 = 0xf233_80a5;
const OTHER: u32 = 0x1234_5678;
const LDR: u32 = 0xe590_1000; // LDR r1, [r0]
const BX_R4: u32 = 0xe12f_ff14;
fn word(bytes: &mut [u8], address: u32, value: u32) {
bytes[address as usize..address as usize + 4].copy_from_slice(&value.to_le_bytes());
}
fn image() -> Vec<u8> {
let mut bios = vec![0x55; BIOS_SIZE];
word(&mut bios, ENTRY, BX_R4);
word(&mut bios, ENTRY + 8, DATA);
word(&mut bios, 0x200, BX_R4);
word(&mut bios, 0x208, OTHER);
bios
}
fn bus(instruction: u32, bios: Vec<u8>) -> Memory {
let rom = [instruction, LDR, OTHER, OTHER];
Memory::with_bios(rom.into_iter().flat_map(u32::to_le_bytes).collect(), bios).unwrap()
}
fn leave_bios(memory: &mut Memory, thumb: bool) -> Cpu {
let mut cpu = Cpu::new(ENTRY);
cpu.registers[4] = ROM_START | u32::from(thumb);
cpu.step(memory).unwrap();
assert_eq!(cpu.pc(), ROM_START);
cpu
}
#[test]
fn protected_bios_loads_use_latched_lanes_for_arm_and_thumb_callers() {
for (arm, thumb, values) in [
(
LDR,
0x6801,
[0xf233_80a5, 0xa5f2_3380, 0x80a5_f233, 0x3380_a5f2],
),
(0xe5d0_1000, 0x7801, [0xa5, 0x80, 0x33, 0xf2]),
(
0xe1d0_10b0,
0x8801,
[0x80a5, 0xa500_0080, 0xf233, 0x3300_00f2],
),
(
0xe1d0_10d0,
0x5681,
[0xffff_ffa5, 0xffff_ff80, 0x33, 0xffff_fff2],
),
(
0xe1d0_10f0,
0x5e81,
[0xffff_80a5, 0xffff_ff80, 0xffff_f233, 0xffff_fff2],
),
] {
for is_thumb in [false, true] {
for address in [0, ENTRY, 0x3ffc] {
for (low, value) in values.into_iter().enumerate() {
let mut memory = bus(if is_thumb { thumb } else { arm }, image());
let mut cpu = leave_bios(&mut memory, is_thumb);
cpu.registers[0] = address + low as u32;
cpu.flags.carry = true;
let mut expected = cpu.clone();
expected.registers[1] = value;
expected.registers[15] += if is_thumb { 2 } else { 4 };
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(
cpu, expected,
"arm={arm:#010x} thumb={is_thumb} address={address:#x} lane={low}"
);
assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1);
assert_eq!(memory.cycles(), 0);
}
}
}
}
}
#[test]
fn protection_depends_on_executing_address_not_processor_mode() {
for mode in [
Mode::User,
Mode::System,
Mode::Fiq,
Mode::Irq,
Mode::Supervisor,
Mode::Abort,
Mode::Undefined,
] {
for thumb in [false, true] {
let mut bios = image();
word(&mut bios, 0x200, if thumb { 0x6801 } else { LDR });
word(&mut bios, 0x300, OTHER);
let mut memory = bus(if thumb { 0x6801 } else { LDR }, bios);
let mut cpu = leave_bios(&mut memory, thumb);
cpu.apply_status(0xb000_00c0 | (u32::from(thumb) << 5) | mode as u32, mode);
cpu.registers[0] = 0x300;
cpu.step(&mut memory).unwrap(); // Outside BIOS: even privileged modes cannot read raw bytes.
assert_eq!(cpu.registers[1], DATA);
cpu.registers[15] = 0x200;
cpu.step(&mut memory).unwrap(); // Inside BIOS: both instruction sets can read raw data.
assert_eq!(cpu.registers[1], OTHER);
assert_eq!(
cpu.cpsr(),
0xb000_00c0 | (u32::from(thumb) << 5) | mode as u32
);
}
}
}
#[test]
fn host_inspection_does_not_change_the_latch_or_expose_cpu_protected_bytes() {
let mut memory = bus(LDR, image());
let mut cpu = leave_bios(&mut memory, false);
for address in [0, 0x200, 0x3ffc] {
assert_eq!(
memory.read32(address).unwrap(),
if address == 0x200 { BX_R4 } else { 0x5555_5555 }
);
}
assert_eq!(memory.read32(ENTRY + 8).unwrap(), DATA);
assert_eq!(memory.read32(0x208).unwrap(), OTHER);
assert_eq!(memory.write32(0, 0), Err(MemoryError::ReadOnly(0)));
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], DATA);
cpu.step(&mut memory).unwrap(); // More ROM execution must not replace the BIOS latch.
assert_eq!(cpu.registers[1], DATA);
}
#[test]
fn bios_reentry_and_exception_returns_refresh_the_image_derived_value() {
let mut memory = bus(LDR, image());
let mut cpu = leave_bios(&mut memory, false);
cpu.registers[15] = 0x200;
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], OTHER);
for swi in [false, true] {
let mut bios = image();
let vector = if swi { 8 } else { 0x18 };
word(
&mut bios,
vector,
if swi { 0xe1b0_f00e } else { 0xe25e_f004 },
); // MOVS/SUBS pc,lr
word(&mut bios, vector + 8, OTHER);
let mut memory = bus(if swi { 0xef00_0000 } else { LDR }, bios);
let mut cpu = leave_bios(&mut memory, false);
if swi {
cpu.step(&mut memory).unwrap();
} else {
cpu.enter_exception(Exception::Irq);
}
assert_eq!(cpu.pc(), vector);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.mode(), Mode::System);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], OTHER);
}
}
#[test]
fn unknown_bios_history_returns_a_diagnostic_not_raw_bytes_or_a_magic_value() {
for bios in [false, true] {
let mut memory = if bios {
bus(LDR, image())
} else {
Memory::new(LDR.to_le_bytes().to_vec()).unwrap()
};
if bios {
assert_eq!(memory.read32(0).unwrap(), 0x5555_5555);
}
let mut cpu = Cpu::new(ROM_START);
let before = cpu.clone();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(0)))
);
assert_eq!(cpu, before);
}
}
#[test]
fn diagnostic_steps_keep_previous_bios_history_but_skipped_instructions_refresh_it() {
for instruction in [0xe7f0_00f0, LDR] {
let mut bios = image();
word(&mut bios, 0x200, instruction);
let mut memory = bus(LDR, bios);
let mut cpu = leave_bios(&mut memory, false);
cpu.registers[15] = 0x200;
cpu.registers[0] = 0x0e00_0000;
let before = cpu.clone();
assert!(cpu.step_timed(&mut memory).is_err());
assert_eq!(cpu, before);
cpu.registers[15] = ROM_START;
cpu.registers[0] = 0;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], DATA);
}
let mut bios = image();
word(&mut bios, 0x200, 0x0590_1000); // LDREQ skipped with Z clear.
let mut memory = bus(LDR, bios);
let mut cpu = leave_bios(&mut memory, false);
cpu.registers[15] = 0x200;
cpu.step(&mut memory).unwrap();
cpu.registers[15] = ROM_START;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], OTHER);
}
#[test]
fn thumb_bios_execution_and_missing_lookahead_invalidate_unsupported_history() {
for (pc, thumb) in [(0x200, true), (0x3ff8, false), (0x3ffc, false)] {
let mut bios = image();
word(&mut bios, pc, if thumb { 0x4720 } else { BX_R4 }); // Thumb BX r4 or ARM BX r4
let mut memory = bus(LDR, bios);
let mut cpu = leave_bios(&mut memory, false);
cpu.registers[15] = pc;
cpu.instruction_set = if thumb {
InstructionSet::Thumb
} else {
InstructionSet::Arm
};
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START);
let before = cpu.clone();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(0)))
);
assert_eq!(cpu, before);
}
}
#[test]
fn bios_block_loads_share_the_latch_but_unused_memory_uses_rom_prefetch() {
let mut memory = bus(0xe8b0_000e, image()); // LDMIA r0!, {r1-r3}
let mut cpu = leave_bios(&mut memory, false);
cpu.step(&mut memory).unwrap();
assert_eq!(&cpu.registers[1..4], &[DATA; 3]);
assert_eq!(cpu.registers[0], 12);
let mut memory = bus(LDR, image());
let mut cpu = leave_bios(&mut memory, false);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], DATA);
cpu.registers[0] = 0x4000;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], OTHER);
}
#[test]
fn original_boot_protected_read_matches_its_actual_last_bios_snapshot() {
let rom = [0xe3a0_0000, LDR]; // MOV r0,#0; LDR r1,[r0]
let mut machine =
crate::bios::boot(rom.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
let bios = crate::bios::image();
let mut last_bios_pc = None;
for _ in 0..128 {
if machine.cpu().pc() == ROM_START {
break;
}
let pc = machine.cpu().pc();
assert!(pc < BIOS_SIZE as u32);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
last_bios_pc = Some(pc);
}
assert_eq!(machine.cpu().pc(), ROM_START);
let offset = last_bios_pc.unwrap() as usize + 8;
let expected = u32::from_le_bytes(bios[offset..offset + 4].try_into().unwrap());
assert_ne!(expected, machine.memory().read32(0).unwrap());
machine.step().unwrap();
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[1], expected);
}
#[test]
fn protected_reads_charge_one_data_access_before_device_progress() {
let mut memory = bus(LDR, image());
let cpu = leave_bios(&mut memory, false);
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()[1], DATA);
assert_eq!(
machine.last_timing(),
StepTiming {
code_cycles: 8,
data_cycles: 1,
internal_cycles: 1,
idle_cycles: 0
}
);
assert_eq!(machine.cycles(), 10);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 10);
}

View file

@ -61,7 +61,8 @@ struct CpuAccess {
/// Work/video/palette RAM, OAM, cartridge ROM, optional BIOS, and supported I/O. /// Work/video/palette RAM, OAM, cartridge ROM, optional BIOS, and supported I/O.
/// ARM data reads from unused memory return a PC+8 snapshot when available. /// ARM data reads from unused memory return a PC+8 snapshot when available.
/// BIOS protection, Thumb open bus, and DMA bus latches are not modeled. /// CPU data reads of BIOS from outside BIOS use its retained ARM PC+8 snapshot.
/// Thumb BIOS prefetch, general Thumb open bus, and DMA bus latches are not modeled.
pub struct Memory { pub struct Memory {
external_ram: Vec<u8>, external_ram: Vec<u8>,
internal_ram: Vec<u8>, internal_ram: Vec<u8>,
@ -74,6 +75,7 @@ pub struct Memory {
cycles: u64, cycles: u64,
data_timing: Cell<Option<DataTiming>>, data_timing: Cell<Option<DataTiming>>,
cpu_access: Option<CpuAccess>, cpu_access: Option<CpuAccess>,
bios_prefetch: Option<u32>,
scanline_capture: Option<video::capture::Capture>, scanline_capture: Option<video::capture::Capture>,
sprite_pipeline: pipeline::SpritePipeline, sprite_pipeline: pipeline::SpritePipeline,
} }
@ -95,6 +97,7 @@ impl Memory {
cycles: 0, cycles: 0,
data_timing: Cell::new(None), data_timing: Cell::new(None),
cpu_access: None, cpu_access: None,
bios_prefetch: None,
scanline_capture: None, scanline_capture: None,
sprite_pipeline: pipeline::SpritePipeline::default(), sprite_pipeline: pipeline::SpritePipeline::default(),
}) })
@ -346,8 +349,14 @@ impl Memory {
Some(u32::from_le_bytes(bytes)) Some(u32::from_le_bytes(bytes))
} }
pub(crate) fn end_cpu_access(&mut self) { pub(crate) fn end_cpu_access(&mut self, succeeded: bool) {
self.cpu_access = None; if let Some(access) = self.cpu_access.take() {
if succeeded && access.pc < BIOS_SIZE as u32 {
// Commit only successful instruction snapshots under our diagnostic policy.
// Thumb BIOS execution or missing lookahead invalidates the known value.
self.bios_prefetch = access.arm_prefetch;
}
}
} }
fn can_write_power_control(&self) -> bool { fn can_write_power_control(&self) -> bool {
@ -415,6 +424,18 @@ impl Memory {
// Multi-byte reads use this helper to avoid charging each byte as a bus access. // Multi-byte reads use this helper to avoid charging each byte as a bus access.
fn read_byte(&self, address: u32) -> Result<u8, MemoryError> { fn read_byte(&self, address: u32) -> Result<u8, MemoryError> {
if address < BIOS_SIZE as u32
&& self.bios.is_some()
&& self
.cpu_access
.is_some_and(|access| access.pc >= BIOS_SIZE as u32)
{
// Protect CPU data reads, not host inspection or BIOS instruction fetches.
return self
.bios_prefetch
.map(|word| (word >> ((address & 3) * 8)) as u8)
.ok_or(MemoryError::Unmapped(address));
}
if matches!(address, 0x0000_4000..=0x01ff_ffff | 0x1000_0000..=0xffff_ffff) { if matches!(address, 0x0000_4000..=0x01ff_ffff | 0x1000_0000..=0xffff_ffff) {
if let Some(word) = self.cpu_access.and_then(|access| access.arm_prefetch) { if let Some(word) = self.cpu_access.and_then(|access| access.arm_prefetch) {
return Ok((word >> ((address & 3) * 8)) as u8); return Ok((word >> ((address & 3) * 8)) as u8);

View file

@ -27,6 +27,7 @@ The repository is a Cargo workspace with three crates:
| `crates/gba-core/src/cpu/status.rs` | Processor modes, register banks, and status transfers | | `crates/gba-core/src/cpu/status.rs` | Processor modes, register banks, and status transfers |
| `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/compare_psr_tests.rs` | Original Rd=15 test/compare status restoration, flags, bank, and timing regressions | | `crates/gba-core/src/cpu/compare_psr_tests.rs` | Original Rd=15 test/compare status restoration, flags, bank, and timing regressions |
| `crates/gba-core/src/cpu/status_tests.rs`, `crates/gba-core/src/cpu/exception_tests.rs` | Status, banking, exception, and return tests | | `crates/gba-core/src/cpu/status_tests.rs`, `crates/gba-core/src/cpu/exception_tests.rs` | Status, banking, exception, and return tests |
| `crates/gba-core/src/cpu/thumb_tests.rs` | Thumb instructions, state switching, and decoder checks | | `crates/gba-core/src/cpu/thumb_tests.rs` | Thumb instructions, state switching, and decoder checks |

View file

@ -90,6 +90,36 @@ Other invalid accesses use normal memory diagnostics. Earlier successful writes
These copy services keep CPU IRQ delivery masked until return; device clocks and DMA continue throughout. These copy services keep CPU IRQ delivery masked until return; device clocks and DMA continue throughout.
BIOS bus protection, exact firmware timing, undocumented side effects, and nested service calls remain incomplete. BIOS bus protection, exact firmware timing, undocumented side effects, and nested service calls remain incomplete.
## CPU BIOS read protection
CPU data reads at `0x00000000..0x00003fff` depend on the executing instruction's address, not its processor mode.
While executing inside BIOS, ARM and Thumb code can read the mapped image directly.
While executing outside BIOS, both instruction sets read byte lanes from retained BIOS prefetch data instead.
Normal word rotation, odd-halfword behavior, sign extension, and load timing still apply.
The current interpreter implements a bounded ARM prefetch model:
- Before an ARM BIOS instruction executes, sample its mapped PC+8 word without charging another data access.
- After successful execution, retain that snapshot for later protected reads. Skipped conditional instructions count as successful execution.
- Instructions outside BIOS do not replace the retained value. Re-entering ARM BIOS code refreshes it.
- Diagnostic failures preserve the previous value and clear the CPU access context.
- Successful Thumb BIOS execution or unavailable PC+8 bytes invalidate the known value. Their fetch history is not yet modeled.
- A protected read without known history returns `MemoryError::Unmapped` at the requested bus address, rather than exposing raw BIOS bytes.
The initial history is unknown. Direct cartridge startup with `Cpu::new` does not manufacture boot prefetch data.
Use BIOS execution, such as `bios::boot`, to establish supported history.
All retained words come from the supplied image. No Nintendo opcode constants are substituted.
The original replacement's boot and return paths therefore produce different values from Nintendo firmware.
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 retain their existing unsupported-source diagnostic.
Missing BIOS images remain unmapped. The unused-memory ARM open-bus snapshot is separate from this retained BIOS value.
This models neither a persistent fetch pipeline nor exact bus history across Thumb BIOS code and DMA.
Original regressions cover synthetic images, boot, exception returns, access widths, alignment, modes, and diagnostics.
The [public BIOS ROM](../public-bios-tests.md) still fails its firmware-specific test 1; this feature is not a public BIOS pass.
## 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

@ -98,7 +98,8 @@ Host inspection, ROM-suite memory assertions, instruction fetches, and DMA do no
Limits remain explicit: Limits remain explicit:
- Thumb open bus depends on instruction region, alignment, and prior bus data; it remains unsupported. - Thumb open bus depends on instruction region, alignment, and prior bus data; it remains unsupported.
- BIOS-protected reads, unused/write-only I/O reads, DMA latches, and disabled-RAM reads are not modeled here. - [BIOS-protected reads](bios.md#cpu-bios-read-protection) use separate retained ARM BIOS snapshots.
- Unused/write-only I/O reads, DMA latches, and disabled-RAM reads are not modeled here.
- 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 cannot silently discard their write.
- No persistent pipeline or bus history exists. Self-modifying code and DMA-to-CPU bus transitions are not hardware-accurate. - No persistent pipeline or bus history exists. Self-modifying code and DMA-to-CPU bus transitions are not hardware-accurate.

View file

@ -60,9 +60,18 @@ Before BIOS read protection:
- The emulator instead exposes the replacement BIOS reset-vector instruction at address zero. - The emulator instead exposes the replacement BIOS reset-vector instruction at address zero.
- The later post-SWI and IRQ checks are not reached. - The later post-SWI and IRQ checks are not reached.
This reveals a missing bus-protection rule and a separate firmware-specific expectation. After the bounded ARM BIOS read-protection implementation:
Implementing protection must not substitute Nintendo opcode constants merely to pass this ROM.
The original BIOS has a different layout, so its retained prefetch data can differ even with protection implemented. - Both builds still report test 1 as failed after 64 steps and 543 nominal cycles.
- The protected read now returns `0xe59f1320`, sampled from the original BIOS at its final boot instruction's PC+8.
- It no longer exposes address zero's reset-vector word, `0xea000006`.
- The test still expects `0xe129f000`. No assertion, checkpoint, ROM byte, or returned constant was changed to force a pass.
- Tests 2–4 remain unreached. Their SWI and IRQ cases have not passed independently.
The initial failure exposed both a missing bus-protection rule and a firmware-specific expectation.
The supported protection subset now derives data from the actual supplied image.
The original BIOS has a different layout, so its retained prefetch data differs from Nintendo firmware.
See [CPU BIOS read protection and its limits](hardware/bios.md#cpu-bios-read-protection).
No public BIOS pass is claimed. The ARM, Thumb, and memory suites remain separate passing results. No public BIOS pass is claimed. The ARM, Thumb, and memory suites remain separate passing results.
Exact fetch-pipeline behavior and full BIOS compatibility remain unverified. Exact fetch-pipeline behavior and full BIOS compatibility remain unverified.

View file

@ -50,9 +50,9 @@ Hardware references used for the core:
- [NanoBoyAdvance keypad implementation](https://github.com/nba-emu/NanoBoyAdvance/blob/master/src/nba/src/hw/keypad/keypad.cc), compared for control access widths; its AND/retrigger behavior differs from the chosen polling model. - [NanoBoyAdvance keypad implementation](https://github.com/nba-emu/NanoBoyAdvance/blob/master/src/nba/src/hw/keypad/keypad.cc), compared for control access widths; its AND/retrigger behavior differs from the chosen polling model.
- [mGBA keypad timing issue](https://github.com/mgba-emu/mgba/issues/2490), for frontend frame-based input timing limits. - [mGBA keypad timing issue](https://github.com/mgba-emu/mgba/issues/2490), for frontend frame-based input timing limits.
- [Tonc hardware interrupts](https://gbadev.net/tonc/interrupts.html), for keypad source enable and IF/IE bit 12. - [Tonc hardware interrupts](https://gbadev.net/tonc/interrupts.html), for keypad source enable and IF/IE bit 12.
- [GBATEK memory mirrors, video byte writes, and unused-memory reads](https://problemkaputt.de/gbatek-gba-unpredictable-things.htm), for unused address ranges, ARM PC+8 open bus, byte lanes, and the distinct Thumb region/history rules. - [GBATEK memory mirrors, video byte writes, and unused-memory reads](https://problemkaputt.de/gbatek-gba-unpredictable-things.htm), for unused address ranges, ARM PC+8 open bus, byte lanes, the distinct Thumb region/history rules, and PC-dependent BIOS read protection.
- [nocash GBA open-bus discussion](https://www.ngemu.com/threads/gba-open-bus.170809/), for the distinction between ARM prefetch and region-dependent Thumb values. - [nocash GBA open-bus discussion](https://www.ngemu.com/threads/gba-open-bus.170809/), for the distinction between ARM prefetch and region-dependent Thumb values.
- [mGBA memory implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/memory.c), reviewed for `GBALoadBad`, ARM prefetch sourcing, byte/halfword lane selection, and separate DMA/history behavior. - [mGBA memory implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/memory.c), reviewed for `GBALoadBad`, ARM prefetch sourcing, byte/halfword lane selection, and separate DMA/history behavior. Also reviewed its BIOS region-exit prefetch retention and protected-load lane selection.
- [GBATEK GBA memory map and bus widths](https://problemkaputt.de/gbatek-gba-memory-map.htm). - [GBATEK GBA memory map and bus widths](https://problemkaputt.de/gbatek-gba-memory-map.htm).
- [GBATEK GBA DMA transfers](https://problemkaputt.de/gbatek-gba-dma-transfers.htm). - [GBATEK GBA DMA transfers](https://problemkaputt.de/gbatek-gba-dma-transfers.htm).
- [mGBA DMA implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/dma.c), used to cross-check masks, repeat behavior, and Game Pak source increments. - [mGBA DMA implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/dma.c), used to cross-check masks, repeat behavior, and Game Pak source increments.

View file

@ -63,7 +63,8 @@ Rules:
- `instruction_set` is exactly `arm` or `thumb`. ARM completion addresses must be word-aligned; Thumb addresses must be halfword-aligned. - `instruction_set` is exactly `arm` or `thumb`. ARM completion addresses must be word-aligned; Thumb addresses must be halfword-aligned.
- Use the actual Thumb instruction address, without the address bit used by `BX`. - Use the actual Thumb instruction address, without the address bit used by `BX`.
- Register indices are integers from 0 through 15. Checks read the active register bank. - Register indices are integers from 0 through 15. Checks read the active register bank.
- `memory32` addresses must be word-aligned. Reads use the existing little-endian memory bus, including its mapping errors. - `memory32` addresses must be word-aligned. Reads use the host inspection path, including mapping errors.
BIOS assertions inspect raw mapped bytes, not the protected values seen by CPU code executing outside BIOS.
- `cpsr` compares the complete current program status register. No mask is applied. - `cpsr` compares the complete current program status register. No mask is applied.
The runner validates the whole manifest before opening or executing any ROM. The runner validates the whole manifest before opening or executing any ROM.

View file

@ -43,7 +43,7 @@ Update this file when a feature lands or a limit is removed.
- CPU/device stepping with nominal ARM7 instruction costs and memory wait states. - CPU/device stepping with nominal ARM7 instruction costs and memory wait states.
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows. - Game Pak wait-state control (`WAITCNT`) for all three ROM windows.
- 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 for vector code, without CPU BIOS read protection. - Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM BIOS PC+8 snapshots.
- 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`.
@ -94,7 +94,9 @@ The reserved ARM condition code `0xF` and Thumb condition code `0xE` return erro
ARMv5+ extensions such as `BLX`, `BKPT`, and Thumb-2 instructions are not supported. ARMv5+ extensions such as `BLX`, `BKPT`, and Thumb-2 instructions are not supported.
The ARMv4T high-register format requires at least one high register; low-to-low forms in that format return errors. The ARMv4T high-register format requires at least one high register; low-to-low forms in that format return errors.
`LDRT`/`STRT` single-transfer variants and invalid register combinations return errors. `LDRT`/`STRT` single-transfer variants and invalid register combinations return errors.
BIOS reads are not protected by the executing PC, and BIOS open-bus behavior is not modeled. CPU BIOS reads are protected by the executing address using a bounded ARM prefetch snapshot.
Thumb BIOS execution and missing lookahead invalidate that history; protected reads with unknown history remain diagnostics.
This is not a full fetch pipeline, and the original BIOS does not reproduce Nintendo's opcode-specific retained values.
Multiply flags N/Z are implemented. Unspecified multiply C/V outputs are preserved deterministically, not hardware-verified. Multiply flags N/Z are implemented. Unspecified multiply C/V outputs are preserved deterministically, not hardware-verified.
Instruction and memory errors are development diagnostics, not emulated CPU exceptions. Instruction and memory errors are development diagnostics, not emulated CPU exceptions.
Block transfers validate all accesses before committing register, RAM, or I/O changes. Block transfers validate all accesses before committing register, RAM, or I/O changes.
@ -125,7 +127,8 @@ Other public suites remain unverified. These results do not establish full CPU o
The runner requires known completion addresses and has no debug-port protocol, scripted input, or rendered-image assertions. The runner requires known completion addresses and has no debug-port protocol, scripted input, or rendered-image assertions.
ARM data reads from unused address ranges use a bounded [PC+8 open-bus snapshot](hardware/cpu.md#arm-unused-memory-data-reads). ARM data reads from unused address ranges use a bounded [PC+8 open-bus snapshot](hardware/cpu.md#arm-unused-memory-data-reads).
This is not a fetch pipeline. Thumb, DMA, BIOS-protected reads, and unused/write-only I/O open bus remain unmodeled. This is not a fetch pipeline. General Thumb, DMA, and unused/write-only I/O open bus remain unmodeled.
BIOS protection uses separate retained ARM BIOS history as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection).
Host inspection and instruction fetches remain strict. Other unmapped reads return errors. Host inspection and instruction fetches remain strict. Other unmapped reads return errors.
Reads beyond the supplied cartridge bytes also return errors. Reads beyond the supplied cartridge bytes also return errors.
Direct memory-bus halfword and word accesses require alignment. Direct memory-bus halfword and word accesses require alignment.
@ -134,8 +137,9 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
## Next steps ## Next steps
1. Add BIOS read protection based on supplied firmware instructions, not hard-coded values from the public BIOS test. 1. Extend BIOS prefetch history to Thumb execution and validate firmware-specific reads with a lawfully supplied BIOS.
Extend open-bus support with verified Thumb region/alignment rules and bus history, rather than a blanket unmapped-read fallback. The desktop runner still uses only the original BIOS. Do not force the public test to pass with guessed opcode constants.
Extend general open-bus support with verified Thumb region/alignment rules and bus history.
2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays. 2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays.
3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available. 3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available.
Validate STOP entry/wake edges and add external wake sources and remaining DMA device modes. Validate STOP entry/wake edges and add external wake sources and remaining DMA device modes.

View file

@ -219,6 +219,11 @@ 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.
Original BIOS-access regressions check retained ARM PC+8 data for ARM/Thumb callers, all processor modes, load widths, lanes, and rotation.
They verify direct reads while executing inside BIOS, re-entry, SWI/IRQ returns, host-inspection isolation, and original-boot provenance.
Unknown history, Thumb BIOS execution, missing lookahead, and diagnostic-step retention have explicit checks.
Protected loads keep normal nominal costs and advance devices only through machine stepping.
The pinned public BIOS ROM remains a failed assertion, not a conformance pass.
Display tests cover color conversion, framebuffer bounds, cursor movement, pause behavior, and animation wraparound. Display tests cover color conversion, framebuffer bounds, cursor movement, pause behavior, and animation wraparound.
Input regression tests check focused arrow keys, focused Space presses, and ignored input when unfocused. Input regression tests check focused arrow keys, focused Space presses, and ignored input when unfocused.
Command-line tests cover mode selection and invalid arguments. Command-line tests cover mode selection and invalid arguments.