From 8930ea93b49928fa42e4f09a68cae27e475fbcc5 Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Tue, 6 Oct 2026 21:04:47 +0700 Subject: [PATCH] test(timing): compare prefetch cancellation with published observations --- .../gba-core/tests/prefetch_cancellation.rs | 98 +++++++++++ crates/gba-core/tests/timing.rs | 2 +- .../examples/prefetch_cancellation.rs | 51 ++++++ crates/gba-demos/src/lib.rs | 1 + crates/gba-demos/src/prefetch_probe.rs | 154 ++++++++++++++++++ docs/architecture.md | 3 + docs/demos.md | 12 ++ docs/hardware/cpu.md | 2 + docs/hardware/timers-irq.md | 4 + docs/references.md | 3 +- docs/research/gamepak-prefetch.md | 9 +- docs/research/prefetch-cancellation.md | 143 ++++++++++++++++ docs/status.md | 9 +- docs/testing.md | 4 + 14 files changed, 487 insertions(+), 8 deletions(-) create mode 100644 crates/gba-core/tests/prefetch_cancellation.rs create mode 100644 crates/gba-demos/examples/prefetch_cancellation.rs create mode 100644 crates/gba-demos/src/prefetch_probe.rs create mode 100644 docs/research/prefetch-cancellation.md diff --git a/crates/gba-core/tests/prefetch_cancellation.rs b/crates/gba-core/tests/prefetch_cancellation.rs new file mode 100644 index 0000000..2cbbe64 --- /dev/null +++ b/crates/gba-core/tests/prefetch_cancellation.rs @@ -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); +} diff --git a/crates/gba-core/tests/timing.rs b/crates/gba-core/tests/timing.rs index 36e1523..c3a9cfd 100644 --- a/crates/gba-core/tests/timing.rs +++ b/crates/gba-core/tests/timing.rs @@ -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] { diff --git a/crates/gba-demos/examples/prefetch_cancellation.rs b/crates/gba-demos/examples/prefetch_cancellation.rs new file mode 100644 index 0000000..d5c6e64 --- /dev/null +++ b/crates/gba-demos/examples/prefetch_cancellation.rs @@ -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 { + 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) + } + } +} diff --git a/crates/gba-demos/src/lib.rs b/crates/gba-demos/src/lib.rs index 843091c..9c6ead4 100644 --- a/crates/gba-demos/src/lib.rs +++ b/crates/gba-demos/src/lib.rs @@ -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; diff --git a/crates/gba-demos/src/prefetch_probe.rs b/crates/gba-demos/src/prefetch_probe.rs new file mode 100644 index 0000000..c831f26 --- /dev/null +++ b/crates/gba-demos/src/prefetch_probe.rs @@ -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, + /// 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 { + 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(), + }) +} diff --git a/docs/architecture.md b/docs/architecture.md index eb2d3cd..c05a46f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 | diff --git a/docs/demos.md b/docs/demos.md index 712a839..2051999 100644 --- a/docs/demos.md +++ b/docs/demos.md @@ -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: diff --git a/docs/hardware/cpu.md b/docs/hardware/cpu.md index 4bc2bab..23be7c0 100644 --- a/docs/hardware/cpu.md +++ b/docs/hardware/cpu.md @@ -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 diff --git a/docs/hardware/timers-irq.md b/docs/hardware/timers-irq.md index ce6cdf7..e99a23a 100644 --- a/docs/hardware/timers-irq.md +++ b/docs/hardware/timers-irq.md @@ -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. diff --git a/docs/references.md b/docs/references.md index 65c1cf7..78d8929 100644 --- a/docs/references.md +++ b/docs/references.md @@ -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. diff --git a/docs/research/gamepak-prefetch.md b/docs/research/gamepak-prefetch.md index e775384..8b77b78 100644 --- a/docs/research/gamepak-prefetch.md +++ b/docs/research/gamepak-prefetch.md @@ -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. diff --git a/docs/research/prefetch-cancellation.md b/docs/research/prefetch-cancellation.md new file mode 100644 index 0000000..989bf9f --- /dev/null +++ b/docs/research/prefetch-cancellation.md @@ -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. diff --git a/docs/status.md b/docs/status.md index 5aa9308..2a8215e 100644 --- a/docs/status.md +++ b/docs/status.md @@ -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. diff --git a/docs/testing.md b/docs/testing.md index 274cce5..0231ee9 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -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.