fix(timers): apply machine accesses at bus completion

This commit is contained in:
0xrsydn 2026-10-06 21:44:24 +07:00
commit bfecc87e84
20 changed files with 765 additions and 137 deletions

View file

@ -24,7 +24,7 @@ Background progress cannot cause an early ROM-file lookup or change instruction
- [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 at 9ca57c13](https://github.com/zaydlang/PrefetchAbuse/blob/9ca57c13da7e3c569937f99a42e7c1caca029a2d/src/main.c) supplies published hardware read-cancellation observations.
A later [original probe](prefetch-cancellation.md) matches the instruction-boundary differences, but exposes incorrect bulk timer sampling.
The [original probe](prefetch-cancellation.md) matches instruction-boundary differences and, after ordered timer accesses, actual timer samples.
The source, README, and build assumptions were reviewed. No upstream implementation or ROM was imported.
No external implementation or ROM was added to the repository.
@ -76,7 +76,7 @@ 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.
Machine timer transactions now follow these events and commit on success. Other devices still advance in bulk after the step.
`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.
@ -135,8 +135,8 @@ Public ARM, Thumb, memory, and BIOS reports match the preceding ordered-timing c
Debug and release reports also match. Original CPU and timer demo traces are unchanged.
These public checkpoints do not independently validate active prefetch timing.
The later [cancellation comparison](prefetch-cancellation.md) matches published read observations only at instruction boundaries.
Actual timer readings still fail because device updates occur after the timer-load instruction.
Next, implement ordered timer observations and validate full-buffer restart and page boundaries independently.
The [cancellation comparison](prefetch-cancellation.md) originally matched published read observations only at instruction boundaries.
[Ordered timer accesses](ordered-timer-access.md) now make the unchanged timer readings match as well.
Next, validate absolute timer edges, full-buffer restart, and page boundaries independently.
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.

View file

@ -0,0 +1,100 @@
# Ordered timer data accesses
## Result
Machine timer reads and writes now use bus-access completion times instead of the instruction's initial clock.
The unchanged [prefetch cancellation probe](prefetch-cancellation.md) matches both published interval totals and unadjusted timer readings.
Its program, published observations, and measurement code were not changed to obtain this result.
This is a timer-specific extension to nominal timing, not a complete cycle-accurate device scheduler.
Display events, DMA arbitration, timer startup delays, and IRQ synchronization remain separate limits.
No physical-hardware measurement or external-emulator differential run was performed locally.
## Evidence and phase choice
The preceding probe isolated the error: its missing interval equaled the difference between the final source-fetch costs.
The opcode queue computed those costs correctly, but the timer read occurred before they advanced device time.
Moving only the probe's reported result would conceal that error rather than correct device behavior.
The reviewed [Jgenesis bus at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs) charges the I/O cycle before register access.
Its timer reads and writes receive the resulting clock value.
This supports the implemented completion-phase rule for a one-cycle I/O access.
No source code was imported from that implementation.
The published subtraction experiment validates relative read intervals.
It does not independently establish every absolute timer edge, startup delay, or control-write phase.
Those remain explicit validation limits.
## Staged clock ownership
`Memory` owns an optional `TimerStep` during each machine instruction, IRQ entry, or DMA unit.
The transaction copies only the four timers and timer IF bits, not RAM, display state, or the entire I/O object.
Its elapsed timestamp starts at zero and increases monotonically.
It has no heap allocation or event log.
For a CPU instruction:
1. Stage timer progress through the source fetch.
2. For each data access, stage its elapsed cycles before reading or writing timer registers.
3. Stage internal cycles and any target-pair fetches in their existing order.
4. On success, commit timers and timer IF bits at the final timestamp.
5. Advance the other devices and the machine clock once, without ticking timers again.
One word access has one sampling time for all four byte lanes.
Reload bytes merge before control bytes at that same time.
Block transfers have separate completion times for successive words.
A start write cannot count earlier source cycles. A stop write includes clocks through its completion.
Prescaler remainders, cascaded overflow pulses, and reload behavior use the existing timer implementation.
Timer IF reads include requests raised through the access phase.
Acknowledgement clears timer requests already present at that phase; later work can raise them again.
Committing timer flags preserves non-timer IF bits.
Those other sources still follow the existing bulk event model, so IF is not a fully cycle-accurate cross-device snapshot.
IRQ delivery remains at machine-step boundaries, never halfway through an instruction.
A committed enabled timer request wakes HALT. STOP does not use timer requests as a wake source.
### DMA
Each unit uses the existing nominal two-cycle startup on its first transfer.
Timer sampling follows startup plus source cost; destination writes follow the destination cost.
A failed source or destination discards the timer transaction.
A successful unit commits timer state before other device clocks and DMA completion IRQ handling.
This extends the existing whole-unit scheduler; it does not establish physical DMA startup or preemption timing.
### Errors and API isolation
CPU diagnostics discard staged time, counter values, prescaler remainders, and pending timer requests.
This includes failures before instruction fetch and failures after earlier block-load accesses.
Existing store validation still prevents partial writes when a block transfer has an invalid destination.
Previously completed machine steps remain committed.
`Cpu::step` and `Cpu::step_timed` remain CPU-only APIs.
They update CPU state, ordinary register writes, instruction retention, and enabled opcode-prefetch timing as before.
They do not create a timer transaction or advance device clocks.
Host reads and writes likewise remain clock-free.
`Memory::advance_cycles` and HALT idle intervals use the existing bulk timer advancement.
STOP idle freezes all clocks. The instruction entering STOP still pays its complete nominal duration.
Instruction fetches from I/O are not given the data-access timer sampling rule.
Other device reads, writes, rendering, and event scheduling retain their documented limits.
## Validation
Original tests cover ARM/Thumb loads, coherent byte lanes, start/stop stores, and ordered block transfers.
They check old-reload overflow behavior, prescaler retention, cascades, timer IF reads, acknowledgement, and later reassertion.
Other tests cover non-timer IF preservation, deferred IRQ delivery, DMA phases, rollback, and CPU-only isolation.
Capture-enabled and capture-disabled steps check that display clock splits do not tick timers twice.
BIOS HALT/STOP stores check final-cycle ownership and timer wake behavior.
The cancellation example exits successfully in debug and release on Darwin arm64.
Its CSV reports match between build profiles. No correction is applied to loaded timer values.
Workspace/core/demo tests, formatting, clippy, rustdoc, Python tests, native ROM windows, and graphics smoke modes pass.
Pinned public ARM, Thumb, memory, and BIOS reports remain byte-identical to the preceding change.
The original timer demo retains its cycle count and IRQ-entry step.
Its stopped counter changes from `0xfffb` to `0xfff4`: start excludes nine preceding cycles, while stop includes two bus cycles.
The alias-load regression now samples `0x00c0fffb` after source and data cycles, instead of the previous `0x00c0fffa`.
These expectation changes follow the access phases; the independent cancellation program and observations remain unchanged.
Next, validate absolute timer startup/control edges and independently test full-buffer prefetch restart and page boundaries.
Extend other device observations only with explicit clock ownership and rollback coverage.

View file

@ -4,9 +4,9 @@
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 do **not** match those observations.
The difference isolates an existing timer-sampling limitation, not a new queue discrepancy.
No core timing behavior was changed for this comparison.
Its actual emulated timer readings now also match, after [ordered timer accesses](ordered-timer-access.md).
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.
@ -70,10 +70,10 @@ 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 currently exits with status 1 because the timer comparisons fail.**
That status is expected evidence of a known limit, not a passing hardware-timing test.
A matching instruction-boundary total cannot override a failed timer comparison.
Status 0 requires both comparisons to match for every case. Status 2 reports invalid arguments or execution errors.
**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.
@ -90,11 +90,11 @@ The CSV columns are:
No timing correction is applied to `timer_delta`.
The runner does not hide, patch, or reinterpret failing timer values.
## Root cause of the timer mismatch
## Historical timer mismatch and correction
The opcode queue sees ordered source, data, and internal costs.
The timer device still updates only after the entire instruction completes.
Consequently, the timer load reads the old counter before its own source-fetch cost advances device time.
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.
@ -107,22 +107,24 @@ For a one-cycle multiply and WAITCNT `0x4000`:
| --- | ---: |
| Published interval difference | 17 |
| Machine-clock boundary difference | 17 |
| Actual timer-sample difference | 14 |
| 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`.
For every compared case, the boundary-minus-timer discrepancy equals the final source-fetch cost difference.
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.
The regression tests explicitly mark the bulk-sampling expectation as a known gap.
Replace that expectation when timer I/O moves to ordered bus phases.
Do not make the probe pass by adding an offset to its loaded timer value.
Timer startup/stop placement, other device events, and atomic failure behavior also need coverage before changing clock ownership.
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](ordered-timer-access.md) for start/stop, IF, DMA, API isolation, and rollback coverage.
## Coverage and limits
@ -139,5 +141,6 @@ The largest multiply delay here does not establish eight-halfword capacity behav
Live WAITCNT changes, DMA, HALT/STOP edges, and exact hardware timer startup delays remain outside this probe.
See [the queue model](gamepak-prefetch.md) for those separate limits.
Next, move timer observations toward ordered bus-access phases with explicit rollback and no duplicate device advancement.
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.