feat(cpu): retain fetch access kinds across instruction execution

This commit is contained in:
0xrsydn 2026-10-06 16:53:05 +07:00
commit 86321f0071
21 changed files with 637 additions and 61 deletions

View file

@ -72,7 +72,8 @@ Cold fill and target-pair samples do not add nominal cycles or data accesses.
The later [refill timing extension](refill-fetch-timing.md) replaces destination-only costs with source-fetch and target-pair costs.
IRQ entry also samples its discarded old-state fetch before the vector pair.
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.
The later [sequencing extension](fetch-access-sequencing.md) stores the next fetch kind in the instruction buffer.
Cold-fill startup costs and per-access 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

@ -53,7 +53,9 @@ DMA still executes only between instructions, not between data accesses or durin
The later [refill timing extension](refill-fetch-timing.md) replaces destination-only summaries with source-fetch and target-pair costs.
Resume changes the source access to N; the first target access independently remains N.
No extra access is appended. A resumed ROM branch can therefore cost more than its non-resumed refill.
Stores and fetches at 128 KiB ROM boundaries already use N and receive no duplicate cost.
The later [sequencing extension](fetch-access-sequencing.md) applies each instruction's data/internal effects to the following source fetch.
CPU history, DMA resume, and ROM page boundaries can all require N without duplicating an access.
A completed transfer after DMA can establish a new N requirement for the following fetch.
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,129 @@
# Persistent CPU fetch access kinds
S means sequential access; N means non-sequential access. W is the instruction width.
## Finding and evidence
Source-fetch addresses and refill target pairs were already timed separately.
However, the current instruction still selected its own source access kind.
Stores selected N immediately; loads, register shifts, and multiplies did not affect the following instruction.
IRQ entry always selected S unless DMA had completed.
This lost the sequence break between instruction execution and the next fetch.
The [ARM7TDMI load-multiple description](https://support.arm.com/documentation/ddi0210/c/Instruction-Cycle-Timings/Load-multiple-registers) separates the initial prefetch, data transfers, and final internal cycle.
Instruction cycle totals alone do not identify which neighboring fetch receives the bus-sequence change.
Two pinned implementations provide a source-level cross-check:
- [NanoBoyAdvance ARM handlers at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/handlers/handler32.inl) set the next pipeline access to N after transfers, swaps, register shifts, and multiplies.
- Its [Thumb handlers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/handlers/handler16.inl) do the same for corresponding Thumb operations.
- Its [CPU loop and refill helpers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) consume that kind before execution, including discarded IRQ fetches.
Failed ARM conditions set the following access to S. Target refills end with S.
- Its [bus timing](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/bus/timing.cc) charges internal cycles and requested ROM wait costs separately when prefetch is inactive.
- [ares memory helpers at 6f6786e0](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/component/processor/arm7tdmi/memory.cpp) end a burst on internal cycles and data loads/stores.
Its [instruction loop](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/component/processor/arm7tdmi/instruction.cpp) fetches before execution and restarts target refills with N followed by S.
Its [ARM handlers](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/component/processor/arm7tdmi/instructions-arm.cpp) supply the corresponding internal cycles.
This is agreement between source implementations, not a physical-hardware timing result.
No external emulator was executed. No external implementation or ROM was imported into the repository.
Game Pak prefetch and within-instruction bus scheduling remain outside this subset.
## Implemented subset
The CPU instruction buffer retains the next source-fetch kind alongside its expected PC and instruction state.
The current source fetch consumes the incoming kind, with the existing one-shot DMA override.
Successful execution then selects the following kind:
| Completed operation | Following fetch |
| --- | --- |
| Arithmetic without an internal cycle, immediate shifts, status transfers, skipped conditions, BL prefix | S |
| Loads/stores, block/stack transfers, swaps | N |
| Register-controlled shifts and multiplies | N |
| Branch, PC load/write, SWI, or machine IRQ target refill | S |
Register-controlled shifts break the sequence even when the shift amount is zero.
Data operations break it even when data lies in RAM or I/O rather than ROM.
A PC load finishes its data/internal work before its target pair; the pair therefore establishes S for target execution.
The target pair itself remains N(T), S(T+W).
`Cpu::step` and `Cpu::step_timed` commit the same sequence state.
Clones and full CPU equality include it. Failed instructions preserve it with the existing instruction buffer.
A failed current-slot fetch cannot consume it. Deferred lookahead errors do not prevent a successful current instruction from updating it.
IRQ entry captures the incoming kind before exception entry discards the old buffer.
Masked IRQ requests leave it unchanged.
DMA history stays in `Memory`, separate from CPU continuation state.
Either source can require N; their combination does not add a second fetch or a fixed surcharge.
A completed load after DMA can establish a new N requirement for the following fetch.
Host reads/writes, clock advancement, and HALT/STOP idle do not consume the CPU continuation.
Cold startup still uses nominal S and has no separate current/decode fill charge.
Debugger invalidation and PC/state mismatches deliberately return to that cold policy.
They cannot reuse another continuation's access kind. Debugger invalidation does not clear Memory's DMA override.
This policy is not a claim about hardware reset/startup timing.
A status-restoration quirk that changes instruction state without a normal refill also retains the existing cold-buffer policy.
## Example with default WS0 settings
For ARM code away from ROM boundaries, S costs 6 cycles and N costs 8 cycles.
Consider a RAM word load followed by two ordinary arithmetic instructions:
1. The load consumes its incoming S source fetch, then performs its data read and internal cycle.
2. The first arithmetic instruction consumes N because of the preceding load.
3. The second arithmetic instruction consumes S again.
A store follows the same source-kind sequence, without the load's internal cycle.
The store no longer changes its own source fetch retroactively.
A WAITCNT store uses old settings for its source fetch, then new settings and N for the following fetch.
A branch after a load consumes N at the source and still charges the target N+S pair.
N is an access kind, not always a positive cycle penalty.
Some wait settings make N cheaper than S. Tests use independent N/S tables rather than adding a fixed cost.
ROM page boundaries can force N independently; a forced boundary and a retained N do not duplicate an access.
## Original regression coverage
Instruction-pair tests cover:
- Supported ARM/Thumb transfer families, block/stack operations, signed loads, swaps, shifts, multiplies, and status operations.
- Skipped conditions, zero shift amounts, BL prefixes, and ordinary arithmetic.
- Every ROM window and wait setting, both instruction widths, and stored prefetch-bit values.
- Repeated transfers, branches after loads, and PC-load refills that restore S.
- Timed/untimed equivalence, cloning, failed data accesses, missing current slots, and masked IRQ requests.
- DMA and CPU sequence-break composition, IRQ source timing, and handler refill state.
- Host and HALT isolation, debugger invalidation, PC mismatches, and page/wait-window crossings.
Existing rollback, instruction semantics, load data/internal costs, and deferred diagnostics remain unchanged.
WAITCNT and DMA tests now distinguish a consumed resume from a new sequence break caused by the completed instruction.
## Validation and report audit
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 checkpoint assertions pass; debug and release reports match.
Compared with the preceding refill-timing change:
- ARM nominal cycles increase by 208; Thumb increases by 106; memory increases by 32.
Their other report fields remain unchanged.
- BIOS executes 963 fewer instructions and steps, with the same final nominal cycle total and all other report fields unchanged.
An original temporary trace runner compared both core versions against the pinned BIOS fixture.
The VBlank polling loop now costs 36 rather than 34 cycles because its load makes the following test instruction fetch N.
The updated core executes 321 fewer three-instruction polling loops.
IRQ entry moves to another position in the loop; the final checkpoint still has r12=0 and one IRQ entry.
This is an audit between our core versions, not external-emulator or hardware timing validation.
The original timer demo keeps its IRQ step and total duration.
Moving store fetch costs to following instructions shifts timer start two cycles earlier.
The timer therefore runs two additional cycles before the handler stops it.
## Remaining limits
- Cold-start costs and state-changing test/compare quirks are not hardware-validated.
- Data timing still uses an instruction-local access trace. This is not a complete bus-event scheduler.
- Device clocks still update once per instruction, IRQ entry, or DMA unit.
- Game Pak prefetch, configurable EWRAM timing, display contention, and DMA during internal cycles remain unimplemented.
- IRQ synchronization delays and complete BIOS/general-bus latch history remain incomplete.
The next timing work is Game Pak prefetch, with explicit startup and invalidation rules and independent tests.

View file

@ -27,7 +27,8 @@ All programs are original; no external source code or ROM was imported.
`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.
The later [sequencing extension](fetch-access-sequencing.md) retains the incoming access kind from the preceding instruction.
The pending DMA resume still overrides that source access to N.
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.
@ -65,8 +66,8 @@ Timer tests and the original timer demo now reflect this bounded rule.
The later [refill timing extension](refill-fetch-timing.md) also charges source fetches and target pairs for branches and IRQ entry.
- 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.
- Source S/N kinds now follow the preceding instruction's data/internal work.
Data timing and device scheduling still do not form a complete chronological bus trace.
- Refills now charge one source fetch plus the target N+S pair, not an additional fetch beside the old destination-only summary.
- Thumb BL prefixes use their lookahead address; suffixes add the target pair.
- Cold fills have no separate startup charge, even though their samples update supported bus history.
@ -74,7 +75,7 @@ The later [refill timing extension](refill-fetch-timing.md) also charges source
- 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 access-kind sequencing between instructions and data accesses.
The next timing step is Game Pak prefetch with verified startup and invalidation rules.
## Original regression coverage

View file

@ -78,7 +78,8 @@ A retry takes a new lookahead sample from current memory.
The later [fetch-address timing extension](fetch-address-timing.md) charges non-refill code at the actual lookahead address.
The [refill timing extension](refill-fetch-timing.md) also charges source fetches and target pairs individually.
Instruction-local access kinds and cold-start costs remain nominal.
The later [sequencing extension](fetch-access-sequencing.md) retains source kinds across CPU data/internal work.
Cold-start costs remain 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

@ -36,7 +36,7 @@ For an instruction executing at P, `Cpu::step_timed` charges its sampled lookahe
- ARM source: P+8, word width.
- Thumb source: P+4, halfword width.
- Access kind: the existing instruction-local summary, overridden to N after successful DMA.
- Access kind: now retained from the preceding instruction by the [sequencing extension](fetch-access-sequencing.md), with DMA override.
- WAITCNT: captured before instruction effects.
If the instruction refills, timing also charges N(T) and S(T+W) using the resulting state.
@ -112,11 +112,11 @@ The stopped counter therefore increases by five even though the final machine-cy
## Remaining limits
- Source access kinds still use instruction-local summaries, including the existing store rule.
Neighboring instruction/data-access sequencing is not a complete chronological bus trace.
- The later [sequencing extension](fetch-access-sequencing.md) retains the next source kind after CPU data/internal work.
Data timing and device scheduling still do not form a complete chronological bus trace.
- Cold current/decode fills have no separate startup timing charge.
- BIOS retained history is not a complete refill/data-access bus latch.
- Missing instruction bytes remain diagnostics, not hardware prefetch aborts.
- Game Pak prefetch, per-access device updates, display contention, configurable EWRAM timing, and sub-instruction DMA arbitration remain unimplemented.
The next timing work is independently verified access-kind sequencing between instructions and data accesses.
The next timing work is Game Pak prefetch with verified startup and invalidation rules.

View file

@ -57,7 +57,7 @@ Strict instruction reads never use protected BIOS or unused-memory data fallback
- 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.
- Non-refill timing now consumes the sample's address in the [fetch-address extension](fetch-address-timing.md).
- Refills now charge the source fetch and target pair; instruction-local source access kinds remain nominal.
- Refills now charge the source fetch and target pair; the [sequencing extension](fetch-access-sequencing.md) retains incoming source kinds.
- Per-access device updates, Game Pak prefetch, and exact refill/arbitration timing remain incomplete.
## Original tests

View file

@ -46,6 +46,7 @@ CPU stores, DMA transfers, and host writes never replace retained instructions a
`Cpu::invalidate_pipeline()` explicitly discards retained instructions for debugger repair or attachment to different memory.
It does not clear separate BIOS or IWRAM bus history.
CPU clones own independent buffers; full CPU equality includes them.
The later [sequencing extension](fetch-access-sequencing.md) also retains the next fetch kind in each buffer.
## Separation from bus history and timing