feat(cpu): account for source fetches in branch and IRQ refills

This commit is contained in:
0xrsydn 2026-10-06 16:27:45 +07:00
commit 2d7eb6e4eb
27 changed files with 638 additions and 87 deletions

View file

@ -69,7 +69,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 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.

View file

@ -27,7 +27,7 @@ A failed unit cannot create or consume it. A blocked-source unit that successful
Register configuration, host reads/writes, and clock-only advancement do not consume it.
Before instruction execution, `Cpu::step_timed` captures the pending state and WAITCNT.
For a non-refill instruction, pending resume changes the nominal code-access kind to non-sequential.
Pending resume changes the source code-access kind to non-sequential.
The existing bus-cost function applies the code width and ROM-window wait settings.
An ARM word still uses a non-sequential first halfword and a sequential second halfword.
This is a change of access kind, not a fixed positive surcharge: some wait settings make N cheaper than S.
@ -50,9 +50,9 @@ The [fetch-address timing extension](fetch-address-timing.md) now charges non-re
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.
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.
Game Pak prefetch remains unimplemented; WAITCNT bit 14 does not alter this nominal timing path.

View file

@ -63,18 +63,18 @@ Timer tests and the original timer demo now reflect this bounded rule.
## Deliberately retained limits
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.
- 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.
- 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.
- 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.
The next timing step is verified access-kind sequencing between instructions and data accesses.
## Original regression coverage

View file

@ -77,7 +77,8 @@ A retry takes a new lookahead sample from current memory.
- All data reads within an instruction still use one entry snapshot.
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.
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.
Per-access device updates, video-bus contention, general DMA handoff, and Game Pak prefetch remain incomplete.
No boundary timing accuracy is claimed.

View file

@ -51,7 +51,8 @@ Debugger invalidation clears only instruction retention, but the following cold
A successful instruction commits its staged lanes regardless of its resulting state, PC, or region.
A failed instruction discards every staged change, including cold-fill samples.
Successful PC-writing instructions then apply target fetches after their data accesses.
Other-region execution, BIOS IRQ entry, and unavailable targets do not erase known local lanes.
Other-region execution, BIOS vector fetches, and unavailable targets do not erase known local lanes.
The later [IRQ fetch extension](refill-fetch-timing.md) updates addressed IWRAM lanes through its discarded old-state source fetch.
Host inspection and setup never drive the latch.
Initial lanes remain unknown until observed accesses establish them.

View file

@ -0,0 +1,122 @@
# Source-fetch and target-pair refill timing
## Finding and evidence
The instruction buffer already sampled a branch's old-state lookahead and its two target instructions.
However, timing charged three destination accesses: N(T), S(T+W), and S(T+2W).
That omitted the source-region cost and charged the third target fetch before target execution.
The totals often matched within one memory region, hiding cross-region and instruction-state errors.
Machine IRQ entry also omitted the discarded source sample entirely.
The [ARM7TDMI branch timing description](https://support.arm.com/documentation/ddi0029/g/instruction-cycle-timings/branch-and-branch-with-link) specifies three accesses:
1. Prefetch from the current PC while calculating the destination. The decision cannot prevent this fetch.
2. Fetch the branch destination.
3. Fetch destination plus one instruction length to refill the pipeline.
The [software-interrupt and exception-entry description](https://support.arm.com/documentation/ddi0029/g/instruction-cycle-timings/software-interrupt-and-exception-entry) likewise describes a forced-address construction cycle followed by pipeline refill.
Its first address is `pc+2L`, using the incoming instruction state.
The vector pair uses ARM state.
[NanoBoyAdvance at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) provides a source cross-check:
- `Run` fetches the old-state lookahead before instruction execution.
- `ReloadPipeline16` and `ReloadPipeline32` read the target N and the next instruction S.
- `SignalIRQ` first performs a discarded halfword/word fetch using the incoming state and access kind.
It then changes mode/state and reloads the ARM vector pair.
This supports source-address and target-pair accounting, not complete chronological bus sequencing in this emulator.
The existing [DMA resume evidence](dma-resume-timing.md) supports changing the next source access to N.
No physical-hardware measurement or external-emulator differential run was performed.
Tests and tracing code are original. No external source code, ROM, or BIOS was added to the repository.
## Implemented subset
For an instruction executing at P, `Cpu::step_timed` charges its sampled lookahead once:
- 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.
- WAITCNT: captured before instruction effects.
If the instruction refills, timing also charges N(T) and S(T+W) using the resulting state.
The existing pipeline samples supply those instructions and update supported local history.
T+2W is sampled and charged when the first target instruction executes, not during the refill.
The same rule covers taken branches, BX, PC loads/writes, SWI, saved-state returns, and Thumb BL suffixes.
Untaken conditions and BL prefixes do not add a target pair.
Accepted machine IRQ entry samples the discarded incoming-state P+2W fetch, then the ARM vector pair.
It does not fetch or execute the interrupted current opcode, nor invent a cold current/decode fill.
The discarded sample drives its addressed IWRAM lanes when mapped; other regions leave that local history unchanged.
An unavailable discarded sample cannot prevent entry and is not retained as a current-slot diagnostic.
Unavailable vector samples fail only when execution reaches them.
IRQ sampling does not commit a BIOS instruction snapshot because no BIOS instruction executed.
The separate successful-BIOS-instruction history policy remains unchanged.
Masked IRQ requests have no fetch, history, CPU, or resume-state effects.
DMA still takes priority at machine-step boundaries.
Successful IRQ entry consumes pending DMA resume through normal machine-step completion.
Devices advance once by the total entry cost, not between its accesses.
The public memory-free `Cpu::enter_exception` and `Cpu::take_interrupt` APIs remain unchanged.
## Examples with default WAITCNT
| Operation | Source | Target pair | Code cycles |
| --- | --- | --- | --- |
| IWRAM ARM BX to Thumb WS1 | 1 | 5 + 5 | 11 |
| IWRAM Thumb BX to ARM WS0 | 1 | 8 + 6 | 15 |
| ARM WS0 branch to IWRAM | 6 | 1 + 1 | 8 |
| ARM WS0 branch within WS0 | 6 | 8 + 6 | 20 |
| Same branch after DMA | 8 | 8 + 6 | 22 |
| IRQ from ARM WS0 | 6 | 1 + 1 | 8 |
| IRQ from ARM WS0 after DMA | 8 | 1 + 1 | 10 |
These examples exclude ROM page boundaries.
Every access independently selects its actual region, wait window, and forced-N 128 KiB boundary cost.
A target pair can cross a region or wait-window boundary.
A boundary at T+2W is charged on target execution, not during the preceding branch.
DMA can produce two N accesses: the source fetch and the first target fetch. No fourth fetch is added.
## Original regression coverage
Tests cover:
- Incoming/outgoing ARM and Thumb widths, all ROM wait settings, and the stored prefetch bit.
- Boundaries in the first, second, and third target slots, plus split wait-window target pairs.
- ROM source boundaries and DMA resume before a RAM refill, with no delayed resume penalty.
- Cross-region source lookahead, data/internal costs for PC loads, and saved-state returns.
- IRQ source widths, missing ROM, masked requests, LR/SPSR preservation, and device-clock totals.
- IRQ IWRAM word and halfword lane updates, cold entry without invented fills, and retained Thumb entry.
- Missing-vector rollback after accepted entry, BIOS-history isolation, and timed/untimed CPU equivalence.
## Validation and report changes
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 fetch-address change, the ARM, Thumb, and memory reports each lose five nominal cycles.
Their step counts and all other report fields remain unchanged.
The BIOS report gains three instructions and 29 nominal cycles; all other fields remain unchanged.
A temporary original trace runner compared the preceding and updated cores against the pinned BIOS fixture.
The changed VBlank arrival position causes one extra three-instruction polling loop, adding 34 cycles.
Cross-region firmware transfers and IRQ entry together remove five cycles, giving the observed net increase of 29.
The trace still reaches the same checkpoint with r12=0 and one IRQ entry.
This trace comparison is a regression audit between our core versions, not external hardware validation.
The original timer demo retains its total nominal duration and IRQ step.
IRQ entry gains five cycles before the handler stops the timer; the return loses five cycles afterward.
The stopped counter therefore increases by five even though the final machine-cycle total is unchanged.
## 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.
- 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.

View file

@ -39,6 +39,7 @@ It does not replay the bus observations of retained current/decode instructions.
The consumer performs no mapped memory read.
After successful control flow, the same target samples fill the instruction buffer and update local IWRAM history in either state.
Machine IRQ entry uses this refill path; BIOS vector fetches leave IWRAM lanes unchanged.
The later [refill timing extension](refill-fetch-timing.md) first samples the discarded source fetch, which can update IWRAM lanes.
Instruction buffers and BIOS/IWRAM latches remain separate retained state.
Sampling alone changes no CPU access context, latch, device clock, DMA resume marker, or data-timing trace.
@ -56,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).
- Destination-based refill summaries and instruction-local access kinds remain nominal.
- Refills now charge the source fetch and target pair; instruction-local source 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,7 @@ 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 remain unchanged.
The later [refill timing extension](refill-fetch-timing.md) charges the incoming-state source fetch and outgoing-state target pair.
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.