fix(ppu): derive the H-IRQ dot from the clock, not a constant (T-06-A) - #300
Conversation
HIRQ_TRIGGER_DELAY = 4 was a dot-domain rounding of ares' `hcounter(10) == (HTIME+1)<<2`, exact only while every dot is four clocks -- which stopped being true when dots 323 and 327 became six. The match is now computed where it happens: clock 4*HTIME + 14, mapped to the first dot boundary at or after it. Below the long dots the two agree EXACTLY, because 4*HTIME + 14 is never a multiple of 4 and the next boundary is HTIME + 4. They diverge only for HTIME 321..=337, where the six-clock dots have displaced every later boundary: the old constant fired up to a whole dot late, and at HTIME 336 suppressed an IRQ that does fire. HTIME 337 lands on dot 340's boundary, which exists only on the long line, so it is honoured there and suppressed elsewhere. The plan records this change as attempted and reverted once because it moved hdmaen_latch_test_2's golden. It does not this time: no framebuffer golden moved, the undisbeliever suite passes unchanged, and cross-validation is byte-identical. B4.16 is a weaker guard than its own doc claimed. Measured either side of the change both its readings are unchanged, including the HTIME = 330 one whose trigger dot moved 334 -> 333: the CPU takes an IRQ at an instruction boundary, so the handler-entry dot quantises to the spin loop's instruction length and a one-dot shift is absorbed. It can say "nothing regressed"; it cannot say "the change took effect". The new unit test does that, sweeping every HTIME and asserting equality with the old constant below the long dots and strict inequality above them. LONG_DOTS and the per-dot clock count move to rustysnes-ppu, which owns the dot model and now needs the layout twice; the scheduler delegates rather than keeping a second copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 29 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
Comment |
…ions The roadmap predicted the clock-domain conversion "would shift IRQ timing by ~2 clocks" and re-bless framebuffer goldens. It shifts nothing below the long dots and moved no golden; only HTIME 321..=337 changes. The plan predicted the change was "the likeliest thing under B1.05's residual". It is not, and B4.16 shows why directly: its HTIME = 330 trigger dot moved 334 -> 333 and its recorded reading did not move, because the handler-entry dot quantises to the spin loop's instruction length. B1.05 stays blocked on the same missing clock-domain instrument. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Antigravity review (Gemini via Ultra)This PR refactors H-IRQ comparator timing in Blocking issuesNone found. Suggestions
Nitpicks
Automated first-pass review by |
There was a problem hiding this comment.
Pull request overview
Moves the HV-timer horizontal IRQ (H-IRQ) trigger from a dot-based constant to a clock-derived compare, so the model remains correct in the presence of 6-clock “long dots”, while keeping behavior unchanged below the long-dot region.
Changes:
- Replace
HIRQ_TRIGGER_DELAYwith clock-domainhirq_match_clockand derivedhirq_trigger_dot, and update the HV-IRQ comparator logic accordingly. - Centralize the long-dot layout in
rustysnes-ppu(LONG_DOTS+dot_clocks) and haverustysnes-coredelegate to it. - Update docs and changelog, and add targeted unit coverage asserting unchanged behavior below long dots and changed behavior above them.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/scheduler.md | Updates scheduler/HV-IRQ documentation to describe the clock-domain compare and its interaction with long dots. |
| docs/ppu.md | Updates PPU timing documentation to reflect clock-domain HV-IRQ behavior and long-line dot-count bounding. |
| crates/rustysnes-ppu/src/lib.rs | Implements clock-domain H-IRQ mapping, exports long-dot timing helpers, updates comparator logic, and adds unit tests. |
| crates/rustysnes-core/src/bus.rs | Removes duplicated long-dot table and uses rustysnes_ppu::dot_clocks for dot duration. |
| CHANGELOG.md | Documents the behavioral/model change and its safety/verification notes under Unreleased. |
| // Walked rather than closed-form: the layout is two irregular dots in a 340-dot line, and a | ||
| // closed form would have to encode their positions a second time. `DOTS_PER_LINE + 1` covers | ||
| // the long line's extra dot; the loop is const-evaluable and runs at most 341 steps. |
| for htime in 321..=336u16 { | ||
| assert!( | ||
| hirq_trigger_dot(htime, false) < htime + 4, | ||
| "HTIME {htime} is past the long dots and must now fire EARLIER than the old constant said, not at the same dot" |
The last of the
v1.29.0dot-model residuals, and the one the plan records as "attempted and reverted because it moveshdmaen_latch_test_2's golden". It does not, this time.What was wrong
HIRQ_TRIGGER_DELAY = 4was a dot-domain rounding of ares'hcounter(10) == (HTIME+1)<<2(sfc/cpu/irq.cpp,sfc/cpu/io.cpp) — exact only while every dot is four clocks, which stopped being true when dots 323 and 327 became six. The match is now computed where it actually happens: clock4·HTIME + 14(hirq_match_clock), mapped to the first dot boundary at or after it (hirq_trigger_dot). The compare is in clocks; the six-clock dots do not move it, they move which dot contains it.Why nothing moved
4·HTIME + 14is never a multiple of 4, so below the long dots the next boundary isHTIME + 4— identical to the old constant. The two diverge only forHTIME321..=337:HTIME≤ 320HTIME + 4HTIME321..=335HTIME + 4HTIME336HTIME337That is why no framebuffer golden moves. Every H-IRQ a game arms in the visible window is below the long dots.
B4.16is a weaker guard than its own doc claimedThe plan says the guard comes first, and
B4.16already existed and is blessed. Measured either side of this change, both of its readings are unchanged — including theHTIME = 330one, whose trigger dot moved 334 → 333. The CPU takes an IRQ at an instruction boundary, so the handler-entry dot quantises to the spin loop's instruction length and a one-dot shift is absorbed.So
B4.16can say "nothing regressed", which is what a guard is for, but it cannot say "the change took effect".the_h_irq_dot_is_unchanged_below_the_long_dots_and_moves_above_themdoes that: it sweeps everyHTIME, asserting equality with the old constant below the long dots and strict inequality above them. The finding is recorded at bothhirq_trigger_dotand indocs/scheduler.mdso a future reader does not take a staticB4.16as evidence the model did not move.Also
LONG_DOTSand the per-dot clock count move torustysnes-ppu, which owns the dot model and now needs the layout in two places.rustysnes-core's scheduler delegates to it rather than keeping a second copy — two statements of "which dots are six clocks" is exactly the kind of fact that drifts.Verification
cargo fmt --check,cargo clippy --workspace --exclude rustysnes-android --all-targets -- -D warnings— clean.cargo test -p rustysnes-ppu -p rustysnes-core— 143 passed, 0 failed.cargo test -p rustysnes-test-harness --test undisbeliever_golden --features test-roms— passes unchanged. This is the suite holdinghdmaen_latch_test/hdmaen_latch_test_2.REF_PROJ=$PWD/ref-proj bash scripts/accuracysnes/crossval.sh—snes9x: OK (14 known),Mesen2: OK (2 known), 53 scenes match on both,2 reference(s) agree with the cart. Byte-identical to before the change.B4.16was measured by temporarily restoring the old constant, not inferred.🤖 Generated with Claude Code