feat(cpu): charge non-refill code at pipeline fetch addresses

This commit is contained in:
0xrsydn 2026-10-06 15:34:45 +07:00
commit 0650b60908
26 changed files with 413 additions and 46 deletions

View file

@ -70,7 +70,8 @@ Failed-instruction tests continue to assert full-state preservation.
This implements persistent ARM instruction words, not a complete cycle-by-cycle pipeline.
Cold fill and target-pair samples do not add nominal cycles or data accesses.
The existing destination-based refill summaries and one-shot DMA resume timing remain unchanged.
The current instruction address still selects nominal code cost rather than a separately scheduled fetch address.
The later [fetch-address timing extension](fetch-address-timing.md) selects non-refill code cost from the newly sampled address.
Access kinds, cold-fill startup costs, and refill scheduling remain incomplete.
ARM PC+8 open bus and protected BIOS readback remain separate snapshots.
The [shared fetch sample](shared-fetch-samples.md) supplies the instruction-buffer entry and supported bus snapshots without duplicate reads.

View file

@ -45,14 +45,15 @@ A completed DMA WAITCNT store changes the settings before CPU resume.
## Limits
The timing model has no per-access fetch scheduling or DMA arbitration.
The later [ARM](arm-instruction-buffer.md) and [Thumb](thumb-instruction-buffer.md) instruction buffers retain instructions without changing these nominal costs.
It charges the current instruction address, not a separately scheduled PC+4/PC+8 pipeline fetch.
The later [ARM](arm-instruction-buffer.md) and [Thumb](thumb-instruction-buffer.md) instruction buffers retain instructions.
The [fetch-address timing extension](fetch-address-timing.md) now charges non-refill code at PC+4/PC+8 rather than the executing address.
A resumed access selects that fetch region's wait settings. It is still not a separately scheduled bus event.
DMA still executes only between instructions, not between data accesses or during CPU internal cycles.
Existing refill summaries remain destination-based `1N+2S` costs.
A resumed branch or exception consumes pending state without adding another nominal access.
This prevents double-counting the existing N component, but does not model the discarded old-PC fetch separately.
Stores and 128 KiB ROM boundaries already use N and receive no duplicate cost.
Stores and fetches at 128 KiB ROM boundaries already use N and receive no duplicate cost.
Game Pak prefetch remains unimplemented; WAITCNT bit 14 does not alter this nominal timing path.
This change does not select a DMA value for CPU open-bus reads.

View file

@ -0,0 +1,102 @@
# Non-refill timing at the actual fetch address
## Finding and evidence
The instruction buffer already sampled ARM P+8 or Thumb P+4 before executing the current instruction at P.
However, `Cpu::step_timed` charged non-refill code at P.
Near a memory-region, ROM wait-window, or 128 KiB page boundary, that selected the wrong bus cost.
It also charged a page-boundary access when execution later reached an already fetched instruction.
The [ARM7TDMI data-operation timing table](https://support.arm.com/documentation/ddi0029/g/instruction-cycle-timings/data-operations) identifies the normal instruction prefetch address as `pc+2L`.
Here L is the instruction length: four bytes in ARM state and two bytes in Thumb state.
An ordinary data operation overlaps this fetch; a register-controlled shift adds an internal cycle.
The [instruction-pipeline description](https://developer.arm.com/documentation/dvi0027/b/arm7tdmi/instruction-pipeline) provides the corresponding fetch/decode/execute model.
[NanoBoyAdvance at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) provides an implementation cross-check.
Its `Run` method takes the current retained opcode and fetches the next halfword/word at the advanced r15 before execution.
Its refill helpers advance visible r15 by two instruction widths after loading the target pair.
This supports using the new fetch address rather than the current retained opcode's address.
Existing [bus-cost rules](../hardware/cpu.md#instruction-and-bus-timing) supply the region, ROM wait-window, and forced page-boundary costs.
Existing [DMA resume evidence](dma-resume-timing.md) supports the retained one-shot sequence break.
This change uses those rules without claiming complete access-kind sequencing or refill timing.
No physical-hardware measurement or external-emulator differential run was performed.
All programs are original; no external source code or ROM was imported.
## Implemented subset
`Cpu::step_timed` captures the address of `Fetched.lookahead` before execution.
For a non-refill instruction, it passes that address to `bus_cycles` with the incoming instruction width.
The existing instruction summary selects sequential or non-sequential access, with the pending DMA resume override.
WAITCNT is captured before CPU execution, so a CPU store cannot retroactively change its own fetch cost.
The same rule applies to cold and retained pipelines.
Sampling alone adds no data-timing entry or device-clock update.
The returned code cost accounts for one new lookahead access, not an additional copy of it.
Cold current/decode fills still have no separate startup charge.
Successful untimed execution still consumes DMA resume without advancing clocks.
Failed execution retains the existing CPU/buffer, timing, and resume-state rollback rules.
A deferred lookahead error does not fail the current instruction early.
Its nominal code cost uses the attempted fetch address, even when supplied ROM bytes are missing there.
The strict instruction diagnostic occurs only when the missing slot becomes current.
An unmapped region retains the bus-cost helper's one-cycle fallback; this is diagnostic policy, not hardware abort timing.
## Examples with default WAITCNT
| Executing instruction | New fetch | Non-refill code cost |
| --- | --- | --- |
| ARM at `0x02fffff8` | IWRAM `0x03000000` word | 1 cycle, not EWRAM's 6 |
| Thumb at `0x02fffffc` | IWRAM `0x03000000` halfword | 1 cycle, not EWRAM's 3 |
| ARM at `0x07fffffc` | ROM `0x08000004` word | 6 cycles, not OAM's 1 |
| ARM at `0x0801fff8` | ROM page boundary `0x08020000` | 8 cycles for N+S |
| ARM at `0x08020000` | ROM `0x08020008` word | 6 cycles; no repeated page-boundary charge |
For ARM/Thumb ROM words/halfwords, a boundary cost now occurs two executed instruction positions earlier.
Crossing into another wait-state window selects that window's settings at the new fetch.
A completed DMA changes the access kind at this address; it does not append a second code access.
An ordinary ARM instruction starting directly at ROM base now costs 6 code cycles rather than 8.
The charged fetch is at ROM base plus eight, not at the page boundary.
This does not model the full hardware cost of initializing an empty pipeline.
Timer tests and the original timer demo now reflect this bounded rule.
## Deliberately retained limits
- Instruction-local S/N summaries remain in use, including the existing store rule.
Neighboring instruction/data-access sequencing is not a complete chronological bus trace.
- Branches, PC loads/writes, SWI, exception returns, and IRQ entry retain destination-based `1N+2S` summaries.
The old-PC fetch and target pair are not yet charged individually.
- The new lookahead cost is not added to those refill summaries; doing so would count an extra access.
- Thumb BL prefixes use their lookahead address; suffixes retain the nominal refill summary.
- Cold fills have no separate startup charge, even though their samples update supported bus history.
- Devices advance once after the whole successful instruction. No per-access timer/IRQ update is introduced.
- Game Pak prefetch, display contention, configurable EWRAM timing, and sub-instruction DMA arbitration remain unimplemented.
This is a fetch-address correction within the existing timing model, not cycle-accurate emulation.
The next timing step is verified source-fetch/target-pair accounting for refills and IRQ entry.
## Original regression coverage
New tests cover:
- ARM/Thumb memory-region crossings with cold and retained pipelines, compared with untimed execution.
- ROM 128 KiB boundaries using independent N/S tables for every wait setting and both prefetch-bit values.
- Wait-window crossings using the newly selected window's settings.
- DMA resume at a cross-region lookahead without duplicate code cost.
- CPU WAITCNT stores using old fetch settings and new settings on the next instruction.
- Deferred missing-lookahead diagnostics and unchanged nominal refill totals.
- Machine timer advancement from the corrected fetch cost before the next IRQ sample.
The address-sensitive tests failed with current-PC charging and pass with the new fetch address.
Existing ROM-start, boundary, BIOS-access, load-alias, and timer assertions were recalculated from PC+8/PC+4 costs.
Data results, access counts, flags, writeback, and failed-step preservation checks remain intact.
## Validation result
Workspace and core/demo tests pass in debug and release on Darwin arm64.
Formatting, lint checks, rustdoc, preparation tests, native ROM windows, and graphics smoke modes pass.
Public ARM, Thumb, memory, and BIOS reports match the prior fetch-boundary reports exactly, including cycle counts.
Debug and release reports also match for the same fixture paths.
The original timer demo's nominal total changes from 153 to 151 cycles because its initial ROM fetch is now charged at PC+8.
These are regression checks, not physical-hardware timing conformance results.

View file

@ -76,8 +76,8 @@ A retry takes a new lookahead sample from current memory.
- Protected BIOS history remains separate and image-derived.
- All data reads within an instruction still use one entry snapshot.
Nominal timing remains based on the current instruction address and existing refill summaries.
Crossing a wait-state window does not yet schedule code cost at the actual lookahead address.
The later [fetch-address timing extension](fetch-address-timing.md) charges non-refill code at the actual lookahead address.
Refill summaries remain destination-based; the instruction-local access kinds and cold-start costs are still nominal.
Per-access device updates, video-bus contention, general DMA handoff, and Game Pak prefetch remain incomplete.
No boundary timing accuracy is claimed.

View file

@ -55,7 +55,8 @@ Strict instruction reads never use protected BIOS or unused-memory data fallback
- BIOS history commits after successful BIOS instruction execution; target refills do not establish additional BIOS history.
- All unused-memory data reads within an instruction still use its entry snapshot.
- DMA remains scheduled between whole instructions. Sampling does not add an interleaving point.
- Current-instruction and destination-based nominal timing remain unchanged.
- Non-refill timing now consumes the sample's address in the [fetch-address extension](fetch-address-timing.md).
- Destination-based refill summaries and instruction-local access kinds remain nominal.
- Per-access device updates, Game Pak prefetch, and exact refill/arbitration timing remain incomplete.
## Original tests

View file

@ -56,7 +56,8 @@ Refill bus history and instruction buffering now consume the same captured targe
Buffer samples add no nominal cycles or data accesses.
Timed and untimed execution use the same buffer logic.
Destination-based refill summaries and one-shot non-sequential CPU resume costs remain unchanged.
Destination-based refill summaries remain unchanged.
The later [fetch-address timing extension](fetch-address-timing.md) selects non-refill and DMA-resume code costs from the new fetch address.
Mapped boundary snapshots now follow the [actual fetch region](fetch-region-boundaries.md).
Complete bus history, exact boundary timing, Game Pak prefetch, and sub-instruction DMA arbitration remain incomplete.
Instruction-state-changing compare quirks also remain hardware-unverified.