feat(cpu): use actual fetch regions for boundary bus snapshots

This commit is contained in:
0xrsydn 2026-10-06 15:13:40 +07:00
commit 0a696ea3e7
14 changed files with 465 additions and 28 deletions

View file

@ -38,6 +38,8 @@ mod dma_resume_tests;
#[cfg(test)]
mod exception_tests;
#[cfg(test)]
mod fetch_boundary_tests;
#[cfg(test)]
mod fetch_history_tests;
#[cfg(test)]
mod instruction_tests;

View file

@ -0,0 +1,303 @@
//! Original boundary programs. Expected data follows the newly fetched region,
//! not the region containing the executing instruction. Timing remains nominal.
use super::*;
use crate::{
dma::{DMA_BASE, DMA_STRIDE},
io::WAITCNT,
memory::{ROM_CAPACITY, ROM_START},
timing::{bus_cycles, AccessKind, AccessWidth},
};
const UNUSED: u32 = 0x8000_0000;
const DATA: u32 = 0x9abc_1234;
const SEED: u32 = 0x5678_abcd;
const NOP: u16 = 0x46c0;
const LDR: u16 = 0x6801; // LDR r1,[r0].
fn cpu(pc: u32) -> Cpu {
let mut cpu = Cpu::new(pc);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[0] = UNUSED;
cpu
}
fn boundary(boundary: u32, pc: u32, instruction: u16) -> Memory {
let mut memory = Memory::new(DATA.to_le_bytes().to_vec()).unwrap();
if boundary < ROM_START {
memory.write32(boundary, DATA).unwrap();
}
for address in [boundary - 6, boundary - 4, boundary - 2] {
memory
.write16(address, if address == pc { instruction } else { NOP })
.unwrap();
}
memory
}
#[test]
fn mapped_video_and_rom_crossings_use_fetch_region_lanes_for_all_load_widths() {
for boundary_address in [0x0600_0000, 0x0700_0000, 0x0800_0000] {
for distance in [4, 2] {
let pc = boundary_address - distance;
let word: u32 = if boundary_address == 0x0700_0000 {
DATA // OAM supplies a full aligned word at either Thumb alignment.
} else if distance == 4 {
0x1234_1234
} else {
0x9abc_9abc
};
for (instruction, width, signed) in [
(LDR, 4, false),
(0x7801, 1, false),
(0x8801, 2, false),
(0x5681, 1, true),
(0x5e81, 2, true), // Signed loads use r2=0 offset.
] {
for lane in 0..4 {
for warm in [false, true] {
let mut memory = boundary(boundary_address, pc, instruction);
let mut cpu = cpu(if warm { pc - 2 } else { pc });
cpu.registers[0] += lane;
cpu.flags = Flags {
negative: true,
zero: false,
carry: true,
overflow: true,
};
if warm {
cpu.step(&mut memory).unwrap();
}
let mut expected = cpu.clone();
let byte = (word >> (lane * 8)) as u8;
let half = (word >> ((lane & 2) * 8)) as u16;
expected.registers[1] = match (width, signed) {
(4, _) => word.rotate_right(lane * 8),
(1, false) => u32::from(byte),
(1, true) => byte as i8 as i32 as u32,
(2, false) => u32::from(half).rotate_right((lane & 1) * 8),
(2, true) if lane & 1 != 0 => byte as i8 as i32 as u32,
(2, true) => half as i16 as i32 as u32,
_ => unreachable!(),
};
expected.registers[15] += 2;
let timing = cpu.step_timed(&mut memory).unwrap();
assert_cpu_arch_eq!(
cpu,
expected,
"boundary={boundary_address:#x} pc={pc:#x}"
);
assert_eq!(timing.code_cycles, 1);
assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1);
assert_eq!(memory.cycles(), 0);
assert_eq!(memory.read32(UNUSED), Err(MemoryError::Unmapped(UNUSED)));
}
}
}
}
}
}
#[test]
fn entering_iwram_uses_actual_local_history_or_reports_unknown_lanes() {
let boundary_address = 0x0300_0000;
for distance in [4, 2] {
for warm in [false, true] {
for seed in [0, 1, 2] {
// Unknown, CPU data read, DMA source read.
let pc = boundary_address - distance;
let mut memory = boundary(boundary_address, pc, LDR);
let source = 0x0300_1000;
memory.write32(source, SEED).unwrap();
if seed == 1 {
memory.write32(0x0200_0100, 0xe592_3000).unwrap(); // ARM LDR r3,[r2].
let mut seeder = Cpu::new(0x0200_0100);
seeder.registers[2] = source;
seeder.step(&mut memory).unwrap();
} else if seed == 2 {
let dma = DMA_BASE + 3 * DMA_STRIDE;
memory.write32(dma, source).unwrap();
memory.write32(dma + 4, 0x0200_1000).unwrap();
memory.write32(dma + 8, 0x8400_0001).unwrap();
memory.step_dma().unwrap().unwrap();
}
let mut cpu = cpu(if warm { pc - 2 } else { pc });
if warm {
cpu.step(&mut memory).unwrap();
}
let before = cpu.clone();
let result = cpu.step(&mut memory);
if distance == 4 && seed == 0 {
// Only IWRAM's low half has actually been fetched.
assert_eq!(result, Err(CpuError::Memory(MemoryError::Unmapped(UNUSED))));
assert_eq!(cpu, before);
} else {
result.unwrap();
assert_eq!(
cpu.registers[1],
if distance == 4 { 0x5678_1234 } else { DATA }
);
}
}
}
}
}
#[test]
fn failed_boundary_load_discards_new_samples_and_retry_fetches_current_bytes() {
let pc = 0x02ff_fffc;
let mut memory = boundary(0x0300_0000, pc, LDR);
memory.write16(pc + 2, LDR).unwrap();
let mut cpu = cpu(pc);
let before = cpu.clone();
assert!(cpu.step_timed(&mut memory).is_err());
assert_eq!(cpu, before);
memory.write16(0x0300_0000, 0x5678).unwrap();
memory.write32(0x0200_1000, SEED).unwrap();
cpu.registers[0] = 0x0200_1000;
cpu.step(&mut memory).unwrap(); // Ordinary mapped load needs no complete bus snapshot.
assert_eq!(cpu.registers[1], SEED);
cpu.registers[0] = UNUSED;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0x9abc_5678);
}
#[test]
fn refill_straddling_ewram_and_iwram_keeps_the_captured_target_lane() {
let target = 0x02ff_fffe;
let mut memory = boundary(0x0300_0000, target, LDR);
memory.write32(0x0200_0100, 0xe12f_ff14).unwrap(); // ARM BX r4.
let mut cpu = Cpu::new(0x0200_0100);
cpu.registers[0] = UNUSED;
cpu.registers[4] = target | 1;
cpu.step(&mut memory).unwrap(); // Refill captures IWRAM's low lane (target+2).
memory.write32(0x0300_0000, 0xdef0_5678).unwrap();
cpu.step(&mut memory).unwrap(); // New fetch drives only the high lane.
assert_eq!(cpu.registers[1], 0xdef0_1234);
}
#[test]
fn rom_page_and_wait_window_crossings_use_mapped_halfwords_without_changing_nominal_timing() {
let mut rom = vec![0; ROM_CAPACITY];
for offset in [0, 0x0100_0000] {
rom[offset..offset + 4].copy_from_slice(&DATA.to_le_bytes());
}
for end in [0x0100_0000, ROM_CAPACITY] {
for distance in [4, 2] {
rom[end - distance..end - distance + 2].copy_from_slice(&LDR.to_le_bytes());
}
}
let mut memory = Memory::new(rom).unwrap();
for boundary in [
0x0900_0000,
0x0a00_0000,
0x0b00_0000,
0x0c00_0000,
0x0d00_0000,
] {
for distance in [4, 2] {
for waitcnt in [0, 0x7fff] {
memory.write16(WAITCNT, waitcnt).unwrap();
let pc = boundary - distance;
let mut cpu = cpu(pc);
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(
cpu.registers[1],
if distance == 4 {
0x1234_1234
} else {
0x9abc_9abc
}
);
assert_eq!(
timing.code_cycles,
bus_cycles(
memory.waitcnt(),
pc,
AccessWidth::Halfword,
AccessKind::Sequential
)
);
assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1);
}
}
}
}
#[test]
fn unsupported_io_and_missing_rom_lookahead_remain_diagnostic_but_branches_can_discard_them() {
for pc in [0x03ff_fffc, 0x03ff_fffe, 0x07ff_fffc, 0x07ff_fffe] {
let mut memory = Memory::new(vec![]).unwrap();
memory.write16(pc, LDR).unwrap();
let mut cpu = cpu(pc);
let before = cpu.clone();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
assert_eq!(cpu, before);
memory.write16(pc, 0x4720).unwrap(); // BX r4 discards the unsupported sequential path.
memory.write16(0x0200_0200, 0x2107).unwrap(); // MOV r1,#7.
cpu.registers[4] = 0x0200_0201;
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 7);
}
}
#[test]
fn a_rom_boundary_requires_only_the_fetched_halfword_not_a_full_word() {
for length in [1, 2] {
let mut memory = Memory::new(DATA.to_le_bytes()[..length].to_vec()).unwrap();
let pc = ROM_START - 4;
memory.write16(pc, LDR).unwrap();
let mut cpu = cpu(pc);
let before = cpu.clone();
let result = cpu.step(&mut memory);
if length == 1 {
assert_eq!(result, Err(CpuError::Memory(MemoryError::Unmapped(UNUSED))));
assert_eq!(cpu, before);
} else {
result.unwrap();
assert_eq!(cpu.registers[1], 0x1234_1234);
}
}
}
#[test]
fn block_and_stack_loads_share_one_boundary_snapshot_and_keep_writeback() {
for instruction in [0xcc06, 0xbc06] {
// LDM r4!,{r1,r2}; POP {r1,r2}.
let pc = 0x06ff_fffc;
let mut memory = boundary(0x0700_0000, pc, instruction);
let mut cpu = cpu(pc);
cpu.registers[4] = UNUSED;
cpu.registers[13] = UNUSED;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], DATA);
assert_eq!(cpu.registers[2], DATA);
assert_eq!(
cpu.registers[if instruction == 0xcc06 { 4 } else { 13 }],
UNUSED + 8
);
assert_eq!(cpu.pc(), pc + 2);
}
}
#[test]
fn arm_crossings_still_use_the_new_word_and_timed_and_untimed_paths_agree() {
for boundary in [0x0300_0000, 0x0600_0000, 0x0700_0000] {
let pc = boundary - 8;
let mut memory = Memory::new(vec![]).unwrap();
memory.write32(pc, 0xe590_1000).unwrap(); // LDR r1,[r0].
memory.write32(boundary, DATA).unwrap();
let mut cpu = Cpu::new(pc);
cpu.registers[0] = UNUSED;
let mut timed = cpu.clone();
cpu.step(&mut memory).unwrap();
timed.step_timed(&mut memory).unwrap();
assert_eq!(cpu, timed);
assert_eq!(cpu.registers[1], DATA);
}
}

View file

@ -319,7 +319,7 @@ fn failed_refill_instructions_preserve_cpu_and_preexisting_history() {
}
#[test]
fn cold_fill_establishes_lanes_but_region_crossings_and_unmapped_targets_stay_diagnostic() {
fn cold_fill_establishes_lanes_but_io_crossings_and_unmapped_targets_stay_diagnostic() {
let mut bus = Memory::new(vec![]).unwrap();
target(&mut bus, TARGET, PROBE);
let mut cpu = Cpu::new(TARGET);

View file

@ -294,18 +294,23 @@ fn short_rom_needs_only_the_fetched_halfword_and_errors_only_when_used() {
}
#[test]
fn cold_iwram_fills_establish_lanes_but_region_crossing_fetches_remain_diagnostics() {
fn cold_fills_and_mapped_crossings_succeed_only_with_known_fetch_lanes() {
for base in [0x0300_0100, 0x0300_8100, 0x02ff_fffc, 0x05ff_fffc] {
for offset in [0, 2] {
let pc = base + offset;
let mut bus = program(pc, LDR);
let mut cpu = cpu(pc);
if pc >> 24 == 3 {
let expected = match (pc >> 24, offset) {
(3, 0) => Some(0x46c0_80a5),
(3, 2) | (2, 2) => Some(DATA),
(5, 0) => Some(0x80a5_80a5),
(5, 2) => Some(0xf233_f233),
(2, 0) => None, // First IWRAM halfword cannot establish its unknown high lane.
_ => unreachable!(),
};
if let Some(expected) = expected {
cpu.step(&mut bus).unwrap();
assert_eq!(
cpu.registers[1],
if offset == 0 { 0x46c0_80a5 } else { DATA }
);
assert_eq!(cpu.registers[1], expected);
assert_eq!(bus.read32(UNUSED), Err(MemoryError::Unmapped(UNUSED)));
continue;
}

View file

@ -396,11 +396,12 @@ impl Memory {
}
}
self.drive_iwram_fetch(fetch);
let same_region = fetch.address >> 24 == pc >> 24;
// The newly accessed bus supplies the snapshot, even across a region boundary.
// Unknown local lanes and unavailable/unsupported fetch observations stay diagnostic.
let prefetch = match instruction_set {
InstructionSet::Arm => fetch.bus_word,
InstructionSet::Thumb if !same_region => None,
InstructionSet::Thumb if pc >> 24 == 3 => self.iwram_bus.snapshot(),
InstructionSet::Thumb if fetch.instruction.is_err() => None,
InstructionSet::Thumb if fetch.address >> 24 == 3 => self.iwram_bus.snapshot(),
InstructionSet::Thumb => fetch.bus_word,
};
self.cpu_access = Some(CpuAccess { pc, prefetch });

View file

@ -46,6 +46,7 @@ The repository is a Cargo workspace with three crates:
| `crates/gba-core/src/memory/fetch.rs` | Strict instruction samples with captured region-dependent bus observations |
| `crates/gba-core/src/memory/fetch_tests.rs` | Fetch widths, bus lanes, captured refill/BIOS values, strict errors, and side-effect isolation |
| `crates/gba-core/src/cpu/fetch_history_tests.rs` | Shared sample consumption, no execution-entry rereads, and failed-step retry |
| `crates/gba-core/src/cpu/fetch_boundary_tests.rs` | Actual fetch-region snapshots, split refills, IWRAM lane requirements, ROM-window boundaries, and nominal timing |
| `crates/gba-core/src/io.rs` | I/O registers, timers, HALT/STOP wake-up, next-event bounds, and IRQ latches |
| `crates/gba-core/src/display.rs` | Display clock, scanline status, comparison edges, and display IRQ events |
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |

View file

@ -152,9 +152,10 @@ Original regressions check the values separately. See [the public ARM result](..
## Thumb unused-memory data reads
Thumb data reads use the same unused address ranges and lane rules as ARM reads above.
The snapshot depends on the executing code region. P below is the executing instruction address, not the visible r15 operand.
The snapshot depends on the newly fetched region at P+4, even when P lies in the preceding region.
P below is the executing instruction address, not the visible r15 operand.
| Code region | Snapshot |
| Fetch region | Snapshot |
| --- | --- |
| External work RAM, palette RAM, video RAM, all three ROM windows | Halfword at P+4, repeated in both word lanes |
| BIOS and object attribute memory (OAM) | Full word at `(P+4) & ~3` |
@ -169,9 +170,14 @@ The pipeline's new fetch supplies the bus snapshot without rereading its instruc
BIOS/OAM samples include the other halfword of the aligned bus word, but retain only the selected halfword as an instruction.
Only the required mapped bytes are sampled. A 16-bit region needs two lookahead bytes, not four.
Normal memory mirrors apply to these bytes, including physical RAM/video-memory wrap within a mapped region.
If P+4 crosses a 16 MiB address-region boundary, the snapshot remains unknown.
This conservative diagnostic policy avoids guessing pipeline behavior during a region transition.
Missing lookahead or unknown history fails only an unused-memory load; ordinary instructions and mapped loads can still execute.
Crossing a 16 MiB region boundary does not by itself make the snapshot unknown.
Palette-to-VRAM fetches repeat the new halfword; VRAM-to-OAM fetches expose the new aligned word.
OAM-to-ROM and mapped ROM-window crossings repeat the fetched ROM halfword.
An EWRAM-to-IWRAM fetch drives its addressed local lane and requires the other lanes to be known.
For example, P=`0x02fffffc` needs prior high-lane history; P=`0x02fffffe` can establish both halves through its fill/refill and lookahead.
Missing lookahead, unknown IWRAM lanes, and unsupported I/O fetch observations still produce diagnostics for unused-memory loads.
Ordinary instructions and mapped loads can execute without a usable unused-memory snapshot.
See [the boundary evidence and original tests](../research/fetch-region-boundaries.md).
All loads in one instruction share the snapshot, including block loads and stack loads.
Normal sign extension, unaligned-load rotation, writeback, register aliases, and PC-load semantics still apply.
@ -207,8 +213,9 @@ Each instruction stages all new fetches and data-access changes.
Success commits history regardless of the resulting PC, instruction state, or region. Failure preserves the previous committed history.
Block-transfer data accesses stage updates in order; unused-memory reads still use one entry snapshot per instruction.
This preserves the existing atomic diagnostic policy without adding bus or device cycles.
Missing or region-crossing Thumb lookahead still leaves that instruction's unused-memory snapshot unsupported.
It does not erase independently known IWRAM lanes.
Missing or unsupported Thumb lookahead still leaves that instruction's unused-memory snapshot unknown.
Mapped boundary fetches into IWRAM use its resulting local lanes, even while the current instruction executes elsewhere.
Failed or other-region observations do not erase independently known IWRAM lanes.
### Thumb IWRAM refill history

View file

@ -50,7 +50,7 @@ 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.
- [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.
- [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. The Thumb implementation follows its 16-bit-region and BIOS/OAM formulas. IWRAM has persistent local lanes across ARM/Thumb fetches, data accesses, and DMA; region-crossing snapshots remain unsupported.
- [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. The Thumb implementation follows its 16-bit-region and BIOS/OAM formulas. IWRAM has persistent local lanes across ARM/Thumb fetches, data accesses, and DMA; mapped boundary snapshots follow the actual fetch region.
- [nocash GBA open-bus discussion](https://www.ngemu.com/threads/gba-open-bus.170809/), for ARM prefetch, repeated Thumb halfwords in 16-bit regions, aligned BIOS/OAM words, and history-dependent IWRAM lanes. Its data-load overwrite note prevents treating P+2 as a universal IWRAM rule.
- [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).
@ -162,6 +162,16 @@ Instruction retention and supported bus snapshots now use [shared fetch samples]
That refactor reuses the pinned NanoBoyAdvance loop/refill and ares local-lane evidence above.
Complete bus history and per-access timing remain incomplete; no new hardware conformance result is claimed.
Fetch-region boundary research:
- The pinned ares `getBus` and IWRAM helpers above, for address-based dispatch, local lane updates, and the full pre-shift bus value.
- [ares OAM reads at 6f6786e0](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/ppu/memory.cpp), for full aligned words before CPU halfword selection.
- The pinned jgenesis bus above, for width-dependent general-bus updates and IWRAM's complete local word driving open bus.
- The nocash open-bus discussion above, revisited through a boundary-focused web search. It documents region formulas, not a dedicated boundary hardware test.
See [fetch-region boundary findings](research/fetch-region-boundaries.md) for source agreement, implementation differences, and retained diagnostics.
No physical-hardware or external-emulator differential result is claimed.
Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work.
Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets.
The ignore file excludes `roms/`, common game ROM extensions, and save files.

View file

@ -0,0 +1,101 @@
# Instruction fetches across memory-region boundaries
## Finding
The shared sampler already selected bus width from the new fetch address.
However, `Memory::begin_cpu_access` rejected every Thumb sample whose upper address byte differed from the executing PC.
It also selected the IWRAM latch from the executing PC rather than the actual fetch address.
This prevented mapped boundary fetches from supplying an otherwise known bus value.
The boundary extension removes that blanket restriction.
Thumb unused-memory reads now use the bus observation at the newly fetched address P+4.
ARM continues to use its fetched PC+8 word.
This is a bounded entry-snapshot model, not a complete general-bus or cycle-accurate implementation.
## Evidence and limits
A boundary-focused web search revisited [nocash's open-bus discussion](https://www.ngemu.com/threads/gba-open-bus.170809/).
It describes repeated Thumb halfwords, full BIOS/OAM words, and history-dependent IWRAM lanes.
The search did not identify a dedicated physical-hardware boundary result for this implementation.
Those formulas alone do not establish every boundary or timing case.
The stronger implementation evidence is address-based bus dispatch:
- [ares bus at 6f6786e0](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp) selects the bus using the actual access address.
It assigns the full result to `mdr` before selecting the addressed CPU halfword/byte.
It does not reject a mapped fetch because the executing instruction occupies another region.
- [ares memory helpers](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/memory.cpp) return repeated halfwords from 16-bit regions and the complete local latch from IWRAM.
- [ares OAM helper](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/ppu/memory.cpp) returns the aligned word before CPU halfword selection.
- [jgenesis bus at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs) independently updates general open bus from access width and the addressed memory helper.
Its `iwram_update_open_bus` drives the complete local IWRAM word after a partial-lane update.
That revision marks OAM behavior as a TODO, so it is not independent confirmation of the full OAM rule.
- [NanoBoyAdvance's loop/refill helpers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) place the new fetch before execution and refill target pairs in order.
Its [open-bus helper](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/bus/bus.cc) uses opcode-based approximations with a BIOS/OAM TODO.
We do not treat that helper as an exact boundary oracle.
These are source comparisons, not physical-hardware measurements or external-emulator differential runs.
No external code, BIOS image, or test ROM was imported. All regression programs are original.
## Supported behavior
The instruction buffer and bus history consume the same captured new fetch.
The consumer does not reread memory or replay observations from retained instructions.
| Lookahead transition | Thumb unused-memory snapshot |
| --- | --- |
| Palette to VRAM | Newly fetched halfword repeated in both lanes |
| VRAM to OAM | Full aligned OAM word |
| OAM to mapped ROM | Newly fetched ROM halfword repeated in both lanes |
| Mapped ROM page or wait-window boundary | Halfword from the normal mapped cartridge offset, repeated |
| EWRAM to IWRAM | Persistent IWRAM word after the new fetch drives its addressed halfword |
The existing ROM mapping applies across all three wait-state windows.
A 16-bit fetch needs only its two mapped bytes, not an adjacent full word.
This includes OAM-to-ROM crossings, despite the executing instruction still residing on a 32-bit bus.
IWRAM does not acquire guessed lanes from adjacent memory.
At P=`0x02fffffc`, only the low half has been fetched from IWRAM; the high half needs prior observed history.
Without that history, an unused-memory load remains diagnostic.
At P=`0x02fffffe`, a cold fill or target-pair refill can capture IWRAM's low half before the high lookahead fetch.
The resulting complete local word is then available.
A preceding CPU data access or successful DMA can also establish the required lanes.
A refill can span two regions. Each actual IWRAM target sample updates only its addressed lanes.
Later host writes do not replace those captured lanes; the target instruction's new lookahead updates its own lane normally.
Diagnostic failures discard staged fetch/history changes and preserve the previous instruction buffer.
A retry takes a new lookahead sample from current memory.
## Preserved diagnostics and timing
- Missing ROM bytes and unmapped instruction reads remain strict errors or deferred fetch diagnostics.
- Unsupported I/O fetch observations do not borrow known IWRAM lanes or another region's bus value.
- Unknown IWRAM lanes do not become zero or fabricated history.
- An ordinary instruction or mapped data load does not require a complete unused-memory snapshot.
- A branch can discard an unavailable sequential path without failing early.
- Host reads, instruction fetches, and DMA do not inherit CPU unused-memory fallback.
- Protected BIOS history remains separate and image-derived.
- All data reads within an instruction still use one entry snapshot.
Nominal timing remains based on the current instruction address and existing refill summaries.
Crossing a wait-state window does not yet schedule code cost at the actual lookahead address.
Per-access device updates, video-bus contention, general DMA handoff, and Game Pak prefetch remain incomplete.
No boundary timing accuracy is claimed.
## Original regression coverage
Tests cover both Thumb alignments and cold/retained pipelines at mapped video/ROM boundaries.
Load matrices check byte, halfword, word, signed and unaligned results without flag changes.
Other tests cover block/stack writeback, CPU/DMA-seeded IWRAM lanes, unknown lanes, split refills, and failed-step retry.
ROM tests span mapped page and wait-window boundaries with distinct wait settings.
Short-ROM tests require exactly the fetched halfword.
Unsupported I/O, missing ROM, discarded branch paths, strict host reads, and unchanged ARM snapshots remain covered.
The mapped-crossing regressions failed under the old upper-address-byte guard, then passed with fetch-address selection.
## Validation result
Workspace and core/demo tests pass in debug and release on Darwin arm64.
Formatting, lint checks, rustdoc, preparation tests, native ROM windows, and graphics smoke modes pass.
Public ARM, Thumb, memory, and BIOS reports match the prior persistent-IWRAM reports exactly, including nominal cycle counts.
Debug and release reports also match for the same fixture paths.
These checks are regression results, not physical-hardware boundary or timing conformance tests.

View file

@ -57,8 +57,8 @@ Host inspection and setup never drive the latch.
Initial lanes remain unknown until observed accesses establish them.
Byte/halfword accesses may establish only part of the word; a word access establishes all lanes.
All unused-memory reads within an instruction still use one entry snapshot.
A Thumb fetch crossing a 16 MiB region boundary leaves that instruction's unused-memory snapshot unsupported.
Successful IWRAM accesses still update the local latch independently of this conservative snapshot policy.
The [boundary extension](fetch-region-boundaries.md) selects the bus observation from the actual new fetch region.
An EWRAM-to-IWRAM lookahead uses the resulting local lanes; incomplete lanes still leave that instruction's snapshot unknown.
## Thumb IWRAM refill extension
@ -102,7 +102,7 @@ Sub-instruction arbitration and DMA during CPU internal cycles remain unmodeled.
This is persistent local IWRAM history, not a complete general-bus implementation or cycle-accurate pipeline.
ARM unused-memory reads still use their supported PC+8 snapshot.
BIOS protection remains separately retained and image-derived.
General DMA open bus, unused/write-only I/O, disabled RAM, exact region-crossing snapshots, and per-access timing remain incomplete.
General DMA open bus, unused/write-only I/O, disabled RAM, unsupported-region snapshots, and per-access timing remain incomplete.
Nominal CPU/DMA costs do not change.
Original tests cover:
@ -117,7 +117,7 @@ Original tests cover:
The persistent-history regressions failed under the previous Thumb-continuation-only implementation.
Existing cold-entry tests now assert captured lane values instead of the removed unsupported-history diagnostic.
Region-crossing and strict unmapped-fetch diagnostics remain covered.
Unknown-lane boundary cases and strict unmapped-fetch diagnostics remain covered.
## Validation result

View file

@ -50,7 +50,8 @@ Strict instruction reads never use protected BIOS or unused-memory data fallback
## Current scope and limits
- Cold fills and cross-state local IWRAM history are now implemented in the [persistent-latch extension](iwram-bus-history.md).
- Thumb 16 MiB region crossings remain unsupported for bus snapshots.
- Mapped Thumb region crossings now use the actual fetched bus in the [boundary extension](fetch-region-boundaries.md).
- Unknown IWRAM lanes and unavailable/unsupported fetch observations remain diagnostic.
- BIOS history commits after successful BIOS instruction execution; target refills do not establish additional BIOS history.
- All unused-memory data reads within an instruction still use its entry snapshot.
- DMA remains scheduled between whole instructions. Sampling does not add an interleaving point.

View file

@ -57,7 +57,8 @@ Refill bus history and instruction buffering now consume the same captured targe
Buffer samples add no nominal cycles or data accesses.
Timed and untimed execution use the same buffer logic.
Destination-based refill summaries and one-shot non-sequential CPU resume costs remain unchanged.
Complete fetch-driven bus history, exact region crossings, per-access timing, Game Pak prefetch, and sub-instruction DMA arbitration remain incomplete.
Mapped boundary snapshots now follow the [actual fetch region](fetch-region-boundaries.md).
Complete bus history, exact boundary timing, Game Pak prefetch, and sub-instruction DMA arbitration remain incomplete.
Instruction-state-changing compare quirks also remain hardware-unverified.
## Original regression coverage

View file

@ -76,7 +76,7 @@ Update this file when a feature lands or a limit is removed.
- The original BIOS exposes documented boot, SoftReset, SWI, and IRQ protected-read words through its actual exit layout.
- ARM test/compare Rd=15 status restoration, without a PC result write or nominal refill.
- ARM unused-memory data reads return a PC+8 word snapshot, with normal byte lanes and load rotation.
- Thumb unused-memory reads use repeated PC+4 halfwords in 16-bit code regions and aligned PC+4 words in BIOS/OAM.
- Thumb unused-memory reads follow the actual PC+4 fetch region, including mapped boundaries, ROM windows, and IWRAM lane history.
- Persistent IWRAM lanes track ARM/Thumb fetches, CPU data accesses, and successful DMA, with known-bit tracking and diagnostic rollback.
- Cold fills and ARM/Thumb target pairs update local history; state changes and BIOS IRQ entry do not erase it.
- Little-endian byte, halfword, and word reads/writes.
@ -144,7 +144,8 @@ State changes, other-region execution, and BIOS IRQ entry preserve local history
Successful DMA can establish lanes before CPU startup. Resumed fetches then update their addressed lanes.
ARM/Thumb instruction buffering retains sequential instructions and refill target pairs.
Instruction buffering and supported bus history now consume the same mapped fetch samples, without sharing retained storage.
Complete ARM bus history, exact region-crossing bus behavior, general DMA handoff, and unused/write-only I/O open bus remain incomplete.
Mapped boundary snapshots follow the actual fetched bus; unknown IWRAM lanes, missing bytes, and I/O fetch values remain diagnostic.
Complete general-bus history, exact boundary timing, general DMA handoff, and unused/write-only I/O open bus remain incomplete.
[DMA channel data latches](hardware/dma.md#retained-channel-data) support blocked-source reuse, but never replace CPU bus history.
BIOS protection uses separate retained BIOS fetch snapshots as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection).
Host inspection and instruction fetches remain strict. Other unmapped reads return errors.
@ -155,7 +156,7 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
## Next steps
1. Extend shared fetch samples to verified general-bus history and region-crossing snapshots.
1. Extend shared fetch samples to verified general-bus data-access history and unsupported fetch regions.
General DMA open-bus reads and sub-instruction arbitration remain separate from retained channel data and local IWRAM lanes.
Keep unknown history diagnostic until fetch ordering and latch ownership have independent coverage.
Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history.

View file

@ -68,7 +68,11 @@ ARM open-bus regressions cover unused-range boundaries, PC+8 source regions, byt
They check branch-target resampling, missing lookahead, skipped conditions, nominal timing, device progress, and retained host/fetch/DMA diagnostics.
Thumb open-bus tests cover supported code regions, both instruction alignments, ROM windows, mirrors, and physical memory wrap.
They check widths, sign extension, rotation, block/stack/PC loads, aliases, modes, state preservation, and sequential/branch resampling.
Short-ROM tests require only the fetched halfword in 16-bit regions. Region-crossing tests retain explicit diagnostics.
Short-ROM tests require only the fetched halfword in 16-bit regions, including OAM-to-ROM boundaries.
Mapped boundary tests cover palette/VRAM/OAM/ROM transitions, EWRAM-to-IWRAM lanes, and ROM page/wait-window crossings.
They verify cold/retained pipelines, load widths and rotation, block/stack loads, split refills, rollback, and nominal timing.
Unknown IWRAM lanes, missing ROM bytes, and unsupported I/O fetch values retain explicit diagnostics.
See [fetch-region boundaries](research/fetch-region-boundaries.md) for source evidence and hardware-validation limits.
Cold IWRAM tests now assert actual current/decode/lookahead lane updates before the first data read.
Sequential IWRAM regressions check retained lanes across fetches, reads, writes, mirrors, and physical RAM wrap.
They cover raw bus values before load rotation/sign extension, all modes, block transfers, and isolation from other memory regions.