gameboy-rust/docs/research/prefetch-cancellation.md

8.6 KiB

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 now also match, after ordered timer accesses. The initial comparison isolated a bulk timer-sampling error, not a queue discrepancy. The original program, published observations, and measurement code remain unchanged.

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

Revision: 9ca57c13da7e3c569937f99a42e7c1caca029a2d

Reviewed file Bytes SHA-256
README.md 1662 4ff13a5ec496bcadc65aaa5264703ebafdf0709a7624c1bb1c6175461d213b9c
src/main.c 6753 0b4286917ae6f7baf9c44012bd521816bcfe7a068df59f12da14fbb8f4e32b04

The 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

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 now exits with status 0 because both comparisons match. It previously exited with status 1, exposing the bulk timer-sampling error described below. A matching instruction-boundary total still cannot override a failed timer comparison. Status 0 requires both comparisons to match for every case. Status 1 reports mismatches; 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.

Historical timer mismatch and correction

Before the correction, the opcode queue saw ordered source, data, and internal costs. The timer device updated only after the entire instruction completed. Consequently, the timer load read the old counter before its own source-fetch cost advanced 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
Previous timer-sample difference 14
Current timer-sample difference 17
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. Before the correction, each boundary-minus-timer discrepancy equaled 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.

Timer data accesses now sample staged device state at bus completion, including the source and data cycles. Timer writes use the same phase. Successful steps commit that state; failed steps discard it. The regression now requires actual timer values to match the unchanged published observations. No offset was added to the loaded value or diagnostic report. See ordered timer accesses for start/stop, IF, DMA, API isolation, and rollback coverage.

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 for those separate limits.

Ordered timer observations now retain rollback and avoid duplicate device advancement. Absolute timer startup and control-edge validation remain necessary. Independent full-buffer and page-boundary measurements remain necessary before claiming complete prefetch timing accuracy.