test(timing): compare prefetch cancellation with published observations

This commit is contained in:
0xrsydn 2026-10-06 21:04:47 +07:00
commit 8930ea93b4
14 changed files with 487 additions and 8 deletions

View 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.