feat(timing): implement nominal Game Pak opcode prefetch

This commit is contained in:
0xrsydn 2026-10-06 20:32:54 +07:00
commit 8f22092e06
25 changed files with 1062 additions and 124 deletions

View file

@ -1,115 +1,139 @@
# Game Pak prefetch: evidence and timing prerequisites
# Game Pak prefetch: nominal queue and evidence
## Current result
Game Pak prefetch is **not enabled in the core yet**.
WAITCNT bit 14 remains stored without accelerating instruction fetches.
This change prepares ordered CPU timing transactions without changing cycle totals or instruction results.
WAITCNT bit 14 enables a nominal Game Pak opcode prefetch queue.
The queue tracks eight halfwords, partial transfers, full-buffer stops, and cancellation stalls.
It follows ordered CPU timing events and the existing instruction-boundary DMA scheduler.
This is source-backed timing behavior, not hardware-verified accuracy.
The previous timing path collected data costs during execution, then calculated source and refill costs afterward.
That produced correct existing totals but could not directly advance a prefetch queue in bus-access order.
A prefetch implementation needs to distinguish cartridge-bus occupation from time available for background fetching.
An aggregate instruction cost cannot identify when to fill or invalidate a queue.
The queue stores addresses and timing metadata, not ROM bytes.
CPU instruction retention, BIOS protected-read history, and IWRAM local lanes remain separate.
Background progress cannot cause an early ROM-file lookup or change instruction values.
## Sources reviewed
- [GBATEK GamePak Prefetch](https://www.problemkaputt.de/gbatek-gba-gamepak-prefetch.htm) describes eight 16-bit entries, opcode-only service, and prefetch during internal or other-memory accesses.
- [mGBA's cycle-counting article](https://mgba.io/2015/06/27/cycle-counting-prefetch/) explains independent cartridge-bus progress, partially completed fetches, and instruction-pair examples.
Its examples and linked timing suite provide leads for independent validation, not a local hardware-test pass.
- [NanoBoyAdvance bus timing at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/bus/timing.cc) distinguishes queue hits, in-progress fetches, misses, and cancellation stalls.
- [ares prefetch at 6f6786e0](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/prefetch.cpp) provides an independent halfword-queue implementation.
Its [bus dispatcher](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp) separates opcode requests, data requests, and other-region progress.
- [GBATEK GamePak Prefetch](https://www.problemkaputt.de/gbatek-gba-gamepak-prefetch.htm) describes eight 16-bit entries, opcode-only service, and idle-bus progress.
- [mGBA's cycle-counting article](https://mgba.io/2015/06/27/cycle-counting-prefetch/) explains independent cartridge progress and partially completed fetches.
Its examples and linked timing suite provide validation leads, not a local hardware-test pass.
- [NanoBoyAdvance bus timing at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/bus/timing.cc) distinguishes hits, partial fetches, misses, and cancellation stalls.
- [ares prefetch at 6f6786e0](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/prefetch.cpp) uses halfword entries and stops at capacity or page boundaries.
Its [bus dispatcher](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp) separates opcode requests, data requests, and other-memory progress.
- [Jgenesis prefetch at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/prefetch.rs) independently supports halfword entries, full-buffer stopping, and last-cycle cancellation stalls.
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.
No external implementation or ROM was added to the repository.
No physical-hardware measurement or external-emulator differential run was performed.
## Agreement and unresolved details
The sources agree on the main model:
- Prefetch is controlled by WAITCNT bit 14.
- The buffer serves Game Pak opcodes, not arbitrary ROM data loads.
- Internal CPU work and accesses to other memory can give the cartridge bus time to fetch ahead.
- A matching ready entry avoids ordinary ROM waits.
- A request for an in-progress fetch waits for the remaining work rather than restarting the full access.
- Cartridge data traffic and nonmatching code requests interact with the active prefetch sequence.
However, the implementation details are not interchangeable:
| Area | Source observation | Required follow-up |
| --- | --- | --- |
| Entry granularity | NanoBoyAdvance groups ARM words; ares tracks halfwords | Verify partial ARM fetches and ARM/Thumb transitions |
| Full buffer | ares stops production and restarts after draining; NanoBoyAdvance uses an instruction-sized countdown/count model | Establish capacity, pause, drain, and restart timing independently |
| ROM page boundary | ares prevents background reads across the forced-N boundary; NanoBoyAdvance's queue step uses its retained duty | Verify 128 KiB boundaries without assuming a normal ready-entry hit |
| Cancellation | Both contain completion-edge stall handling | Verify the exact halfword phase and which code/data requests trigger a stall |
| DMA and idle | Prefetch shares cartridge time with other bus owners | Verify RAM-only DMA, cartridge DMA, HALT/STOP, and resume separately |
| WAITCNT changes | Queue state and current transfer timing depend on register changes | Define enable, disable, and changed-wait behavior before integration |
These differences do not establish that either implementation is wrong.
They show why copying one queue policy is not sufficient evidence for the entire subsystem.
The first implementation should state its supported cases and retain explicit limits for unresolved behavior.
## Ordered CPU timing transactions
`CpuTiming` replaces the data-only accumulator.
A timed CPU instruction now records these events in order:
1. Source instruction fetch, using the incoming access kind and instruction width.
2. Actual data reads/writes, using their aligned bus addresses and widths.
3. Internal CPU cycles from the incoming instruction classification.
4. Refill target N and target+width S accesses, using the resulting instruction state.
## Implemented queue rules
S means sequential access; N means non-sequential access.
An instruction without data, internal work, or a refill omits those events.
The two refill events occur in `Cpu::refill_pipeline`, alongside its existing mapped target samples.
No separate refill-cost formula runs after execution.
IRQ entry records its discarded incoming-state source fetch before its ARM vector pair, without a data or internal event.
Raw ROM costs include one clock plus the configured wait states for each halfword.
The public `bus_cycles` helper remains stateless and does not apply prefetch.
Code timing retains the entry WAITCNT snapshot.
Data timing retains the settings observed at each actual bus access, including pre-write settings for a WAITCNT store.
The next instruction uses the new WAITCNT settings.
The existing data N/S rule and persistent CPU source-fetch kind are unchanged.
1. A ROM code miss pays the requested raw cost, plus any cancellation stall.
It starts background fetching at the following halfword address.
2. Internal cycles and non-cartridge memory accesses advance an already active stream.
They cannot start a stream by themselves.
3. A code request must match the queue head. CPU N requests can still match queued code.
A queued Thumb halfword or complete ARM word costs one CPU-side cycle.
4. That CPU-side cycle also advances background work when production is active.
A partial ARM word waits separately for its missing halfwords.
5. Background production stops when an advance observes all eight entries occupied.
It remains stopped while the queue drains. The next empty demand restarts with N timing.
6. ROM data accesses cancel the stream instead of consuming opcode entries.
A nonmatching ROM code request also cancels it before starting a new stream.
7. Cancellation costs one additional cycle when an active halfword has one cycle remaining.
A paused or fully occupied queue does not add that stall.
8. Background production pauses before a 128 KiB ROM boundary.
Demand at that boundary uses raw forced-N timing and starts the next stream.
No background transfer crosses beyond the final ROM window.
The transaction returns the same `StepTiming` fields as before.
A diagnostic discards the entire transaction, including any data events recorded before a late failure.
Missing lookahead or target bytes retain their existing deferred diagnostic policy and nominal attempted-fetch cost.
Device clocks still advance once after a successful machine step.
This is ordered cost accounting, not a cycle-by-cycle bus scheduler.
The full-buffer policy follows ares and Jgenesis.
The page-boundary policy follows ares conservatively; the reviewed sources differ in this area.
The cancellation cycle is included in the requesting code or data field of `StepTiming`.
These rules have original regression tests, not a hardware conformance result.
Untimed execution, host inspection, cold-fill sampling, and DMA do not join a CPU timing transaction.
They retain their existing behavior and do not create extra code/data charges.
Untimed CPU execution still updates instruction retention and the next fetch kind, but does not compute a timing transaction.
A future active prefetch model must define timing-state progression for this API explicitly.
## Timing transactions and ownership
`Memory` owns the committed queue because CPU, DMA, and HALT share cartridge-bus time.
Each `CpuTiming` transaction stages a copy and applies events in order:
1. Source instruction fetch at ARM P+8 or Thumb P+4.
2. Actual data accesses, using aligned bus addresses and widths.
3. Internal cycles from the incoming instruction classification.
4. Refill target N and target+width S accesses, using the resulting instruction state.
IRQ entry records the discarded incoming-state source, then the ARM vector pair.
Non-ROM code fetches advance the existing ROM stream without consuming it.
A branch can therefore preserve the stream while executing a BIOS handler.
A later ROM fetch still needs an exact head match to use queued timing.
Success commits queue progress. A diagnostic discards all staged progress, including earlier successful data accesses.
Missing lookahead or target bytes retain deferred diagnostics and nominal attempted-fetch costs.
Device clocks still advance once after a successful machine step, not after each timing event.
`Cpu::step_timed` updates the queue and returns costs without advancing devices.
`Cpu::step` uses the same path when prefetch is enabled, but discards returned costs.
With prefetch disabled, the CPU-only path retains its previous untimed behavior.
Host reads, setup writes, and `Memory::advance_cycles` do not supply queue progress.
Host WAITCNT writes do apply queue configuration.
Cold entry, debugger invalidation, and PC/state mismatches clear staged queue metadata.
They preserve the existing nominal S startup policy and add no current/decode fill charge.
Accepted IRQ entry uses the interrupted pipeline's cold/retained state before replacing that pipeline.
Failed CPU steps preserve the previously committed queue.
### WAITCNT changes
A store pays source and data costs using the previous settings.
After the write, changing ROM wait fields or bit 14 clears timing metadata immediately.
Changing only SRAM or PHI fields preserves the stream.
CPU, host byte/halfword/word, and DMA writes use this configuration policy.
This reset policy is deliberate, not a verified model of live hardware reconfiguration.
Jgenesis models retained entries after disable; that behavior is not implemented here.
Exact enable/disable transitions and changes during a partial transfer need independent tests.
### DMA and idle
A DMA unit stages queue progress independently of the CPU transaction.
Its existing two startup cycles precede source and destination accesses in the nominal model.
RAM-only DMA supplies idle cartridge time. Cartridge DMA cancels the stream and can incur the completion-edge stall.
A failed unit preserves queue state and clocks. A successful WAITCNT destination applies the new configuration.
DMA's CPU-resume N request remains separate; a matching queue entry can still satisfy that request.
`HaltIdle` advances an active queue with its idle cycles. `StopIdle` freezes it.
Machine device updates do not advance the queue a second time.
Exact DMA startup/completion placement, bus arbitration, and hardware HALT/STOP edges remain unverified.
## Original regression coverage
Test builds retain events from the last successful CPU timing transaction.
Production builds retain only totals, the code WAITCNT snapshot, and data-sequence state; they allocate no event log.
The test log covers the maximum supported block load, including PC, without truncation.
Tests inspect event order and metadata independently of total cycle assertions.
Queue tests cover all ROM windows, all N/S settings, ARM/Thumb widths, partial words, capacity, and restart.
They check every cancellation phase around a fill, exact-head matching, data cancellation, and page/window boundaries.
Configuration tests distinguish relevant fields from PHI/SRAM fields.
A seeded batched-versus-single-clock comparison checks progression consistency, not independent hardware correctness.
Coverage includes:
Integration tests cover RAM loads, register shifts, ROM data stalls, branch hits/misses, and PC-load refills.
They check timed/CPU-only equivalence, debugger invalidation, IRQ entry, WAITCNT writes, DMA, HALT, and STOP.
Failure tests cover late block loads, missing ROM bytes, failed DMA, retries, and host isolation.
Instruction-pair matrices separately assert CPU N/S kinds and prefetch-adjusted costs.
Existing ordered-event tests retain source/data/internal/refill ordering checks, including a full-register LDM with PC.
Test builds retain an event log; production builds allocate no event log or queue storage on the heap.
- Word PC loads and the full register-list LDM sequence.
- Thumb POP with word data and halfword target fetches.
- ARM-to-Thumb BX, taken branches to fallthrough, and skipped conditions.
- Swaps, register-shift internal cycles, and absence of duplicate source/refill events.
- WAITCNT stores with old source settings and new settings on the next fetch.
- IRQ entry after a Thumb load, including the retained non-sequential source kind.
- Failed data accesses, late block-load failure, missing current/lookahead slots, and retry isolation.
- Host reads, untimed CPU execution, and DMA isolation from the CPU trace.
## Validation
## Validation and remaining limits
Workspace and core/demo tests pass in debug and release on Darwin arm64.
Formatting, clippy, rustdoc, Python preparation tests, native ROM windows, and all graphics smoke modes pass.
The release executable is native Mach-O arm64.
Public ARM, Thumb, memory, and BIOS reports match the preceding sequencing change exactly, including cycles and step counts.
Public ARM, Thumb, memory, and BIOS reports match the preceding ordered-timing change exactly, including cycles and step counts.
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 implementation work
Implement and test queue progress against these ordered timing events before enabling WAITCNT acceleration.
Keep the cartridge queue separate from CPU instruction retention, BIOS protected-read history, and IWRAM local lanes.
Resolve startup, capacity, page boundaries, partial ARM fetches, cancellation, and WAITCNT changes with independent expectations.
DMA/idle progression and diagnostic rollback need separate tests before claiming those interactions are supported.
Next, validate this queue against independent timing tests, especially full-buffer restart, cancellation, and page boundaries.
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.