Skip to content

perf(core): wire fast-exec through the scheduler — 1.53x on a real frame - #233

Merged
doublegate merged 3 commits into
mainfrom
perf/fast-exec-wiring
Jul 31, 2026
Merged

perf(core): wire fast-exec through the scheduler — 1.53x on a real frame#233
doublegate merged 3 commits into
mainfrom
perf/fast-exec-wiring

Conversation

@doublegate

Copy link
Copy Markdown
Owner

The number

1.53× on a real frame — the first result in this project that comes from changing what is computed rather than when.

A-B-A in one sitting, main + this branch, Super Mario 64, 120 timed frames after the VI comes up, examples/frame_bench.rs:

leg build mean frame
A1 accurate 100.582 ms
A2 accurate 99.847 ms
B1 --features fast-exec 64.822 ms
B2 --features fast-exec 65.321 ms
A3 accurate 99.712 ms
A4 accurate 99.598 ms

The legs do not overlap. Every A is above 99.5 ms and every B below 65.4 ms, while the four A readings span 0.99% — the ordinary within-session spread this repo records elsewhere. The gap is thirty times that.

Conservative pairing (best A over worst B), as docs/performance.md quotes throughout:

99.598 → 65.321 ms — 1.525×. 10.04 → 15.31 FPS.

The return legs earn their keep: two legs would have reported 1.55× from A1, and "measure A-B-A, not before/after" is in this repo because a two-leg comparison has been wrong here before.

Why the accurate baseline reads ~99.6 ms and not the 93.06 ms recorded in performance.md. That figure was measured with fast-scheduler enabled; its accurate pairing was 98.12 ms. So ~99.6 ms is that baseline plus cross-session drift, not a regression — and comparing a featured build against an unfeatured one is exactly the mistake the note now in performance.md exists to prevent.

What the wiring does

System::run_until_exec inverts who sets the pace. The CPU executes one instruction, reports what it cost in PCycles, master_ticks advances by that many CPU periods, and the RCP runs every one of its edges in the span that just elapsed. The CPU no longer lands on a derived edge — that is the relaxation, stated plainly.

ADR 0006 still holds. master_ticks remains the only incremented counter and every other position is still derived from it. What changed is how far it moves per step, not who owns it.

The divergence is measured, not asserted away

ADR 0013 §4 requires the divergence to be measured, bounded, and recorded before it ships. It is:

mode instructions retired, 120 frames
accurate 171,471,972
fast-exec 173,254,496
divergence +1.04%

Recorded as C-16 in docs/accuracy-ledger.md with its method, why it is above zero (the load-delay interlock the accurate path charges and this does not), what would narrow it, and when it must be re-measured. Both figures are stable to the digit across runs — the counter is deterministic, only the wall clock varies — so this is a reading, not a sample.

Three design points

  • It can land past target, by at most one instruction's cost, because a cost is only known after the instruction has run. Nothing drifts: the next call's target is absolute, so an overshoot means less work next time.
  • A halted CPU advances on RCP edges. A failed real-PIF boot checksum freezes the CPU while the RCP keeps running; with no instruction to time the advance, the loop steps to the next RCP edge. Deliberately not a bail-out — ADR 0011 §6 sanctions a test-only seam only where a boundary genuinely cannot be reached, and this one can simply be handled.
  • fast-exec therefore adds no new BailOut variant. Saying so plainly is better than inventing an exit to justify the ADR 0012 machinery; the enumeration exists so that a real one cannot be added silently.

Feature plumbing

run_frame now selects one of three entry points by cfg, with fast-exec taking precedence where both features are enabled — settled here per ADR 0013 §1 rather than left to whichever cfg happens to be written first. All four configurations build (neither, either, both).

full still excludes both, because promoting an alternate execution mode into a shipped artifact is an ADR decision rather than a build-configuration one.

CI gains five entries: core and frontend clippy with the feature, the both-features configuration, and the core no_std build. Clippy runs exactly once in this repo, so a cfg arm nobody compiles is a cfg arm that rots.

Verification

One guarded conditional, no pipes: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; the same for -p rustyn64-core, -p rustyn64-cpu and -p rustyn64-frontend with --features fast-exec; cargo test --workspace; cargo test -p rustyn64-cpu --features fast-exec; the fast-scheduler differential gate; RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps; the no_std builds; scripts/check_en_us.sh; pre-commit run markdownlint --all-files.

The accurate path is untouched by this PR — it adds a second entry point and changes nothing the default build reaches.

What is still open

60 FPS needs 16.67 ms; at 65.3 ms this is 3.92× short. The CPU pipeline was the largest single bucket and has now been addressed, so what remains is in buckets that were never the biggest. The next measurement is a fresh profile of the fast-exec frame — the shares in performance.md were taken on the accurate path and no longer describe what this build spends its time on. Guessing from stale shares is how this project has misallocated effort before.

🤖 Generated with Claude Code

Step 3 of task #64, and the first number from ADR 0013's second execution mode.

`System::run_until_exec` inverts who sets the pace: the CPU executes one
instruction, reports what it cost in PCycles, `master_ticks` advances by that
many CPU periods, and the RCP runs every one of its edges in the span that just
elapsed. The CPU no longer lands on a derived edge, which is the relaxation
stated plainly.

ADR 0006 STILL HOLDS. `master_ticks` is still the only incremented counter and
every other position is still derived from it; what changed is how far it moves
per step, not who owns it.

MEASURED, A-B-A in one sitting, Super Mario 64, 120 timed frames:

  A 100.582 / 99.847   B 64.822 / 65.321   A 99.712 / 99.598

The legs do not overlap — every A above 99.5 ms, every B below 65.4, while the
four A readings span 0.99%, the ordinary within-session spread. Conservative
pairing (best A over worst B): 99.598 -> 65.321 ms, **1.525x**, 10.04 -> 15.31
FPS.

The return legs matter for the usual reason: two legs would have reported 1.55x
from A1, and this project has been wrong that way before.

Note the accurate baseline is ~99.6 ms, not the 93.06 ms recorded earlier —
that figure was measured WITH `fast-scheduler` on, and its accurate pairing was
98.12 ms. Comparing a featured build against an unfeatured one is the mistake
the note in docs/performance.md exists to prevent.

THE DIVERGENCE IS MEASURED AND RECORDED, as ADR 0013 section 4 requires: the two
modes retire 173,254,496 vs 171,471,972 instructions over the same 120 frames,
+1.04%. Ledger C-16 carries the method, why it is above zero (the load-delay
interlock the accurate path charges and this does not), and what would move it.

Three design points, each documented where it lives:

- It can land PAST `target`, by at most one instruction's cost, because a cost
  is only known after the instruction has run. Nothing drifts: the next call's
  target is absolute.
- A HALTED CPU advances on RCP edges. A failed real-PIF boot checksum freezes
  the CPU while the RCP keeps running; with no instruction to time the advance,
  the loop steps to the next RCP edge. Deliberately NOT a bail-out — ADR 0011
  section 6 sanctions a test-only seam only where a boundary genuinely cannot be
  reached, and this one can simply be handled.
- `fast-exec` therefore adds NO new BailOut variant. Saying so is better than
  inventing an exit to justify the machinery.

`run_frame` now picks one of three entry points, with fast-exec taking
precedence where both features are on (ADR 0013 section 1), settled here rather
than left to whichever cfg is written first. All four configurations build.
`full` still excludes both, because promoting an execution mode into a shipped
artifact is an ADR decision rather than a build-configuration one.

CI gains five entries (core + frontend clippy, the both-features configuration,
the core no_std build), because clippy runs exactly once and a cfg arm nobody
compiles is a cfg arm that rots.

Gates: fmt, clippy (workspace + every feature combination), test --workspace,
the fast-exec CPU gate, the fast-scheduler differential gate, rustdoc, no_std,
en-US, markdownlint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@doublegate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 774f0fc7-4d6f-4b09-89ca-fc565f7fc21d

📥 Commits

Reviewing files that changed from the base of the PR and between 10dadcd and c6727e2.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • crates/rustyn64-core/src/scheduler.rs
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added an optional instruction-granular execution mode for improved frame-processing performance.
    • Frame execution now supports selecting the new mode alongside existing accurate and fast-scheduling options.
    • Combined execution options use the instruction-granular mode as the priority path.
  • Documentation

    • Documented execution behaviour, timing characteristics, performance results and known instruction-count differences.
    • Added guidance on benchmarking and interpreting accuracy results.
  • Tests

    • Expanded continuous integration validation across core, frontend and no-standard builds.

Walkthrough

The PR adds a feature-gated fast-exec mode. Core execution steps CPU instructions using PCycle timing, frontend frame execution selects the mode, CI validates feature combinations, and documentation records measured timing and performance results.

Changes

Fast-exec execution mode

Layer / File(s) Summary
Feature contracts and frontend selection
crates/rustyn64-core/Cargo.toml, crates/rustyn64-core/src/lib.rs, crates/rustyn64-frontend/Cargo.toml, crates/rustyn64-frontend/src/emu.rs
The core and frontend expose fast-exec. The fastpath module supports both fast modes. run_frame gives fast-exec precedence over fast-scheduler.
Instruction-granular scheduler
crates/rustyn64-core/src/scheduler.rs, crates/rustyn64-core/src/fastpath.rs, crates/rustyn64-core/tests/fast_exec_scheduler.rs, crates/rustyn64-core/tests/fast_scheduler_differential.rs
System::run_until_exec steps instructions, advances master_ticks by PCycle costs, processes RCP edges, handles boot-NMI halts, permits one-instruction overshoot, and returns FastRunReport with mode-specific work_units.
Build validation and measured records
.github/workflows/ci.yml, docs/scheduler.md, docs/accuracy-ledger.md, docs/performance.md
CI builds the new feature combinations and the bare-metal target. Documentation records scheduling behaviour, the 1.04% retirement divergence, and benchmark measurements.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant run_frame
  participant System_run_until_exec
  participant CPU
  participant RCP
  run_frame->>System_run_until_exec: execute frame target
  System_run_until_exec->>CPU: step instruction
  CPU-->>System_run_until_exec: return PCycle cost
  System_run_until_exec->>RCP: process elapsed RCP edges
  System_run_until_exec-->>run_frame: return FastRunReport
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 9 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Changelog Entry For User-Visible Changes ⚠️ Warning fast-exec adds a user-visible execution mode and System::run_until_exec, but CHANGELOG.md has no fast-exec entry under [Unreleased]. Add a concise fast-exec entry under the [Unreleased] section, under Added or Changed, describing the user-visible feature.
✅ Passed checks (9 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the required Conventional Commits format, uses an imperative subject, has no trailing period, and describes the main scheduler change.
Description check ✅ Passed The description directly covers the scheduler integration, benchmark results, feature configuration, tests, and documented accuracy divergence.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Oracle Number Is Stated ✅ Passed The PR documents that fast-exec preserves architectural state at instruction retirement boundaries (cpu.md: "executes the same instruction stream through the same semantics"), changing only timing...
Docs-As-Spec Sync ✅ Passed The PR changes observable rustyn64-core scheduling and updates docs/scheduler.md with the run_until_exec contract, overshoot, RCP catch-up, halted CPU, and BailOut behaviour.
Measured, Never Tuned ✅ Passed All new timing values are either pre-existing constants tested for exactness, cited to manual pages (UM §, PIF-NUS.md), or recorded in docs/accuracy-ledger.md with measurement method (C-16: +1.04%...
Unsafe Stays Out Of The Chip Crates ✅ Passed No unsafe code introduced in chip crates or rustyn64-core. All forbid(unsafe_code) attributes are present and unchanged. Frontend modifications use cfg-gated conditional compilation only.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 114-121: Add a CI step alongside the existing rustyn64-core clippy
command to run cargo test for rustyn64-core with the fast-exec feature enabled.
Ensure the test target covers System::run_until_exec, including RCP-edge,
overshoot, and halted-CPU paths; add feature-specific scheduler tests only if
those paths are not already covered.

In `@crates/rustyn64-core/src/scheduler.rs`:
- Around line 576-580: Make Scheduler::next_edge_after fallible when no
representable later edge exists, avoiding tick + 1 overflow at u64::MAX. Update
every caller, including the RCP loop using self.step_rcp, to stop processing
when edge discovery returns no value while preserving existing edge handling for
valid results.
- Around line 545-565: Update FastRunReport usage in scheduler.rs around
run_until_exec so blocks remains a count of whole scheduler periods, while
instruction execution increments a separately named instruction counter; update
all report construction and consumers as needed to preserve consistent public
units across run_until_fast and run_until_exec. In
crates/rustyn64-core/src/lib.rs lines 27-29, revise the module rustdoc to
document the explicit report units.

In `@docs/scheduler.md`:
- Around line 343-345: Update the comparison table’s fast-exec entry to state
that ADR 0006 remains unchanged, removing the claim about per-domain deficit
counters. Keep the surrounding documentation consistent with the implementation
and the later statement that master_ticks remains the sole incremented counter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 319c30d0-eda9-414c-83c9-03119bcba1b9

📥 Commits

Reviewing files that changed from the base of the PR and between ae38935 and 234f1d2.

📒 Files selected for processing (9)
  • .github/workflows/ci.yml
  • crates/rustyn64-core/Cargo.toml
  • crates/rustyn64-core/src/lib.rs
  • crates/rustyn64-core/src/scheduler.rs
  • crates/rustyn64-frontend/Cargo.toml
  • crates/rustyn64-frontend/src/emu.rs
  • docs/accuracy-ledger.md
  • docs/performance.md
  • docs/scheduler.md

Comment thread .github/workflows/ci.yml
Comment thread crates/rustyn64-core/src/scheduler.rs Outdated
Comment thread crates/rustyn64-core/src/scheduler.rs
Comment thread docs/scheduler.md
doublegate and others added 2 commits July 31, 2026 10:40
…ccessor

All four adopted; the third is the one worth reading.

1. CI LINTED THE CORE WIRING WITHOUT EXECUTING IT. `run_until_exec` had no test
   at all -- the CPU crate's differential gate cannot reach it. Added
   crates/rustyn64-core/tests/fast_exec_scheduler.rs (5 tests: engagement, the
   bounded overshoot swept across an RCP period, the no-op, the RCP catch-up, and
   both modes honoring the same target contract) plus the missing
   `cargo test -p rustyn64-core --features fast-exec` CI entry.

2. `FastRunReport::blocks` MEANT TWO DIFFERENT THINGS. `run_until_fast` counts
   whole edge periods; `run_until_exec` counted instructions. Same public field,
   two units, and a consumer could not read it consistently. Renamed to
   `work_units`, with a table naming the unit per mode and an explicit "do not
   compare across modes" -- it answers ADR 0012's *did it engage*, for which any
   positive count means the same thing, and a magnitude comparison between two
   units means nothing.

3. THE RCP CATCH-UP TEST WAS VACUOUS, and mutation is what showed it. The first
   version asserted `System::rcp_cycles` advanced. Deleting the entire RCP
   catch-up loop left it GREEN -- because `rcp_cycles` is a DERIVED ACCESSOR off
   `master_ticks` (ADR 0006's whole point), so it advances whether or not a
   single RCP step ran.

   The witness has to be RETAINED state. `VI_V_CURRENT` is incremented by
   `Vi::tick`, which only `step_rcp` calls, so it stays put if the RCP never
   steps. The mutation now fails with exactly that message.

   The general lesson, recorded in the test: in a codebase that derives every
   position from one counter, most of the obvious progress indicators cannot
   witness that work happened. Ask what is STORED, not what is REPORTED.

4. RCP EDGE-SEARCH OVERFLOW. `saturating_add` could put `end` at u64::MAX
   WITHOUT the machine ever having run there, and `next_edge_after` then
   evaluates `tick + 1` at u64::MAX -- a panic in debug, and in release a wrap to
   a low tick that keeps `rcp <= end` true forever. That is the difference from
   the accurate loop, whose top-of-range is unreachable because reaching it means
   emulating three thousand years. Now `checked_mul` + `checked_add`, and
   overflow ends the run.

Also: docs/scheduler.md's comparison table claimed `fast-exec` amends ADR 0006
with per-domain deficit counters. The implementation has none -- that is the
deficit-counter scheduler, still ahead. Corrected to say ADR 0006 is unchanged
AS IMPLEMENTED, with the authorization falling due when the counters arrive.

The halted-CPU branch is still untested, and the test file records why rather
than leaving a hole: `boot_nmi_halt` is latched only by a real-PIF boot with a
failing IPL2 checksum, which needs images that are never committed. A test-only
seam on `Bus` for one three-line branch is not what ADR 0011 section 6 sanctions.

Gates: fmt, clippy (workspace + every feature), test --workspace, both feature
test suites, the CPU gate, rustdoc, no_std, en-US, markdownlint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Antigravity flagged a hang if `step_instruction_at` ever returns 0: `end`
stays at `master_ticks`, no RCP edge steps, and the outer loop spins.

It is NOT reachable today -- `Pipeline::step_instruction` charges one PCycle to
issue before adding anything -- but that guarantee lives in ANOTHER CRATE, and a
reader of this loop cannot see it. The hazard is the remoteness, not the
absence, and the finding is right on those grounds.

A `debug_assert` is the real check. The `max(1)` beside it is a floor so a
release build makes progress rather than hanging; it is deliberately not a fix,
since a zero cost is a defect either way, but running one PCycle fast is a far
better way to report a defect than a hang.

Also adds the `cargo test -p rustyn64-frontend --features fast-exec` CI entry.
The core one and the FastRunReport unit documentation were already in 10dadcd,
which the review predates.

Gates: fmt, clippy (workspace + every feature), test --workspace, both feature
suites, the frontend feature suite, rustdoc, en-US, markdownlint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@doublegate

Copy link
Copy Markdown
Owner Author

Adjudication of the Antigravity (Gemini) review. One blocking finding, three suggestions — answered in c6727e2 and 10dadcd.

BLOCKING — infinite loop on a zero-cost instruction. Adopted, and the finding is right for a reason slightly different from the one given.

A cost == 0 is not reachable today: Pipeline::step_instruction charges one PCycle to issue before adding anything, and every path adds to that. So there is no live bug. But that guarantee lives in another crate, and a reader of this loop cannot see it — the loop's termination depends on an invariant nothing here states or checks. That is the hazard, and it is the same shape as this repo's "a comment stating a rule is not an implementation of it", one step worse: there was not even a comment.

Now:

debug_assert!(cost > 0, "an instruction cost 0 PCycles, which cannot happen");
let Some(end) = u64::from(cost.max(1))

The debug_assert is the real check. The max(1) is a floor so a release build makes progress rather than hanging — deliberately not a fix, since a zero cost would be a defect either way, but running one PCycle fast is a far better way to report a defect than a hang.

Suggestion — CI missing cargo test for the feature on core and frontend. Already adopted for core in 10dadcd (which this review predates), and the frontend entry is added now. Both were real gaps: linting a cfg arm without executing it is the "gate that never runs" shape this repo has been bitten by before.

Suggestion — FastRunReport.blocks semantic overload. Already adopted in 10dadcd, and more thoroughly than documenting it: the field is renamed to work_units, with a table naming the unit per mode (fast-scheduler = one edge period, fast-exec = one instruction) and an explicit instruction not to compare it across modes. "Blocks" reads as a single unit, which is what invited the misreading; the rename removes the invitation rather than annotating it.

Nitpick — no tests validating RCP stepping and termination. Already adopted in 10dadcd: crates/rustyn64-core/tests/fast_exec_scheduler.rs, five tests. Worth flagging one thing that came out of writing them — the first version of the RCP test asserted System::rcp_cycles advanced, and deleting the entire RCP catch-up loop left it green, because rcp_cycles is a derived accessor off master_ticks (ADR 0006's whole point). The witness had to be retained state: VI_V_CURRENT, which only step_rcp increments. The mutation now fails with exactly that message.

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

Wires the fast-exec instruction-granular execution mode through rustyn64-core's scheduler and rustyn64-frontend's EmuCore, refactoring FastRunReport to track generalized work units and adding documentation and CI coverage.

Blocking issues

  • crates/rustyn64-core/src/fastpath.rs:198: Renaming public field pub blocks: u64 to pub work_units: u64 on pub struct FastRunReport is a breaking change to rustyn64-core's public API without a corresponding version bump in Cargo.toml. Per the project style guide, breaking public API changes require a crate version bump or a backwards-compatible accessor/deprecation strategy.

Suggestions

  • crates/rustyn64-core/src/scheduler.rs:570: In run_until_exec, using cost.max(1) in release builds silently masks potential CPU instruction bugs returning zero cost by forcing a 1-PCycle advance. If zero cost indicates an invalid state or CPU pipeline defect, consider logging or handling the condition explicitly rather than allowing release builds to silently swallow the invariant violation.
  • crates/rustyn64-core/src/scheduler.rs:555: The boot_nmi_halt() branch in run_until_exec does not increment report.work_units. If an execution period is spent entirely halted, report.engaged() will return false even though run_until_exec executed to completion without bailing out to the accurate scheduler.

Nitpicks

  • crates/rustyn64-core/src/scheduler.rs:549: use crate::fastpath::FastRunReport; inside the method body of run_until_exec is redundant because crate::fastpath::FastRunReport is already referenced directly in the public function signature.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
crates/rustyn64-core/src/scheduler.rs (1)

585-602: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

RCP catch-up loop can still overflow next_edge_after at u64::MAX.

The checked_mul/checked_add guard on end (lines 585-590) is correct, but it only bounds the outer computation. end can legitimately equal u64::MAX. When rcp == end == u64::MAX, the loop body at line 601 still calls Self::next_edge_after(rcp, ...), which computes tick + 1 unchecked and overflows — a panic in debug, a wrap to a low tick in release.

The comment on lines 598-600 states this "cannot be asked for an edge past u64::MAX" because "the loop condition fails at or before it." That claim is incorrect: the while rcp <= end condition is checked before the iteration that computes the overflowing value, not after it, so next_edge_after(u64::MAX, ...) is reachable when end == u64::MAX. Treat the comment and the code as independent claims — the comment does not match what the code actually does.

This is the same class of hazard the prior review raised for this function ("Prevent RCP edge-search overflow... Apply that contract to all callers of next_edge_after"). That request was only partially completed: end's derivation is now checked, but next_edge_after itself remains unchecked at the point where it matters.

As per path instructions, "A comment that asserts what the code does but disagrees with it... this project has been bitten by that four times and no test failed any of them."

🛡️ Proposed minimal guard
             let mut rcp = Self::next_edge_after(self.master_ticks, self.phases.rcp, RCP_DIVIDER);
             while rcp <= end {
                 self.master_ticks = rcp;
                 self.step_rcp();
-                // `end` is a real tick because it came from `checked_add` above, so
-                // this cannot be asked for an edge past `u64::MAX`: the loop
-                // condition fails at or before it.
-                rcp = Self::next_edge_after(rcp, self.phases.rcp, RCP_DIVIDER);
+                // `next_edge_after` overflows if asked for the edge after
+                // `u64::MAX` itself; stop rather than compute it.
+                if rcp == u64::MAX {
+                    break;
+                }
+                rcp = Self::next_edge_after(rcp, self.phases.rcp, RCP_DIVIDER);
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustyn64-core/src/scheduler.rs` around lines 585 - 602, Update the RCP
catch-up loop in the scheduler around next_edge_after so it does not call that
helper when rcp is already u64::MAX; terminate the loop after processing the
final valid edge. Remove or revise the existing comment asserting overflow is
impossible so it accurately describes the guard, while preserving normal
catch-up behavior for all other ticks.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@crates/rustyn64-core/src/scheduler.rs`:
- Around line 585-602: Update the RCP catch-up loop in the scheduler around
next_edge_after so it does not call that helper when rcp is already u64::MAX;
terminate the loop after processing the final valid edge. Remove or revise the
existing comment asserting overflow is impossible so it accurately describes the
guard, while preserving normal catch-up behavior for all other ticks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2da1a839-f541-48cb-a1fb-6690932ea029

📥 Commits

Reviewing files that changed from the base of the PR and between 234f1d2 and 10dadcd.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • crates/rustyn64-core/src/fastpath.rs
  • crates/rustyn64-core/src/scheduler.rs
  • crates/rustyn64-core/tests/fast_exec_scheduler.rs
  • crates/rustyn64-core/tests/fast_scheduler_differential.rs
  • docs/scheduler.md

@doublegate
doublegate merged commit 81c4359 into main Jul 31, 2026
12 checks passed
@doublegate
doublegate deleted the perf/fast-exec-wiring branch July 31, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant