test(timing): compare prefetch cancellation with published observations
This commit is contained in:
parent
8f22092e06
commit
8930ea93b4
14 changed files with 487 additions and 8 deletions
98
crates/gba-core/tests/prefetch_cancellation.rs
Normal file
98
crates/gba-core/tests/prefetch_cancellation.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
//! Original ARM probes compared with independently published read observations.
|
||||
//! See docs/research/prefetch-cancellation.md for provenance and measurement limits.
|
||||
use gba_demos::prefetch_probe::{measure, ProbeError, PUBLISHED_READ_DELTAS, WAITCNT_SETTINGS};
|
||||
|
||||
#[test]
|
||||
fn instruction_boundary_deltas_match_published_read_observations() {
|
||||
for (index, expected) in PUBLISHED_READ_DELTAS.into_iter().enumerate() {
|
||||
for (waitcnt, expected) in WAITCNT_SETTINGS.into_iter().zip(expected) {
|
||||
let idle = index as u8 + 1;
|
||||
let control = measure(waitcnt, idle, false).unwrap();
|
||||
let read = measure(waitcnt, idle, true).unwrap();
|
||||
assert_eq!(
|
||||
read.boundary_cycles - control.boundary_cycles,
|
||||
u64::from(expected),
|
||||
"idle={idle}, WAITCNT={waitcnt:#06x}"
|
||||
);
|
||||
for sample in [control, read] {
|
||||
assert_eq!(sample.multiply_internal_cycles, u32::from(idle));
|
||||
assert_eq!(sample.completion_pc, 0x0800_0100 + sample.steps as u32 * 4);
|
||||
assert_eq!(sample.sample_timing.data_cycles, 1);
|
||||
assert_eq!(sample.sample_timing.internal_cycles, 1);
|
||||
}
|
||||
assert_eq!(read.steps, control.steps + 1);
|
||||
assert_eq!(control.rom_read_timing, None);
|
||||
assert_eq!(read.rom_read_timing.unwrap().internal_cycles, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observed_extra_cycle_is_in_rom_data_cancellation_not_internal_work() {
|
||||
for idle in 1..=8 {
|
||||
for (index, waitcnt) in WAITCNT_SETTINGS.into_iter().enumerate() {
|
||||
let sample = measure(waitcnt, idle, true).unwrap();
|
||||
let data = sample.rom_read_timing.unwrap();
|
||||
let raw_word = [8, 7, 7, 6][index];
|
||||
let stall = u32::from(index >= 2 && matches!(idle, 4 | 7));
|
||||
assert_eq!(data.data_cycles, raw_word + stall);
|
||||
assert_eq!(data.internal_cycles, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unadjusted_timer_samples_expose_the_instruction_boundary_sampling_limit() {
|
||||
// This is a known-gap regression, not an assertion that hardware behaves this way.
|
||||
// Replace it when timer I/O moves to ordered bus phases; do not correct probe output.
|
||||
for (index, expected) in PUBLISHED_READ_DELTAS.into_iter().enumerate() {
|
||||
for (waitcnt, expected) in WAITCNT_SETTINGS.into_iter().zip(expected) {
|
||||
let control = measure(waitcnt, index as u8 + 1, false).unwrap();
|
||||
let read = measure(waitcnt, index as u8 + 1, true).unwrap();
|
||||
let sampled = i64::from(read.timer_sample) - i64::from(control.timer_sample);
|
||||
let boundary = read.boundary_cycles as i64 - control.boundary_cycles as i64;
|
||||
let endpoint_fetch_difference = i64::from(read.sample_timing.code_cycles)
|
||||
- i64::from(control.sample_timing.code_cycles);
|
||||
assert_ne!(sampled, i64::from(expected));
|
||||
assert!(endpoint_fetch_difference > 0);
|
||||
assert_eq!(boundary - sampled, endpoint_fetch_difference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabling_prefetch_removes_the_cancellation_stall_and_endpoint_fetch_difference() {
|
||||
for idle in 1..=8 {
|
||||
for (index, waitcnt) in WAITCNT_SETTINGS.into_iter().enumerate() {
|
||||
let waitcnt = waitcnt & !0x4000;
|
||||
let control = measure(waitcnt, idle, false).unwrap();
|
||||
let read = measure(waitcnt, idle, true).unwrap();
|
||||
// Extra LDR: N word code + N word data + one internal cycle.
|
||||
let expected = [17, 15, 15, 13][index];
|
||||
assert_eq!(read.boundary_cycles - control.boundary_cycles, expected);
|
||||
assert_eq!(
|
||||
u64::from(read.timer_sample - control.timer_sample),
|
||||
expected
|
||||
);
|
||||
assert_eq!(read.sample_timing, control.sample_timing);
|
||||
assert_eq!(
|
||||
read.rom_read_timing.unwrap().data_cycles,
|
||||
[8, 7, 7, 6][index]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_rejects_unbounded_idle_requests_and_repeats_without_shared_state() {
|
||||
for idle in [0, 9, u8::MAX] {
|
||||
for with_read in [false, true] {
|
||||
assert!(
|
||||
matches!(measure(0x4000, idle, with_read), Err(ProbeError::IdleCycles(value)) if value == idle)
|
||||
);
|
||||
}
|
||||
}
|
||||
let first = measure(0x4010, 4, true).unwrap();
|
||||
measure(0x4004, 8, false).unwrap();
|
||||
assert_eq!(measure(0x4010, 4, true).unwrap(), first);
|
||||
}
|
||||
|
|
@ -113,7 +113,7 @@ fn internal_regions_and_ewram_use_fixed_width_specific_costs() {
|
|||
|
||||
#[test]
|
||||
fn stored_prefetch_phi_and_sram_settings_do_not_change_supported_bus_costs() {
|
||||
// Explicit limitation: no prefetch queue, PHI output, or SRAM mapping yet.
|
||||
// This stateless helper excludes opcode prefetch. PHI output and SRAM remain unmapped.
|
||||
for width in [AccessWidth::Halfword, AccessWidth::Word] {
|
||||
for address in [ROM_START, ROM_START + 4, 0x0a00_0004, 0x0c00_0004] {
|
||||
for kind in [AccessKind::NonSequential, AccessKind::Sequential] {
|
||||
|
|
|
|||
51
crates/gba-demos/examples/prefetch_cancellation.rs
Normal file
51
crates/gba-demos/examples/prefetch_cancellation.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
//! Diagnostic only: a matching boundary total does not make a timer mismatch pass.
|
||||
use std::process::ExitCode;
|
||||
|
||||
use gba_demos::prefetch_probe::{measure, ProbeError, PUBLISHED_READ_DELTAS, WAITCNT_SETTINGS};
|
||||
|
||||
fn run() -> Result<bool, ProbeError> {
|
||||
eprintln!("Original ARM read probe; published reference: PrefetchAbuse 9ca57c13.");
|
||||
eprintln!("Timer samples are unadjusted. This is not a run of the upstream ROM.");
|
||||
println!("idle_cycles,waitcnt,published_delta,boundary_delta,timer_delta,boundary_matches,timer_matches");
|
||||
let mut boundary_matches = 0;
|
||||
let mut timer_matches = 0;
|
||||
for (index, expected) in PUBLISHED_READ_DELTAS.into_iter().enumerate() {
|
||||
for (waitcnt, expected) in WAITCNT_SETTINGS.into_iter().zip(expected) {
|
||||
let control = measure(waitcnt, index as u8 + 1, false)?;
|
||||
let read = measure(waitcnt, index as u8 + 1, true)?;
|
||||
let boundary_delta = read.boundary_cycles as i64 - control.boundary_cycles as i64;
|
||||
let timer_delta = i64::from(read.timer_sample) - i64::from(control.timer_sample);
|
||||
let boundary_ok = boundary_delta == i64::from(expected);
|
||||
let timer_ok = timer_delta == i64::from(expected);
|
||||
boundary_matches += usize::from(boundary_ok);
|
||||
timer_matches += usize::from(timer_ok);
|
||||
println!(
|
||||
"{},0x{waitcnt:04x},{expected},{boundary_delta},{timer_delta},{boundary_ok},{timer_ok}",
|
||||
index + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
let cases = PUBLISHED_READ_DELTAS.len() * WAITCNT_SETTINGS.len();
|
||||
eprintln!(
|
||||
"Boundary matches: {boundary_matches}/{cases}; timer matches: {timer_matches}/{cases}."
|
||||
);
|
||||
eprintln!(
|
||||
"Full-buffer restart, Thumb, cartridge writes, and page boundaries are outside this probe."
|
||||
);
|
||||
Ok(boundary_matches == cases && timer_matches == cases)
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
if std::env::args_os().len() != 1 {
|
||||
eprintln!("prefetch_cancellation takes no arguments");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
match run() {
|
||||
Ok(true) => ExitCode::SUCCESS,
|
||||
Ok(false) => ExitCode::FAILURE,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ pub mod bitmap_demo;
|
|||
pub mod effects_demo;
|
||||
pub mod graphics_demo;
|
||||
pub mod mosaic_demo;
|
||||
pub mod prefetch_probe;
|
||||
pub mod raster_demo;
|
||||
pub mod tile_demo;
|
||||
pub mod timer_demo;
|
||||
|
|
|
|||
154
crates/gba-demos/src/prefetch_probe.rs
Normal file
154
crates/gba-demos/src/prefetch_probe.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
//! Original ARM probe for ROM-read cancellation timing, not an upstream ROM port.
|
||||
//! Published observations and scope: docs/research/prefetch-cancellation.md.
|
||||
//! Timer samples remain unadjusted. End-of-instruction clock differences are a
|
||||
//! separate diagnostic, not a replacement for a hardware-visible timer result.
|
||||
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
use gba_core::{
|
||||
cpu::Cpu,
|
||||
io::{TIMER_BASE, WAITCNT},
|
||||
machine::{Machine, MachineError},
|
||||
memory::{Memory, MemoryError, ROM_START},
|
||||
timing::StepTiming,
|
||||
};
|
||||
|
||||
pub const WAITCNT_SETTINGS: [u16; 4] = [0x4000, 0x4004, 0x4010, 0x4014];
|
||||
|
||||
/// Published hardware observations, indexed by idle count minus one, then WAITCNT_SETTINGS.
|
||||
/// Source: zaydlang/PrefetchAbuse, revision 9ca57c13da7e3c569937f99a42e7c1caca029a2d,
|
||||
/// src/main.c read_expected. These are measurement facts, not imported implementation code.
|
||||
/// They measure a test-minus-control interval, not the isolated ROM data-access cost.
|
||||
pub const PUBLISHED_READ_DELTAS: [[u16; 4]; 8] = [
|
||||
[0x11, 0x0f, 0x0f, 0x0d],
|
||||
[0x11, 0x0f, 0x0f, 0x0d],
|
||||
[0x11, 0x0f, 0x0f, 0x0d],
|
||||
[0x11, 0x0f, 0x10, 0x0e],
|
||||
[0x11, 0x0f, 0x0f, 0x0d],
|
||||
[0x11, 0x0f, 0x0f, 0x0d],
|
||||
[0x11, 0x0f, 0x10, 0x0e],
|
||||
[0x11, 0x0f, 0x0f, 0x0d],
|
||||
];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProbeError {
|
||||
IdleCycles(u8),
|
||||
Memory(MemoryError),
|
||||
Machine(MachineError),
|
||||
}
|
||||
|
||||
impl fmt::Display for ProbeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::IdleCycles(value) => write!(f, "idle cycles must be 1 through 8, got {value}"),
|
||||
Self::Memory(error) => write!(f, "cannot prepare prefetch probe: {error}"),
|
||||
Self::Machine(error) => write!(f, "cannot execute prefetch probe: {error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ProbeError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
Self::IdleCycles(_) => None,
|
||||
Self::Memory(error) => Some(error),
|
||||
Self::Machine(error) => Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Measurement {
|
||||
/// Actual timer value loaded by the ARM program, with no timing correction.
|
||||
pub timer_sample: u16,
|
||||
/// Clock interval between completion of timer enable and completion of timer sampling.
|
||||
pub boundary_cycles: u64,
|
||||
pub sample_timing: StepTiming,
|
||||
pub rom_read_timing: Option<StepTiming>,
|
||||
/// Sum of the multiply instructions' internal cycles, excluding other instructions.
|
||||
pub multiply_internal_cycles: u32,
|
||||
pub steps: usize,
|
||||
pub completion_pc: u32,
|
||||
}
|
||||
|
||||
/// Execute one bounded original ARM sequence with or without the extra ROM read.
|
||||
/// WAITCNT is host setup, outside the measured interval. No BIOS or assets are used.
|
||||
/// Other WAITCNT settings are allowed for experiments but have no published oracle here.
|
||||
pub fn measure(
|
||||
waitcnt: u16,
|
||||
idle_cycles: u8,
|
||||
with_rom_read: bool,
|
||||
) -> Result<Measurement, ProbeError> {
|
||||
if !(1..=8).contains(&idle_cycles) {
|
||||
return Err(ProbeError::IdleCycles(idle_cycles));
|
||||
}
|
||||
const ENTRY: usize = 0x100;
|
||||
const LITERALS: usize = 0x300;
|
||||
let mut rom = 0xe1a0_0000_u32.to_le_bytes().repeat(256);
|
||||
let registers = [
|
||||
(0, TIMER_BASE),
|
||||
(1, 0x0080_0000), // Timer 0: zero reload, enable, prescaler 1, no IRQ.
|
||||
(2, ROM_START),
|
||||
(8, 0xaa),
|
||||
(9, 0xaaaa),
|
||||
(10, 0x00aa_aaaa),
|
||||
(11, 0xaaaa_aaaa),
|
||||
];
|
||||
let mut code = Vec::new();
|
||||
for (index, (register, value)) in registers.into_iter().enumerate() {
|
||||
let offset = ENTRY + index * 4;
|
||||
let literal = LITERALS + index * 4;
|
||||
code.push(0xe59f_0000 | register << 12 | (literal - offset - 8) as u32);
|
||||
rom[literal..literal + 4].copy_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
let enable_index = code.len();
|
||||
code.push(0xe580_1000); // STR r1,[r0]: start timer, then expose one free cartridge cycle.
|
||||
let multiply_start = code.len();
|
||||
let mut remaining = idle_cycles;
|
||||
while remaining != 0 {
|
||||
let cycles = remaining.min(4);
|
||||
let rs = u32::from(7 + cycles);
|
||||
code.push(0xe004_0098 | rs << 8); // MUL r4,r8,Rs: 1..4 internal cycles.
|
||||
remaining -= cycles;
|
||||
}
|
||||
let multiply_end = code.len();
|
||||
if with_rom_read {
|
||||
code.push(0xe592_6000); // LDR r6,[r2]: ROM data cancels opcode prefetch.
|
||||
}
|
||||
code.push(0xe1d0_50b0); // LDRH r5,[r0]: sample timer at the same instruction phase in each run.
|
||||
for (index, instruction) in code.iter().enumerate() {
|
||||
let offset = ENTRY + index * 4;
|
||||
rom[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes());
|
||||
}
|
||||
let mut memory = Memory::new(rom).map_err(ProbeError::Memory)?;
|
||||
memory
|
||||
.write16(WAITCNT, waitcnt)
|
||||
.map_err(ProbeError::Memory)?;
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START + ENTRY as u32), memory);
|
||||
let mut start = 0;
|
||||
let mut multiply_internal_cycles = 0;
|
||||
let mut rom_read_timing = None;
|
||||
// The generated sequence is straight-line and bounded. Stop after the timer sample,
|
||||
// before any padding or literal is executed. Extra lookahead bytes remain mapped.
|
||||
for index in 0..code.len() {
|
||||
machine.step().map_err(ProbeError::Machine)?;
|
||||
if index == enable_index {
|
||||
start = machine.cycles();
|
||||
}
|
||||
if (multiply_start..multiply_end).contains(&index) {
|
||||
multiply_internal_cycles += machine.last_timing().internal_cycles;
|
||||
}
|
||||
if with_rom_read && index == multiply_end {
|
||||
rom_read_timing = Some(machine.last_timing());
|
||||
}
|
||||
}
|
||||
Ok(Measurement {
|
||||
timer_sample: machine.cpu().registers()[5] as u16,
|
||||
boundary_cycles: machine.cycles() - start,
|
||||
sample_timing: machine.last_timing(),
|
||||
rom_read_timing,
|
||||
multiply_internal_cycles,
|
||||
steps: code.len(),
|
||||
completion_pc: machine.cpu().pc(),
|
||||
})
|
||||
}
|
||||
|
|
@ -67,6 +67,9 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/gba-core/src/cpu/dma_resume_tests.rs` | One-shot non-sequential DMA resume costs, wait settings, failures, idle, IRQs, and API isolation |
|
||||
| `crates/gba-core/src/cpu/timing_tests.rs` | Instruction timing and semantic-equivalence checks |
|
||||
| `crates/gba-demos/src/timer_demo.rs` | Original timer-configuration program and IRQ handler |
|
||||
| `crates/gba-demos/src/prefetch_probe.rs` | Original bounded ARM read/control probes and pinned published cancellation observations |
|
||||
| `crates/gba-demos/examples/prefetch_cancellation.rs` | CSV comparison of published, instruction-boundary, and unadjusted timer intervals; fails on mismatches |
|
||||
| `crates/gba-core/tests/prefetch_cancellation.rs` | Published read comparisons, cancellation phase, disabled-prefetch controls, and the known timer sampling gap |
|
||||
| `Cargo.toml` | Workspace members and shared package settings |
|
||||
| `crates/gba-core/src/lib.rs` | Core modules |
|
||||
| `crates/gba-demos/src/lib.rs` | Demo modules, original instruction/exception bytes, and raw input-test ROM |
|
||||
|
|
|
|||
|
|
@ -3,6 +3,18 @@
|
|||
Every demo runs original test content through the `gameboy-rust` desktop executable. None of them load games.
|
||||
Keyboard mapping to GBA buttons lives in `crates/desktop/src/desktop.rs`.
|
||||
|
||||
## Diagnose prefetch cancellation timing
|
||||
|
||||
```sh
|
||||
direnv exec . cargo run --locked -p gba-demos --example prefetch_cancellation > /tmp/prefetch-cancellation.csv
|
||||
```
|
||||
|
||||
This headless original ARM probe compares published read-cancellation observations with two separate emulator measurements.
|
||||
Instruction-boundary totals match, but actual timer samples currently fail. The example therefore exits with status 1.
|
||||
Do not treat the matching totals as a hardware-timing pass.
|
||||
See [the protocol, source hashes, and timer sampling gap](research/prefetch-cancellation.md).
|
||||
The probe needs no downloaded ROM, assets, window, or external assembler.
|
||||
|
||||
## Open the affine raster demo
|
||||
|
||||
From a logged-in macOS desktop session:
|
||||
|
|
|
|||
|
|
@ -386,6 +386,8 @@ Host reads and `Memory::advance_cycles` do not advance the queue; machine steps
|
|||
|
||||
These are source-backed nominal rules, not a hardware conformance claim.
|
||||
See [queue evidence, regression coverage, and limits](../research/gamepak-prefetch.md).
|
||||
The [original cancellation probe](../research/prefetch-cancellation.md) matches published read interval totals at instruction boundaries.
|
||||
Its unadjusted timer samples still fail because timer reads precede the instruction's device-clock update.
|
||||
|
||||
### Nominal CPU resume after DMA
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ Device clocks update in batches after instructions, exception entries, DMA units
|
|||
Reads observe register state before that batch.
|
||||
Writes, including timer start/stop and IF acknowledgement, take effect before the entire batch.
|
||||
This does not reproduce bus-access timing within an instruction or IRQ synchronization delays.
|
||||
The [prefetch cancellation probe](../research/prefetch-cancellation.md) makes this limitation measurable.
|
||||
Its timer load omits its own source-fetch time, which differs between the read and control runs.
|
||||
Nominal instruction-boundary totals match published observations; the actual timer samples do not.
|
||||
The diagnostic reports both values and returns failure rather than correcting the timer sample.
|
||||
|
||||
`Cpu::step` remains a CPU-only API. It does not honor HALT/STOP, execute DMA, advance device clocks, or sample device IRQs.
|
||||
`Cpu::step_timed` executes the same operation and returns `StepTiming`, without advancing devices.
|
||||
|
|
|
|||
|
|
@ -208,7 +208,8 @@ Game Pak prefetch research:
|
|||
|
||||
- [Jgenesis prefetch at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/prefetch.rs) and its [bus integration](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs), for independent halfword capacity, full-buffer pause/restart, CPU delivery, and cancellation rules.
|
||||
- [NanoBoyAdvance boundary test at cc3f4a28](https://codeberg.org/nba-emu/hw-test/src/commit/cc3f4a286cdef823980d9b353bd70befc9927d28/bus/128kb-boundary/source/main.c), reviewed for data LDM/DMA boundaries, not active opcode-queue conformance.
|
||||
- [PrefetchAbuse](https://github.com/zaydlang/PrefetchAbuse), a search-excerpt-only cancellation-test lead. Its source and ROM were not used.
|
||||
- [PrefetchAbuse source at 9ca57c13](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/src/main.c), its [README](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/README.md), and [Makefile](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/Makefile), for published read observations, actual multiply delays, and build assumptions. No upstream program or ROM was imported.
|
||||
See [the original cancellation comparison](research/prefetch-cancellation.md) for hashes, correspondence limits, matching nominal intervals, and failing timer samples.
|
||||
|
||||
See [prefetch behavior and evidence](research/gamepak-prefetch.md).
|
||||
The nominal queue is implemented. Source differences and live WAITCNT transitions remain validation questions.
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ Background progress cannot cause an early ROM-file lookup or change instruction
|
|||
Its [bus integration](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs) separates CPU delivery cycles from background transfers and DMA ownership.
|
||||
- [NanoBoyAdvance 128 KiB boundary test at cc3f4a28](https://codeberg.org/nba-emu/hw-test/src/commit/cc3f4a286cdef823980d9b353bd70befc9927d28/bus/128kb-boundary/source/main.c) tests data LDM and DMA boundaries.
|
||||
Reading this test does not validate active opcode-queue crossings. It was not run here.
|
||||
- [PrefetchAbuse](https://github.com/zaydlang/PrefetchAbuse) appeared in search results as a cartridge-access cancellation test lead.
|
||||
Only its search excerpt was reviewed. No expectations or implementation code were taken from it.
|
||||
- [PrefetchAbuse at 9ca57c13](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/src/main.c) supplies published hardware read-cancellation observations.
|
||||
A later [original probe](prefetch-cancellation.md) matches the instruction-boundary differences, but exposes incorrect bulk timer sampling.
|
||||
The source, README, and build assumptions were reviewed. No upstream implementation or ROM was imported.
|
||||
|
||||
No external implementation or ROM was added to the repository.
|
||||
No physical-hardware measurement or external-emulator differential run was performed.
|
||||
|
|
@ -134,6 +135,8 @@ Public ARM, Thumb, memory, and BIOS reports match the preceding ordered-timing c
|
|||
Debug and release reports also match. Original CPU and timer demo traces are unchanged.
|
||||
These public checkpoints do not independently validate active prefetch timing.
|
||||
|
||||
Next, validate this queue against independent timing tests, especially full-buffer restart, cancellation, and page boundaries.
|
||||
The later [cancellation comparison](prefetch-cancellation.md) matches published read observations only at instruction boundaries.
|
||||
Actual timer readings still fail because device updates occur after the timer-load instruction.
|
||||
Next, implement ordered timer observations and validate full-buffer restart and page boundaries independently.
|
||||
Cold-start timing, live WAITCNT reconfiguration, per-access device scheduling, and sub-instruction DMA arbitration remain incomplete.
|
||||
Exact bus history, display contention, configurable EWRAM timing, and timer/IRQ delays remain separate work.
|
||||
|
|
|
|||
143
docs/research/prefetch-cancellation.md
Normal file
143
docs/research/prefetch-cancellation.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# Prefetch cancellation: independent read observations
|
||||
|
||||
## Result
|
||||
|
||||
An original ARM probe reproduces the measured structure of the read cases in `zaydlang/PrefetchAbuse`.
|
||||
Its instruction-boundary cycle differences match every published read observation.
|
||||
Its actual emulated timer readings do **not** match those observations.
|
||||
The difference isolates an existing timer-sampling limitation, not a new queue discrepancy.
|
||||
No core timing behavior was changed for this comparison.
|
||||
|
||||
This is not a run or a port of the upstream ROM.
|
||||
The expected values are published measurement facts. The program builder and runner are original.
|
||||
No external implementation, ROM, saved data, Nintendo code, or assets were added.
|
||||
No physical-hardware measurement or external-emulator differential run was performed locally.
|
||||
|
||||
## Pinned evidence
|
||||
|
||||
Repository: [zaydlang/PrefetchAbuse](https://github.com/zaydlang/PrefetchAbuse)
|
||||
|
||||
Revision: `9ca57c13da7e3c569937f99a42e7c1caca029a2d`
|
||||
|
||||
| Reviewed file | Bytes | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| [README.md](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/README.md) | 1662 | `4ff13a5ec496bcadc65aaa5264703ebafdf0709a7624c1bb1c6175461d213b9c` |
|
||||
| [src/main.c](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/src/main.c) | 6753 | `0b4286917ae6f7baf9c44012bd521816bcfe7a068df59f12da14fbb8f4e32b04` |
|
||||
|
||||
The [Makefile](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/Makefile) was also reviewed for build assumptions.
|
||||
It expects devkitARM and libgba. This probe does not need either dependency.
|
||||
The pinned tree contains no license file or built ROM. Its saved-data file was not used.
|
||||
Only source links, hashes, and factual observations are recorded here; upstream code is not redistributed.
|
||||
|
||||
The author describes the expected arrays as hardware results.
|
||||
The README calls the delays NOPs, but `src/main.c` uses ARM multiply instructions.
|
||||
The source, not that wording, defines the compared protocol.
|
||||
The display labels also start at zero, while the first multiply supplies one internal cycle.
|
||||
The original probe labels cases by actual internal-cycle count, from one through eight.
|
||||
|
||||
## Compared protocol
|
||||
|
||||
Each case runs twice, once with an additional ROM data read and once without it.
|
||||
Both runs use fresh machines, the same WAITCNT setting, and the same multiply delay.
|
||||
|
||||
1. Set up the timer address, enable value, ROM data address, and multiplier values before measurement.
|
||||
2. Start Timer 0 with zero reload, prescaler one, and no interrupt request.
|
||||
3. Execute a multiply with one through four internal cycles.
|
||||
For larger delays, execute a four-cycle multiply followed by another multiply supplying the remainder.
|
||||
4. In the read run only, execute a word load from `0x08000000`.
|
||||
5. Read Timer 0 with a halfword load.
|
||||
|
||||
Code executes in the first ROM wait-state window, away from a 128 KiB boundary.
|
||||
The final setup literal load cancels earlier queue activity before the timer-start store establishes the measured stream.
|
||||
All measured instruction bytes and lookahead are mapped.
|
||||
The probe stops after the timer sample; it does not execute padding, literals, or a return handler.
|
||||
|
||||
Our setup register allocation and code layout differ from upstream.
|
||||
There is no C runtime, console, input loop, BIOS boot, or upstream function prologue in the probe.
|
||||
The measured store/multiply/read/sample structure and multiplier values are preserved.
|
||||
This correspondence supports a bounded comparison, not a claim of binary equivalence.
|
||||
|
||||
The source compares `read_test - calibration` separately for each delay and WAITCNT setting.
|
||||
The observations measure that interval difference, not the isolated data-access cost of one LDR.
|
||||
The four WAITCNT values are `0x4000`, `0x4004`, `0x4010`, and `0x4014`.
|
||||
The factual read table is recorded in `gba_demos::prefetch_probe::PUBLISHED_READ_DELTAS`.
|
||||
The queue implementation does not supply or calculate that table.
|
||||
|
||||
## Reproduce on Darwin
|
||||
|
||||
```sh
|
||||
direnv exec . cargo test --locked -p gba-core --test prefetch_cancellation
|
||||
direnv exec . cargo run --locked -p gba-demos --example prefetch_cancellation > /tmp/prefetch-cancellation.csv
|
||||
```
|
||||
|
||||
**The example currently exits with status 1 because the timer comparisons fail.**
|
||||
That status is expected evidence of a known limit, not a passing hardware-timing test.
|
||||
A matching instruction-boundary total cannot override a failed timer comparison.
|
||||
Status 0 requires both comparisons to match for every case. Status 2 reports invalid arguments or execution errors.
|
||||
The example takes no arguments and needs no downloaded files, window, or external assembler.
|
||||
|
||||
Use `--release` to repeat the same checks in a release build.
|
||||
Debug and release CSV reports match on Darwin arm64.
|
||||
The CSV columns are:
|
||||
|
||||
- `idle_cycles`: sum of multiply internal cycles, not total elapsed time.
|
||||
- `waitcnt`: configured wait-state control.
|
||||
- `published_delta`: upstream read-minus-control observation, in decimal cycles.
|
||||
- `boundary_delta`: read-minus-control machine-clock interval between completed timer-start and timer-sample instructions.
|
||||
- `timer_delta`: difference between the actual timer values loaded into the CPU register.
|
||||
- `boundary_matches`, `timer_matches`: separate comparisons with the published observation.
|
||||
|
||||
No timing correction is applied to `timer_delta`.
|
||||
The runner does not hide, patch, or reinterpret failing timer values.
|
||||
|
||||
## Root cause of the timer mismatch
|
||||
|
||||
The opcode queue sees ordered source, data, and internal costs.
|
||||
The timer device still updates only after the entire instruction completes.
|
||||
Consequently, the timer load reads the old counter before its own source-fetch cost advances device time.
|
||||
|
||||
The read run cancels prefetch with ROM data traffic.
|
||||
Its subsequent timer load must fetch code from ROM again.
|
||||
The control run retains queue progress, so its timer load has a shorter code-fetch cost.
|
||||
Sampling both timers too early therefore loses different numbers of cycles; calibration cannot cancel that difference.
|
||||
|
||||
For a one-cycle multiply and WAITCNT `0x4000`:
|
||||
|
||||
| Quantity | Cycles |
|
||||
| --- | ---: |
|
||||
| Published interval difference | 17 |
|
||||
| Machine-clock boundary difference | 17 |
|
||||
| Actual timer-sample difference | 14 |
|
||||
| Timer-load source fetch in the read run | 8 |
|
||||
| Timer-load source fetch in the control run | 5 |
|
||||
|
||||
The missing three cycles equal `8 - 5`.
|
||||
For every compared case, the boundary-minus-timer discrepancy equals the final source-fetch cost difference.
|
||||
The sample instructions have identical data and internal costs, so those costs cancel in the subtraction.
|
||||
|
||||
With prefetch disabled, the two final source-fetch costs match.
|
||||
The timer and boundary differences then match each other, and the cancellation-specific extra cycle disappears.
|
||||
This negative control helps separate the sampling problem from timer subtraction or multiply decoding errors.
|
||||
|
||||
The regression tests explicitly mark the bulk-sampling expectation as a known gap.
|
||||
Replace that expectation when timer I/O moves to ordered bus phases.
|
||||
Do not make the probe pass by adding an offset to its loaded timer value.
|
||||
Timer startup/stop placement, other device events, and atomic failure behavior also need coverage before changing clock ownership.
|
||||
|
||||
## Coverage and limits
|
||||
|
||||
The extra data cycle appears in the fast sequential settings after four or seven multiply internal cycles.
|
||||
Those cases match the published completion-edge cancellation pattern.
|
||||
Original regressions inspect data timing separately from multiply internal work and final timer-fetch timing.
|
||||
Other checks cover deterministic fresh runs, bounded delay arguments, completion PCs, and disabled-prefetch controls.
|
||||
Workspace/core/demo tests, clippy, formatting, and rustdoc pass on Darwin arm64 in debug and release.
|
||||
The existing pinned ARM, Thumb, memory, and BIOS reports remain unchanged.
|
||||
|
||||
This comparison does not cover Thumb code, cartridge writes, full-buffer restart, or page crossings.
|
||||
The upstream write cases remain unsupported because the core reports cartridge writes as diagnostics.
|
||||
The largest multiply delay here does not establish eight-halfword capacity behavior.
|
||||
Live WAITCNT changes, DMA, HALT/STOP edges, and exact hardware timer startup delays remain outside this probe.
|
||||
See [the queue model](gamepak-prefetch.md) for those separate limits.
|
||||
|
||||
Next, move timer observations toward ordered bus-access phases with explicit rollback and no duplicate device advancement.
|
||||
Independent full-buffer and page-boundary measurements remain necessary before claiming complete prefetch timing accuracy.
|
||||
|
|
@ -51,6 +51,7 @@ Update this file when a feature lands or a limit is removed.
|
|||
- Persistent fetch access kinds follow CPU data/internal work, compose with DMA resume, and reset after target refills.
|
||||
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows.
|
||||
- Bit 14 enables a nominal eight-halfword opcode prefetch queue, with partial fetches, cancellation, and DMA/idle integration.
|
||||
- An original ARM cancellation probe matches published read interval totals, while separately reporting incorrect bulk 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.
|
||||
- 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.
|
||||
|
|
@ -120,6 +121,7 @@ CPU/bus costs are nominal estimates, not a cycle-accurate implementation.
|
|||
The instruction buffer retains the next fetch kind; cold startup costs and per-access device scheduling remain unmodeled.
|
||||
Game Pak prefetch uses source-backed queue rules, not hardware-verified timing.
|
||||
Live WAITCNT transitions, page-boundary behavior, and exact DMA/idle ordering still need independent timing validation.
|
||||
Timer reads occur before the current instruction's device update. Prefetch cancellation probes expose the resulting sample error.
|
||||
Graphics support includes Mode 0–5 debug snapshots and HBlank row capture with sprites, mosaic, windows, and color effects.
|
||||
Internal affine origins, vertical window flags, and vertical mosaic counters track line boundaries.
|
||||
Horizontal window comparators retain four-cycle event history.
|
||||
|
|
@ -171,9 +173,10 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
|
|||
Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history.
|
||||
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.
|
||||
2. Validate the active nominal Game Pak prefetch queue against independent timing tests.
|
||||
Prioritize full-buffer restart, cancellation, page boundaries, and live WAITCNT transitions.
|
||||
See [prefetch behavior and evidence](research/gamepak-prefetch.md).
|
||||
Validate cold-start costs, then extend per-access device updates, bus arbitration, and timer/IRQ delays.
|
||||
The [cancellation probe](research/prefetch-cancellation.md) isolates a timer sampling gap despite matching nominal interval totals.
|
||||
Move timer observations to ordered bus phases without losing rollback or advancing devices twice.
|
||||
Validate full-buffer restart, page boundaries, live WAITCNT transitions, and cold-start costs.
|
||||
See [prefetch behavior and evidence](research/gamepak-prefetch.md); exact bus arbitration and timer/IRQ delays remain separate work.
|
||||
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.
|
||||
4. Replace nominal sprite work limits with verified individual fetch timing; add background fetch timing and per-pixel composition.
|
||||
|
|
|
|||
|
|
@ -275,6 +275,10 @@ They also check page/window boundaries, configuration changes, and batched-versu
|
|||
Integration tests check RAM/internal progress, ROM data stalls, branch and IRQ fetches, CPU-only equivalence, and debugger invalidation.
|
||||
WAITCNT CPU/host/DMA writes, RAM/cart DMA, HALT/STOP, failed steps, and missing ROM bytes have separate checks.
|
||||
See [prefetch evidence and limits](research/gamepak-prefetch.md). These tests validate the nominal model, not physical-hardware accuracy.
|
||||
An [original cancellation probe](research/prefetch-cancellation.md) compares read/control intervals with pinned, published hardware observations.
|
||||
Its instruction-boundary totals match; its actual timer samples fail. The example returns nonzero for those failures.
|
||||
Regressions identify the missing source-fetch interval without applying a correction to the timer values.
|
||||
Disabled-prefetch controls, delay bounds, completion PCs, and fresh-run determinism have separate checks.
|
||||
Instruction timing tests cover arithmetic, shifts, branches, transfers, block operations, multiply, and exception returns.
|
||||
Timed and untimed execution are compared across all 65,536 Thumb encodings with fixed initial registers.
|
||||
Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue