Skip to content

docs(perf): anatomize the remaining in-model targets, and pin Latch's size in code - #218

Merged
doublegate merged 18 commits into
mainfrom
docs/latch-copy-anatomy
Jul 30, 2026
Merged

docs(perf): anatomize the remaining in-model targets, and pin Latch's size in code#218
doublegate merged 18 commits into
mainfrom
docs/latch-copy-anatomy

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Motivation

ADR 0011 recorded the pipeline latch copies as "~16% of total runtime" and "not safely removable", with a 1.19x ceiling — from a perf annotate view. That was enough to rule the target out, and not enough to say which copies were the problem.

Per-line attribution of the post-6a6adfa render capture, plus size_of on the struct, says something more specific — and it changes what a future attempt should aim at.

What the measurement shows

Six copy sites in pipeline.rs total 14.66% of the frame (plus 3.77% inlined with no line, so ~15–18% — about 19–22 ms of 125, more than the whole VI scan-out cost after the memo).

Latch is 120 bytes, and its fields sum to exactly 120 — so ADR 0011's "zero-padding-optimal" claim holds, measured rather than assumed:

field bytes
occupied, pc, word, in_delay_slot, rs_val, rt_val 30
decoded 16
abort: Option<Exception> 2
write_back 24
mem: Option<MemOp> 24
cop0: Option<Cop0Access> 24

The new part: those last three — 72 of the 120 bytes — are produced at EX. In ic_rf and rf_ex they are structurally always None / WriteBack::None. So those two latches copy 72 bytes of provably-empty payload, twice each per cycle: 6.01% of the frame moving nothing.

That is not the hazard ADR 0011 ruled out. That hazard is specific to the DC path — dc_stage's error branch re-reads self.ex_dc after abort_with has stamped it, entangling the :848 / :2061 / :913 copies with abort propagation. The upstream pair has no such entanglement.

What this PR deliberately does NOT do

It does not split the struct. The finding is recorded as an untested hypothesis with an upper bound of 1.064x, and left for after the dispatch question is settled — 1.064x against a 7.5x gap is not the thing to spend the next session on, and saying so with a number is better than leaving the target unexamined.

It also carries the warning any attempt needs: this repository records four pipeline changes that compiled, passed every test, and did nothing, so this one needs the CPU golden-log 0-diff and n64-systemtest rather than cargo test.

Verification

Documentation only. scripts/check_en_us.sh and pre-commit run markdownlint --all-files pass. The sizes come from a temporary size_of probe in rustyn64-cpu (this crate is no_std, so the values were surfaced through a deliberately failing assert_eq!); the probe was removed and cargo test -p rustyn64-cpu --lib is green with the tree back to main's content.

…bly-empty bytes

ADR 0011 recorded the latch copies as ~16% of runtime and "not safely
removable", from a perf annotate view. Per-LINE attribution of the post-6a6adfa
capture, plus size_of on the struct, says something more specific.

Six copy sites in pipeline.rs total 14.66%, and Latch is 120 bytes whose fields
sum to exactly 120 -- so 0011's zero-padding claim holds. But 72 of those bytes
are write_back, mem and cop0, all produced at EX. In ic_rf and rf_ex they are
structurally always empty, so those two latches copy 72 bytes of nothing, twice
each per cycle: 6.01% of the frame.

That is NOT the hazard 0011 ruled out. That one is specific to the DC path,
where dc_stage's error branch re-reads ex_dc after abort_with stamps it. The
upstream pair carries no such entanglement.

Recorded as an untested hypothesis with an upper bound of 1.064x, and
deliberately not acted on: it is worth doing after the dispatch question is
settled rather than instead of it, and any attempt needs the golden-log 0-diff
and n64-systemtest, not just cargo test -- this repository records four pipeline
changes that compiled, passed everything, and did nothing.
@coderabbitai

coderabbitai Bot commented Jul 30, 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: 39 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: 19feeef0-2110-4514-8d26-56553e581bbb

📥 Commits

Reviewing files that changed from the base of the PR and between f3cd85a and e084c09.

📒 Files selected for processing (1)
  • crates/rustyn64-cpu/src/pipeline.rs
📝 Walkthrough

Summary by CodeRabbit

  • Documentation
    • Expanded the performance report with new measured deep dives covering pipeline latch-copy costs and bus split-borrow memory traffic.
    • Added a detailed latch-copy site breakdown by pipeline stage, documented the current latch size assumptions, and included unverified optimisation hypotheses and projected speedups.
  • Bug Fixes
    • Added a build-time safeguard to prevent accidental changes to the pipeline latch layout that could impact emulation performance.

Walkthrough

The change adds compile-time layout guards for Latch and expands performance documentation with measured latch-copy and Bus split-borrow analyses, including quantified costs and untested optimisation hypotheses.

Changes

Performance analysis

Layer / File(s) Summary
Latch layout guard and copy analysis
crates/rustyn64-cpu/src/pipeline.rs, docs/performance.md
The pipeline checks Latch padding and ABI-dependent size assumptions, while the performance document details latch-copy attribution, field sizes, correctness constraints, and potential split-latch gains.
Bus split-borrow analysis
docs/performance.md
The document quantifies mem::take and mem::replace traffic in Bus tick paths and records optimisation hypotheses involving request returns and device idle predicates.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • doublegate/RustyN64#217: Both changes extend docs/performance.md with measured performance and latch-related cost analyses.
🚥 Pre-merge checks | ✅ 8 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning Violates the Conventional Commits title rule: it is 82 characters, exceeding the 72-character limit. Shorten it to 72 characters or fewer while keeping the type(scope): imperative subject form.
Measured, Never Tuned ⚠️ Warning Violates docs/accuracy-ledger.md’s provenance rule: the new 120/128-byte and 5.32%/6.01%/1.35 GB figures are neither cited nor ledgered. Add each new constant/timing value to docs/accuracy-ledger.md with measurement method, or cite a manual/wiki source inline.
✅ Passed checks (8 passed)
Check name Status Explanation
Description check ✅ Passed The description is clearly about the latch-layout and performance-documentation changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 PASS — this PR is docs-only plus a compile-time latch-layout guard; the new performance hypotheses are explicitly marked unmeasured, and docs/STATUS.md holds the oracle count.
Docs-As-Spec Sync ✅ Passed pipeline.rs only adds compile-time layout assertions/comments, and docs/performance.md was updated in the same PR; no unchecked runtime behaviour change.
Changelog Entry For User-Visible Changes ✅ Passed No user-facing feature/fix is introduced; the PR is docs/internal guard work, so the Unreleased changelog rule is not triggered.
Unsafe Stays Out Of The Chip Crates ✅ Passed No unsafe code was added in the diff, and the chip/core crates still carry #![forbid(unsafe_code)].

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

core::mem::replace is 5.32% of the frame on its own (mem/mod.rs:930 at 4.21%,
:929 at 1.11%), about 6.7 ms of 125, and all of it is rdp_tick and audio_tick
taking their chip out and putting it back.

Rdp is 344 bytes and Audio is 88, size_of-measured, so each tick touches ~3x
size_of and the scheduler runs both EVERY RCP step -- ~1.04 M a frame, or
~1.35 GB moved to satisfy the borrow checker.

The fix pattern is already here: Bus::rsp_tick used to do this and no longer
does, and its comment records that the take was worse than the "no allocation"
claim above it -- take needs Default, and constructing an Rsp allocated 8 KiB of
DMEM and IMEM every RCP step. Rdp and Audio were never converted.

Recorded with both candidate routes as hypotheses and an upper bound of 1.056x.
Not acted on: the "take only when there is work" route turns on an exact idle
predicate, which is a claim about the RDP state machine rather than about
performance, and the Angrylion vectors plus the audio goldens are what would
catch it being wrong.
…e padding question

Antigravity review of PR #218.

Line numbers drift, so each site now carries its enclosing function and its
statement, with the line marked as being from the capture. Verifying those
attributions found two of my own wrong: 2061 is in ex_stage rather than
dc_stage, and 913 is in dc_stage rather than wb_stage. Grouped by stage the
shares are ex_stage 6.26%, dc_stage 4.97%, ic_stage 2.15%, rf_stage 1.28%.

On padding: size_of::<Latch>() INCLUDES alignment padding and the component
figures are each size_of of that component, so their summing to the whole is
exactly the evidence that none is left over -- the six scalars occupy 30 bytes,
which is also their naive sum, so the two bools sit in gaps rather than being
padded out. There is no field ordering that makes this struct smaller.
@doublegate

Copy link
Copy Markdown
Owner Author

All three adopted — and checking the first one found two attributions of mine were wrong.

1. "Hardcoded line numbers will drift; reference function or stage names" — ADOPTED, and it caught a defect.

Each site now carries its enclosing function and its statement, with the line number marked as being from the capture. Verifying those function names against the file found two of mine were wrong: :2061 is in ex_stage, not dc_stage, and :913 is in dc_stage, not wb_stage. Both are now right, and the table gains a per-stage grouping — ex_stage 6.26%, dc_stage 4.97%, ic_stage 2.15%, rf_stage 1.28% — which is the durable form of the finding and reads better than six sites anyway.

2. "The field table sums to 120 without accounting for alignment padding" — ADOPTED, and the summing is the padding evidence.

Stated explicitly now: size_of::<Latch>() includes alignment padding, and each component figure is size_of of that component. So the fact that they account for the whole 120 is precisely what shows none is left over. The six scalars occupy 30 bytes, which is also their naive sum (1 + 8 + 4 + 1 + 8 + 8), so repr(Rust) has placed the two bools in gaps rather than padding them out — there is no field ordering that makes this struct smaller.

3. Nitpick: "separate the line numbers into their own column" — ADOPTED, they have one.


Also added since you last saw this branch: a second measured section on the Bus split-borrow. core::mem::replace is 5.32% of the frame, and Rdp (344 B) plus Audio (88 B) are moved three times each per RCP step — about 1.35 GB a frame to satisfy the borrow checker. Bus::rsp_tick already had this defect and was fixed; rdp_tick and audio_tick were not converted.

Antigravity round two on PR #218. The reviewer asks whether 1.064x and 1.056x
are cumulative or isolated -- they are isolated, and they address disjoint
shares (6.01% and 5.32%), so collecting both entirely gives 1/(1-0.1133) =
1.128x: 125.24 ms down to ~111 ms, or 9.0 FPS, against a 7.5x gap. Saying so
makes the case for sequencing them after the dispatch question stronger than
leaving two unrelated-looking numbers on the page.

Also disambiguates that mem/mod.rs is the Rust standard library inside the
toolchain rather than a file in this repository, and swaps a unicode ellipsis
inside a code span for ASCII.
@doublegate

Copy link
Copy Markdown
Owner Author

Round two, all three adopted — and the nitpick was the most useful of them.

1. "Unicode ellipsis inside a code span" — ADOPTED, ASCII ... now.

2. "Disambiguate whether core/src/mem/mod.rs:930 is stdlib or a local file" — ADOPTED. A real ambiguity, and the path shape genuinely looks like it could be crates/rustyn64-core/src/mem/mod.rs. It now says "lines 930 and 929 of the Rust standard library's library/core/src/mem/mod.rs, inside the toolchain, not a file in this repository".

3. Nitpick: "are 1.064x and 1.056x cumulative or isolated?" — ADOPTED, and composing them sharpens the section's whole point.

Isolated, and they address disjoint shares — 6.01% for the latch payload, 5.32% for the split-borrow — so both collected entirely give 1 / (1 - 0.1133) = 1.128x: 125.24 ms down to about 111 ms, or 9.0 FPS.

Against a 7.5x gap. Two unrelated-looking numbers on a page invite someone to hope they add up to something; stated together they make the sequencing argument themselves — these are worth doing after the dispatch question, not instead of it.

Antigravity round three on PR #218: the latch table lost its filename when the
sites were regrouped by function, and the split-borrow section named its
functions without saying which file they are in. Both carry repo-relative paths
now, and rsp_tick -- the one already converted -- is located too, since it is the
thing a reader would want to open next.
@doublegate

Copy link
Copy Markdown
Owner Author

Round three, both adopted. Both are the same defect in two places, and it was introduced by adopting your previous round.

1. "The split-borrow section omits repo-relative paths for rdp_tick / audio_tick" — ADOPTED. Named now: crates/rustyn64-core/src/bus.rs, lines 539 and 548 at the time of writing. Bus::rsp_tick — the one already converted, and the thing a reader would want to open next — is located too, at line 519 of the same file.

2. Nitpick: "the stage table omits the filename" — ADOPTED. crates/rustyn64-cpu/src/pipeline.rs, stated above the table.

Worth noting how that one happened: the table had the path, in the column header, and lost it when I restructured the columns to adopt your round-one suggestion about function names. A tidy-up that quietly drops information is the failure mode of tidy-ups, and it is the second time in this batch that adopting a suggestion cost something elsewhere — both caught by the next round, which is a reasonable argument for the loop.

…e ones with nothing in them

The section named the two concentrated targets and left a reader to wonder about
the RSP's 11.4%. Per-line it has no target: the hottest attributable line is
0.65% and the largest entry is 1.17% of inlined code with no line, so it is
thinly spread instruction execution -- which is why ADR 0011 scopes the fast
path to the VR4300 first. Recording the null result is what makes the
enumeration a claim rather than a selection.
@doublegate

Copy link
Copy Markdown
Owner Author

One addition since the last round, unprompted, because the section was making a claim it had not earned.

It named the two concentrated targets — the latch payload and the split-borrow — and said nothing about the RSP's 11.4%, leaving a reader to assume it had been skipped. Per line, it has no target: the hottest attributable line is 0.65%, and the largest single entry is 1.17% of inlined code with no line at all. It is thinly spread instruction execution, which is exactly why ADR 0011 scopes the fast path to the VR4300 first and leaves the RSP for later. The remainder of the CPU's 41.0%, once the 14.66% of latch copying is set aside, is the same shape.

The section now says so. Recording the null result is what turns "here are the two targets" into "every bucket above 3% was examined and these two are what is in them" — a claim rather than a selection, and the difference matters for a document whose job is to stop the next person re-deriving this.

…ing conflated

Antigravity round four on PR #218, and the reviewer caught an inconsistency of
mine: the text said the split cuts the two upstream copies by 60% -- the share
of bytes those latches carry for nothing -- and then quoted 1.064x, which
assumes the whole 6.01% disappears. Those are different claims.

60% of 6.01% is 3.61%, so the EXPECTED gain is 1.037x. The 1.064x is the ceiling
if those copies vanished outright, which nothing proposed here achieves, since
the front half still has to move. Both are stated, labelled, and composed with
the split-borrow figure in a table: expected 1.098x (~114 ms, 8.8 FPS), ceiling
1.128x (~111 ms, 9.0 FPS). Against a 7.5x gap either way.

The snippet also notes that take needs Rdp: Default and writes a fresh default
in place, which is why it is a read AND a write rather than a move.
@doublegate

Copy link
Copy Markdown
Owner Author

Round four. Two adopted, one rejected — and the first is a real inconsistency of mine.

1. "The text says a 60% cut but the bound assumes 100% elimination" — ADOPTED. They were two different claims wearing one number.

The 60% is the share of bytes those two latches carry for nothing (72 of 120). The 1.064x assumed the whole 6.01% disappears. Both are now stated and labelled, with your arithmetic:

  • expected: 60% of 6.01% is 3.61%, so 1 / (1 - 0.0361) = 1.037x
  • ceiling: 1 / (1 - 0.0601) = 1.064x, which nothing proposed here achieves, because the front half still has to move

Composed with the split-borrow figure in a table rather than in prose:

latch split split-borrow together
expected 1.037x 1.056x 1.098x → ~114 ms, 8.8 FPS
ceiling 1.064x 1.056x 1.128x → ~111 ms, 9.0 FPS

Against a 7.5x gap either way — which is the conclusion, and it survives the correction. But an optimistic figure standing next to the mechanism that contradicts it is exactly what this document exists to prevent.

2. "The snippet does not indicate Rdp: Default" — ADOPTED. The comment now says take needs Rdp: Default and writes a fresh default in place of the value it hands back — which is why it is a read and a write rather than a move, and that is the whole cost being described.

3. Nitpick: "replace exact line numbers with offsets or function names" — REJECTED, they already are, and the numbers earn their place.

Every site carries its enclosing function, its statement, and its file, adopted from your round-one and round-three suggestions; the line number is an extra column explicitly marked "at capture". Removing it would break the one thing it is for: re-deriving these shares from the retained perf capture, which is keyed on line numbers. A stale line number next to a function name and a statement costs a reader seconds; not being able to reproduce the table costs them the measurement.

…uition

Antigravity round five on PR #218 asked which RDP state an idle predicate must
validate. Reading Rdp::tick's own early-outs answers it exactly: the tick
returns having touched nothing in two cases, both pure reads of self --
status & (FREEZE|XBUS) != 0, and cmd_current >= cmd_end.

The other two do not qualify and it matters why. stall > 0 decrements stall, so
it is not idle -- but it does not need the bus either, so it can be handled
before the take rather than being a reason to keep it. The partially-written
multi-word case is NOT decidable without the bus, because it reads the opcode
from RDRAM to learn the length, so that one must still take.

Audio::tick is different: it writes self.last_tick = now unconditionally on
entry, so it is never idle by this definition and needs the return-a-request
route instead. That is the kind of asymmetry a predicate written from intuition
would have missed.

Also marks the bus.rs line numbers as drifting, and says the 41.0% is the whole
CPU share including the latch copies.
@doublegate

Copy link
Copy Markdown
Owner Author

Round five, all three adopted — and the second turned a hand-wave into an actual specification.

1. "Note that the bus.rs line numbers drift too" — ADOPTED, same disclaimer as the latch table, with the function names marked as the durable part.

2. "Document the specific RDP state a sound idle predicate must validate" — ADOPTED, and reading Rdp::tick answered it exactly.

The tick returns having touched nothing at all in two cases, and both are pure reads of self:

  • status & (DP_STATUS_FREEZE | DP_STATUS_XBUS) != 0 — pipeline counter halted;
  • cmd_current >= cmd_end — command FIFO empty.

The other two early-outs do not qualify, and why matters more than the fact:

  • stall > 0 decrements stall, so it is not idle. But it does not need the bus either, so it can be handled before the take rather than being a reason to keep it.
  • A partially-written multi-word command (cmd_end - cmd_current < len_bytes) is not decidable without the bus — it reads the opcode from RDRAM to learn the length — so that case must still take.

And the asymmetry a predicate written from intuition would have missed: Audio::tick writes self.last_tick = now unconditionally on entry, so it is never idle by this definition and needs the return-a-request route instead. The two chips are not the same problem, which the section previously implied they were.

Your framing is why that got derived rather than asserted — "which state must be validated" is answerable from the code, and "is this risky" is not.

3. Nitpick: "is 41.0% inclusive or exclusive of latch copies?" — ADOPTED. Inclusive: it is the whole rustyn64-cpu/ share, so setting the 14.66% aside leaves ~26% of genuine instruction execution — which is the number that matters for the null result that paragraph is making.

…the code at it

Antigravity round six on PR #218. A stdlib line number is only citable because
this workspace pins an EXACT toolchain -- 1.96.0, per rust-toolchain.toml -- so
the section now says which one, and names what is actually at 929-930: the
read_via_copy / write_via_move pair inside replace. On any other toolchain, look
for that pair rather than for the numbers.
@doublegate

Copy link
Copy Markdown
Owner Author

Round six, adopted.

"Specify the toolchain version alongside the stdlib line references" — ADOPTED, and it is the reason those numbers are citable at all.

A stdlib line number would normally be useless in a document, and it is usable here only because this workspace pins an exact toolchain rather than a minimum — rustc 1.96.0, per rust-toolchain.toml, chosen for libretro build reproducibility. The section says so now.

It also names what is actually at those lines, which is the part that survives a toolchain bump:

let result = crate::intrinsics::read_via_copy(dest);
crate::intrinsics::write_via_move(dest, src);

the read_via_copy / write_via_move pair inside replace — i.e. the read and the write that make mem::take cost twice what a move would. On any other toolchain, look for that pair rather than for 929–930.

Antigravity has raised line-number drift three times, and the third framing --
reference a commit SHA so the numbers stay resolvable as permalinks -- is the
one that settles it rather than mitigating it. They are the lines of 2abc817,
the tree the profile was captured on, so they stay correct forever at that ref
and the function names remain the durable part in main.
@doublegate

Copy link
Copy Markdown
Owner Author

Round seven, adopted — and this framing is the one that settles the point rather than mitigating it.

Line-number drift has come up three times now (rounds one, three, and this one). My previous answers added the enclosing function, then the filename, then a "will drift" disclaimer — all of which reduce the damage without removing it. Referencing the commit does remove it.

The numbers are now stated as those of 2abc817, the tree the capture was taken on. That makes them permanently resolvable as a permalink, correct forever at that ref regardless of what main does — and it is also more honest, because a profile is a measurement of one particular tree, so citing its line numbers against any other tree was always slightly wrong.

The function names stay as the durable pointer for someone reading current main.


That is the last change I plan to make here. Every comment across seven rounds has been adjudicated and answered, CI is green, and the document now says: where the frame goes after the memo, the two in-model targets that remain with expected and ceiling figures for each, the exact idle predicate one of them needs derived from the code rather than guessed, and the null result for every other bucket above 3%.

…t decay

Antigravity round eight on PR #218, and the suggestion is right that a
repr(Rust) layout claim in a document is only true of the toolchain that
measured it. A const assert next to the struct makes it enforced instead:

    const _: () = assert!(core::mem::size_of::<Latch>() == 120, ...);

Mutation-checked -- changed to 121, cargo check fails with the message, restored.
The message tells the next person to RE-MEASURE rather than change the number,
because either a field was added or the layout algorithm moved, and the "no
padding is wasted" conclusion needs re-deriving before it is re-quoted.

Also answers the reviewer's question about the RDP command length: it is NOT
cached. Rdp holds only cmd_start / cmd_current / cmd_end, and tick re-reads
word0_hi from RDRAM every time, so the partial-command check genuinely needs the
bus and that case must still take. Plus the crates/ prefix and the 4 KiB + 4 KiB
split.
@doublegate

Copy link
Copy Markdown
Owner Author

Round eight. All four adopted, and the first turns a documentation claim into an enforced one — the best suggestion this PR has had.

1. "repr(Rust) layout is not stable across compiler versions; add a compile-time assertion" — ADOPTED, in code.

crates/rustyn64-cpu/src/pipeline.rs now carries, next to the struct:

const _: () = assert!(
    core::mem::size_of::<Latch>() == 120,
    "Latch changed size; docs/performance.md's copy-cost breakdown must be re-measured"
);

Mutation-checked, not assumed: changed to 121, cargo check fails with that message, restored.

The message is deliberately an instruction rather than a fact. If it fires, the fix is to re-measure, not to change the number — either a field was added, or the layout algorithm moved, and in the second case the "no padding is wasted" conclusion has to be re-derived before anyone re-quotes it. That is the difference between a guard and a speed bump.

2. "Verify whether the command length is already cached in Rdp" — VERIFIED; it is not.

Rdp holds only cmd_start, cmd_current, and cmd_end — no opcode, no length — and tick re-reads word0_hi from RDRAM on every pass to recover both. So the partial-command check genuinely needs the bus, and that case must still take. The section says so now, with the field list, so the next reader does not have to re-check.

Worth asking, though: if it had been cached, the predicate would have covered a third case.

3. Nitpick: "rustyn64-cpu/ without the crates/ prefix" — ADOPTED.

4. Nitpick: "clarify the 8 KiB as 4 KiB DMEM + 4 KiB IMEM" — ADOPTED, confirmed against rustyn64-rsp's own docs (each 4 KiB).

Gates re-run after the code change: fmt, clippy -D warnings, cargo test --workspace, rustdoc, and the no_std build all green.

@doublegate doublegate changed the title docs(perf): anatomize the latch copies — 6% of the frame moves provably-empty bytes perf/docs: anatomize the remaining in-model targets, and pin Latch's size in code Jul 30, 2026
…replace

Antigravity round nine on PR #218. The section attributed 5.32% to
core::mem::replace while the snippet below showed core::mem::take, with nothing
saying why a reader looking at profile frames sees one and code the other:
mem::take(x) IS mem::replace(x, Default::default()), and that default is where
the second write comes from. Said outright.

The read_via_copy / write_via_move pair now leads and the line numbers follow it,
since the names are what survives a toolchain bump and the numbers are only
quotable because this workspace pins an exact one.
@doublegate

Copy link
Copy Markdown
Owner Author

Round nine. Two adopted, one rejected on repository convention.

1. "Rely primarily on function names rather than stdlib line numbers" — ADOPTED. The read_via_copy / write_via_move pair now leads and the line numbers follow it as corroboration, with the note that the numbers are quotable only because this workspace pins an exact toolchain. The names are what survives a bump; the numbers are what lets someone re-derive the shares from the retained capture.

2. "The narrative says replace but the snippet shows take" — ADOPTED, and it was a genuine gap.

Now stated outright: mem::take(x) is mem::replace(x, Default::default()), which is why a profile frame reads replace while the code reads take — and it is also where the second write comes from, since the default has to be written into the vacated slot. That is the whole cost the section is about, and it was left as an inference.

3. Nitpick: "full commit SHA permalinks rather than plain short hashes" — REJECTED, on convention.

Every commit reference in this file and in the ADRs is a bare short hash — 646a3e0, 6a6adfa, 2abc817 — and these documents are read in-tree as often as on GitHub, where a URL is noise and git show 2abc817 is the natural move. Introducing URLs in one section would make it the odd one out, and a repository-wide convention change is not something a performance note should carry.


That is where I stop pushing on this PR. Nine rounds, every comment adjudicated and answered, CI green. The document now records: where the frame goes after the memo, the two in-model targets that remain with expected and ceiling figures for each, the exact idle predicate one of them needs derived from Rdp::tick's own early-outs, the null result for every other bucket above 3%, and a const assertion in pipeline.rs so the layout claim cannot decay silently.

Antigravity round ten asks whether the const assert breaks a 32-bit build. It
does not, and the workspace already proves that rather than assuming it: the
no_std gate builds this crate for thumbv7em-none-eabihf, a 32-bit target, and a
divergence would fail it. Every field is fixed-width -- no usize, no references,
no pointers -- so 120 holds on both widths.

Deliberately not #[cfg]-gated on target_pointer_width, and the comment now says
so: a gate would switch the guard OFF on exactly the target where a
pointer-sized field would first change the answer, which is the opposite of what
the suggestion is trying to protect.
@doublegate

Copy link
Copy Markdown
Owner Author

Round ten. One adopted as an explanation, two rejected as already answered.

1. "The const assertion assumes 64-bit layout; consider #[cfg(target_pointer_width = "64")]" — the concern is right, the conclusion is backwards, and the workspace already answers it.

It does not break a 32-bit build, and that is proven rather than assumed: the no_std gate builds this crate for thumbv7em-none-eabihf — a 32-bit target — and it passes with the assert in place. Every field in Latch is fixed-width (u64, u32, bool, enums with fixed payloads); there is no usize, reference, or pointer anywhere in it, so 120 holds on both widths.

Gating it would be actively worse: #[cfg(target_pointer_width = "64")] switches the guard off on exactly the target where a future pointer-sized field would first change the answer. The comment now records both the reasoning and the gate that demonstrates it.

2 & 3. "Line numbers in tables / stdlib line numbers are fragile" — REJECTED, fourth and fifth time.

Every durable form of this has been adopted across rounds one, three, seven and nine: each site carries its enclosing function, its statement, and its file; the stdlib reference now leads with read_via_copy / write_via_move; and the numbers are pinned to commit 2abc817, so they are permanently correct at that ref.

What the numbers still buy is the one thing the names cannot: re-deriving these shares from the retained perf capture, which is keyed on line numbers. Removing them would make the table unreproducible in order to make it tidier — and this section exists precisely so the next person does not have to re-run a two-minute profile to check a claim.


Stopping here, as stated last round. CI green, every comment across ten rounds adjudicated and answered.

… assert

Antigravity round eleven proposes #[repr(C)] as the way to get layout
stability. Measured instead of argued: repr(C) lays fields out in declaration
order, so the two bools can no longer occupy alignment gaps and Latch becomes
128 bytes rather than 120.

On a struct copied four times per emulated cycle that is ~1.2 ms a frame ADDED,
to exactly the copies this work is trying to shrink -- the suggestion would
regress the thing it appears next to. The const assert already provides the
stability it is reaching for, and costs nothing.

Found by bisecting the assert against repr(C) applied temporarily, then
reverting.
@doublegate

Copy link
Copy Markdown
Owner Author

Round eleven. The #[repr(C)] suggestion is new, so I measured it rather than arguing — and it would make things worse.

"If deterministic layout and size preservation are required, annotate Latch with #[repr(C)]" — REJECTED, measured.

repr(C) lays fields out in declaration order, so the two bools can no longer occupy alignment gaps:

layout size_of::<Latch>()
repr(Rust) (current) 120
#[repr(C)] 128

Found by applying repr(C) temporarily and bisecting the assert's expected value, then reverting.

Eight bytes more, on a struct copied four times per emulated CPU cycle. That takes the copy share from 14.66% to ~15.6% — about +1.2 ms a frame, added to precisely the copies this PR exists to measure and shrink. The suggestion would regress the thing it appears next to.

And the stability it is reaching for is already there: the const assert catches a layout change loudly at compile time, which is what "breaking builds on toolchain updates" describes — that is the guard working, not failing. repr(C) would buy the same guarantee by permanently paying for it.

Both the code comment and the document now record the 128, so the next person does not have to re-derive it.

"Line numbers will drift" — REJECTED, sixth time. Every durable form has been adopted across rounds one, three, seven, nine and ten: enclosing function, statement, file, stdlib symbol names leading, and the numbers pinned to commit 2abc817. What remains is what makes the table re-derivable from the retained capture, which is keyed on line numbers.


Stopping, as stated. Every comment across eleven rounds adjudicated; the last three have re-raised settled points and this one would have cost 1.2 ms a frame.

@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: 2

🤖 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 `@docs/performance.md`:
- Around line 494-502: Revise the prose in the performance documentation around
the split-borrow discussion: make the sentence beginning “At 125 ms a frame”
grammatically complete, and replace the non-idiomatic phrase “what it wants
done” with clearer wording while preserving the technical meaning about
Rsp::tick returning its requested operation.
- Around line 428-432: Correct the byte-breakdown explanation in the section
“What the byte breakdown adds to 0011's analysis” so it reconciles the three
cited sites (:2351, :2147, and :1903) with the stated copy count. Either
describe the exact three-copy path supported by the measurements or update the
cited rows and 6.01% figure to match four copies; keep the claim reproducible
and falsifiable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 77268d48-8b64-4d67-a8d1-98f5dca5a4a6

📥 Commits

Reviewing files that changed from the base of the PR and between 7d75553 and a47f0bd.

📒 Files selected for processing (2)
  • crates/rustyn64-cpu/src/pipeline.rs
  • docs/performance.md

Comment thread docs/performance.md
Comment on lines +428 to +432
**What the byte breakdown adds to 0011's analysis.** The last three fields — **72 of the
120 bytes** — are *produced at `EX`*. In `ic_rf` and `rf_ex` they are structurally always
`None`/`WriteBack::None`, so those two latches copy 72 bytes of provably-empty payload,
twice each per cycle. That is the `:2351`, `:2147`, and `:1903` rows above — **6.01%** of
the frame, moving nothing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reconcile the copy count with the measured rows.

The table identifies three sites (:2351, :2147, and :1903) totalling 6.01%, but “twice each per cycle” implies four copies. State the exact three-copy path, or adjust the cited rows and percentage; otherwise the byte-volume explanation is not reproducible.

Suggested wording
- so those two latches copy 72 bytes of provably-empty payload, twice each per cycle.
+ so the three copy sites in the ic_rf → rf_ex → EX path each move 72 bytes of
+ provably empty payload per cycle.

As per path instructions, measured claims must remain honest and falsifiable.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**What the byte breakdown adds to 0011's analysis.** The last three fields — **72 of the
120 bytes** — are *produced at `EX`*. In `ic_rf` and `rf_ex` they are structurally always
`None`/`WriteBack::None`, so those two latches copy 72 bytes of provably-empty payload,
twice each per cycle. That is the `:2351`, `:2147`, and `:1903` rows above — **6.01%** of
the frame, moving nothing.
**What the byte breakdown adds to 0011's analysis.** The last three fields — **72 of the
120 bytes** — are *produced at `EX`*. In `ic_rf` and `rf_ex` they are structurally always
`None`/`WriteBack::None`, so the three copy sites in the `ic_rf``rf_ex``EX` path each
move 72 bytes of provably empty payload per cycle. That is the `:2351`, `:2147`, and
` :1903` rows above — **6.01%** of the frame, moving nothing.
🧰 Tools
🪛 LanguageTool

[uncategorized] ~430-~430: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ..., so those two latches copy 72 bytes of provably-empty payload, twice each per cycle. That is ...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

🤖 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 `@docs/performance.md` around lines 428 - 432, Correct the byte-breakdown
explanation in the section “What the byte breakdown adds to 0011's analysis” so
it reconciles the three cited sites (:2351, :2147, and :1903) with the stated
copy count. Either describe the exact three-copy path supported by the
measurements or update the cited rows and 6.01% figure to match four copies;
keep the claim reproducible and falsifiable.

Source: Path instructions

Comment thread docs/performance.md
Comment on lines +494 to +502
At 125 ms a frame that is ~10.8 GB/s of memory traffic to satisfy the borrow checker,
which is consistent with the 5.32% the profile attributes to `core::mem`.

**The fix pattern is already in this repository.** `Bus::rsp_tick` (same file, line 519
at that commit)
used to do exactly this and no longer does; its comment records that the `take` was worse than the "no
allocation" claim above it, because `take` needs `Default` and constructing an `Rsp`
allocated its 8 KiB of scratch — 4 KiB DMEM and 4 KiB IMEM — **every RCP step**. `Rsp::tick` now *returns* what it
wants done instead of borrowing its owner. `Rdp` and `Audio` were not converted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Tighten the split-borrow prose.

“At 125 ms a frame that is…” is grammatically incomplete, and “what it wants done” is non-idiomatic.

Suggested wording
- At 125 ms a frame that is ~10.8 GB/s of memory traffic to satisfy the borrow checker,
+ At 125 ms per frame, that is ~10.8 GB/s of memory traffic to satisfy the borrow checker,

- `Rsp::tick` now *returns* what it wants done instead of borrowing its owner.
+ `Rsp::tick` now *returns a request* instead of borrowing its owner.

Based on learnings, LanguageTool prose findings are independent of Markdown-structure checks.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
At 125 ms a frame that is ~10.8 GB/s of memory traffic to satisfy the borrow checker,
which is consistent with the 5.32% the profile attributes to `core::mem`.
**The fix pattern is already in this repository.** `Bus::rsp_tick` (same file, line 519
at that commit)
used to do exactly this and no longer does; its comment records that the `take` was worse than the "no
allocation" claim above it, because `take` needs `Default` and constructing an `Rsp`
allocated its 8 KiB of scratch — 4 KiB DMEM and 4 KiB IMEM — **every RCP step**. `Rsp::tick` now *returns* what it
wants done instead of borrowing its owner. `Rdp` and `Audio` were not converted.
At 125 ms per frame, that is ~10.8 GB/s of memory traffic to satisfy the borrow checker,
which is consistent with the 5.32% the profile attributes to `core::mem`.
**The fix pattern is already in this repository.** `Bus::rsp_tick` (same file, line 519
at that commit)
used to do exactly this and no longer does; its comment records that the `take` was worse than the "no
allocation" claim above it, because `take` needs `Default` and constructing an `Rsp`
allocated its 8 KiB of scratch — 4 KiB DMEM and 4 KiB IMEM — **every RCP step**. `Rsp::tick` now *returns a request* instead of borrowing its owner. `Rdp` and `Audio` were not converted.
🧰 Tools
🪛 LanguageTool

[uncategorized] ~494-~494: Possible missing comma found.
Context: ... | | total | ~1.35 GB | At 125 ms a frame that is ~10.8 GB/s of memory tr...

(AI_HYDRA_LEO_MISSING_COMMA)


[formatting] ~500-~500: If the ‘because’ clause is essential to the meaning, do not use a comma before the clause.
Context: ... than the "no allocation" claim above it, because take needs Default and constructing...

(COMMA_BEFORE_BECAUSE)


[style] ~502-~502: The double modal “wants done” is nonstandard (only accepted in certain dialects). Consider “to be done”.
Context: ...Rsp::tick now returns what it wants done instead of borrowing its owner. Rdp a...

(NEEDS_FIXED)

🤖 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 `@docs/performance.md` around lines 494 - 502, Revise the prose in the
performance documentation around the split-borrow discussion: make the sentence
beginning “At 125 ms a frame” grammatically complete, and replace the
non-idiomatic phrase “what it wants done” with clearer wording while preserving
the technical meaning about Rsp::tick returning its requested operation.

Sources: Learnings, Linters/SAST tools

Antigravity round twelve: /// on `const _: () = assert!(...)` is inert --
rustdoc does not emit documentation for an unnamed item, so twenty-five lines of
reasoning were addressed to a reader who would never see them. // now.

A small instance of the inert-API hazard docs/engineering-lessons.md §3.2
describes: it compiled, the rustdoc gate passed, and the text was simply never
going anywhere.
@doublegate doublegate changed the title perf/docs: anatomize the remaining in-model targets, and pin Latch's size in code docs(perf): anatomize the remaining in-model targets, and pin Latch's size in code Jul 30, 2026
@doublegate

Copy link
Copy Markdown
Owner Author

Round twelve. Both adopted — the second is a small instance of a hazard this repository has a lesson about.

1. "perf/docs: violates Conventional Commits" — ADOPTED. Retitled docs(perf):. A single type is the rule, and the substance here is measurement written down; the one code change is a build-time guard supporting it, which the squash body says explicitly.

2. "/// on const _: () = assert!(...) is inert — rustdoc does not document unnamed items" — ADOPTED, and thank you, because it is a real defect rather than a style point.

Twenty-five lines of reasoning — why the size matters, what to do if the assert fires, why it is not #[cfg]-gated, what repr(C) would cost — were addressed to a reader who would never see them. It compiled, the rustdoc gate passed with -D warnings, and the text was simply not going anywhere.

That is the shape docs/engineering-lessons.md §3.2 calls the inert-API hazard: something that looks correct, passes every gate, and carries no information. Now //, where it reaches the person who will actually be standing in front of that assert when it fires.

@doublegate

Copy link
Copy Markdown
Owner Author

Both items in this round were already applied in the commit that landed after the review ran: the doc comments became // (unused_doc_comments on an unnamed const _ — the inert-comment defect, adopted last round), and the title is docs(perf):. No further change.

…6 is 108

Antigravity round thirteen was right and my previous answer was wrong. I
verified the assert on thumbv7em-none-eabihf and generalised that to "32-bit",
which does not follow: what varies is u64 ALIGNMENT, not pointer size. On
thumbv7em and wasm32 u64 aligns to 8 and Latch is 120; on 32-bit x86 it aligns
to 4 and Latch is 108 -- measured by bisecting the assert against
i686-linux-android.

So "every field is fixed-width, therefore the layout is width-independent" was a
plausible-sounding inference from one confirming case, which is the shape this
repository has a lesson about.

The assert stays ungated, and the comment now says why in the corrected terms:
all three supported targets (x86_64, thumbv7em, wasm32) are built in CI and all
three are 120, and on a newly added ABI the assert firing is the guard WORKING --
that is exactly when the copy-cost breakdown has to be re-measured before it is
re-quoted. A #[cfg] would make the new target the one place the numbers go
unchecked.
@doublegate

Copy link
Copy Markdown
Owner Author

Round thirteen. You are right and my previous answer was wrong. Measured.

Last round I said the assert is safe on 32-bit because every field is fixed-width. I had verified that on thumbv7em-none-eabihf and generalised from one confirming case — which does not follow, and you named exactly why: what varies is u64 alignment, not pointer size.

Bisected the assert against i686-linux-android:

target size_of::<Latch>()
x86_64, thumbv7em-none-eabihf, wasm32-unknown-unknown 120
i686-* (32-bit x86, u64 aligns to 4) 108

So "fixed-width fields ⇒ width-independent layout" was a plausible inference from one data point, which is the failure shape this repository keeps a lessons file about. Corrected in both the comment and the document, with the number.

The assert stays ungated, and the reasoning changes rather than the code:

  • All three targets this workspace actually builds are 120, and all three are covered in CI — the no_std gate builds thumbv7em, and wasm32 is in the same target list.
  • On a newly added ABI it would fire, and that is the guard working. A different ABI is precisely when the copy-cost breakdown has to be re-measured before anyone re-quotes it, which is what the assert's message already instructs.
  • #[cfg]-gating it would make the new target the one place the numbers go unchecked — the inverse of the protection.

Second item — "mention the enclosing function names alongside line numbers" — they have been there since round one: the table's first column is the function (ex_stage, dc_stage, ic_stage, rf_stage), the second the statement, the third the line at commit 2abc817, plus the file named above the table.

Antigravity round fourteen's objection is fair: a hard build failure on i686,
with a message about a docs file, is a poor experience for someone who only
wants to cross-compile. Defending the guard was the wrong response; there is a
design that satisfies both sides.

Two assertions now, and neither breaks a cross-compile:

1. NO PADDING, universally -- size_of::<Latch>() equals the sum of its
   components. That is the property the copy-cost breakdown actually rests on
   ("no field ordering would make this smaller"), and it is verified to hold on
   x86_64, thumbv7em-none-eabihf, wasm32-unknown-unknown AND i686-linux-android.

2. 120 WHERE A u64 ALIGNS TO 8, written as align_of::<u64>() != 8 || size == 120
   rather than as a #[cfg]. This pins the documented figure on every ABI the
   figure describes and makes no claim on the ones it does not -- which is the
   real mechanism, rather than pointer width.

Both mutation-checked: breaking either produces its own message.

@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: 2

🤖 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 `@crates/rustyn64-cpu/src/pipeline.rs`:
- Around line 281-283: Correct the latch-copy comment near the inter-stage latch
to match the six measured copy sites documented in docs/performance.md: two in
EX, two in DC, one in RF, and one in IC. If the existing four-copy figure refers
to a narrower operation, explicitly define that scope instead of leaving the
broader claim ambiguous.

In `@docs/performance.md`:
- Around line 435-438: Update the measured timing claim in the section
discussing #[repr(C)] to include reproducible provenance: identify the target
hardware/platform, toolchain, benchmark or capture commit, and measurement
method used to obtain the 128-byte layout and approximately 1.2 ms/frame result.
Keep the existing comparison intact while ensuring the values are not presented
as unsupported facts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 257706bd-1446-4599-9c0a-8e481e458910

📥 Commits

Reviewing files that changed from the base of the PR and between a47f0bd and 50125aa.

📒 Files selected for processing (2)
  • crates/rustyn64-cpu/src/pipeline.rs
  • docs/performance.md

Comment on lines +281 to +283
// The inter-stage latch is **copied four times per emulated CPU cycle**, so its size is
// a performance fact and not merely a layout detail: at ~1.56 M steps a frame those
// copies are ~15% of the frame (`docs/performance.md` §"The latch copies, anatomized").

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reconcile the latch-copy count with the measured sites.

The comment says the latch is copied four times per CPU cycle, but docs/performance.md identifies six copy sites: two in EX, two in DC, one in RF, and one in IC. Correct the count or explicitly define the narrower operation being counted.

As per path instructions, comments that contradict measured behaviour are correctness hazards. Based on learnings, documentation and comments must match the implementation.

🤖 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-cpu/src/pipeline.rs` around lines 281 - 283, Correct the
latch-copy comment near the inter-stage latch to match the six measured copy
sites documented in docs/performance.md: two in EX, two in DC, one in RF, and
one in IC. If the existing four-copy figure refers to a narrower operation,
explicitly define that scope instead of leaving the broader claim ambiguous.

Sources: Path instructions, Learnings

Comment thread docs/performance.md
Comment on lines +435 to +438
**`#[repr(C)]` would be the wrong way to get that stability**, measured: it lays fields
out in declaration order, so the two `bool`s can no longer occupy alignment gaps and the
struct becomes **128 bytes**. On something copied four times per emulated cycle that adds
~1.2 ms a frame — to the very copies this section is about.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add provenance for the measured timing claim.

“Measured” is not sufficient provenance for the 128-byte layout or the approximately 1.2 ms/frame cost. Record the target, toolchain, benchmark or capture commit, and measurement method so this value can be reproduced rather than treated as an unsupported fact.

As per coding guidelines, measured timing values require provenance and must not be presented as undocumented hardware or performance facts.

🧰 Tools
🪛 LanguageTool

[style] ~435-~435: To form a complete sentence, be sure to include a subject or ‘there’.
Context: ...wn must be re-measured. *#[repr(C)] would be the wrong way to get that stability...

(MISSING_IT_THERE)


[uncategorized] ~437-~437: Possible missing comma found.
Context: ...omething copied four times per emulated cycle that adds ~1.2 ms a frame — to the very...

(AI_HYDRA_LEO_MISSING_COMMA)

🤖 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 `@docs/performance.md` around lines 435 - 438, Update the measured timing claim
in the section discussing #[repr(C)] to include reproducible provenance:
identify the target hardware/platform, toolchain, benchmark or capture commit,
and measurement method used to obtain the 128-byte layout and approximately 1.2
ms/frame result. Keep the existing comparison intact while ensuring the values
are not presented as unsupported facts.

Source: Coding guidelines

@doublegate

Copy link
Copy Markdown
Owner Author

Round fourteen. Your objection is fair and I stopped defending the guard. There is a design that satisfies both positions, and it is better than either.

A hard build failure on i686, carrying a message about a documentation file, is a poor experience for someone who only wants to cross-compile — and my "that is the guard working" answer was defending a choice rather than reconsidering it.

Two assertions now, and neither can break a cross-compile:

  1. No padding, universally. size_of::<Latch>() equals the sum of its components. That is the property the breakdown actually rests on — "no field ordering would make this struct smaller" — and it is the thing worth protecting. Verified to hold on x86_64, thumbv7em-none-eabihf, wasm32-unknown-unknown, and i686-linux-android.

  2. 120 where a u64 aligns to 8, as an implication rather than a #[cfg]:

    assert!(core::mem::align_of::<u64>() != 8 || core::mem::size_of::<Latch>() == 120,);

    This pins the documented figure on every ABI the figure describes, and makes no claim on the ones it does not. It keys on the mechanism you identified — alignment — rather than on pointer width, which is what my earlier reasoning got wrong.

Both mutation-checked: breaking either produces its own distinct message, and all four targets build clean.

The result is strictly better than where this started. The universal invariant is the one that would actually catch a regression, and the previous single assert was pinning a number instead of a property — which is the weaker of the two things to protect.

On the line numbers, sixth raising: the function names are the primary identifiers and have been since round one — the table's first column is ex_stage / dc_stage / ic_stage / rf_stage, the second the statement, with the file above it; the line number is a fourth column pinned to commit 2abc817 so the shares stay re-derivable from the retained capture.

Antigravity round fifteen. Both items checkable, both checked.

The scalar term was literals -- 1 + 8 + 4 + 1 + 8 + 8 -- which desynchronizes if
a field type changes and would then MASK the padding it exists to detect. It is
size_of of each field's type now.

The second asks whether keying the 120 on align_of::<u64>() == 8 could fail on a
32-bit ARM target, where u64 is 8-byte aligned but inner-type layout might
differ. Built for armv7-linux-androideabi: it passes, i.e. Latch is 120 there.
That is the exact combination the implication is keyed on -- 32-bit with an
8-byte-aligned u64 -- so it is the case that would have failed first, and the
table now lists it as measured rather than assumed.
@doublegate

Copy link
Copy Markdown
Owner Author

Round fifteen. Both adopted, both checkable, both checked.

1. "let scalars = 1 + 8 + 4 + 1 + 8 + 8; hardcodes byte sizes and would desynchronize if a field type changes" — ADOPTED, and your second sentence is the important half.

It would not merely go stale; it would mask the very padding the assert exists to detect — a wrong constant on both sides of an equality is indistinguishable from a correct one. It is now size_of of each field's type, so a type change updates the term instead of silently invalidating it.

2. "align_of::<u64>() == 8 may not imply 120 on 32-bit ARM, where inner-type layout could differ" — CHECKED on armv7-linux-androideabi: it is 120, and both asserts pass.

That is precisely the case that would have failed first — 32-bit with an 8-byte-aligned u64 is the exact combination the implication is keyed on, so if the keying were wrong, armv7 is where it shows. The layout table now lists it as measured rather than assumed.

Current state, all built and passing:

target size_of::<Latch>()
x86_64, thumbv7em-none-eabihf, wasm32-unknown-unknown, armv7-linux-androideabi 120
i686-linux-android 108, and only the universal no-padding assert applies

Five targets, two asserts, no #[cfg], no cross-compile broken. That is a better place than this thread started, and it got there because the objection was pressed rather than dropped.

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR documents remaining performance bottlenecks in CPU pipeline latch copies and bus split-borrows, while adding compile-time assertions in crates/rustyn64-cpu/src/pipeline.rs to enforce struct size and zero-padding invariants for Latch.

Blocking issues

None found.

Suggestions

  • Brittle target architecture alignment assertion: In pipeline.rs, the assertion core::mem::align_of::<u64>() != 8 || core::mem::size_of::<Latch>() == 120 assumes u64 alignment is the sole factor governing Latch size. Future compiler layout changes, niche optimizations in Option<T> fields, or platform ABI variations on targets where u64 alignment is 8 bytes will trigger compile errors on valid toolchains or target triples.
  • Manual type listing in scalar size computation: In pipeline.rs, scalars manually lists scalar primitive types (bool, u64, u32) rather than referencing actual struct fields. If a field type on Latch changes, this list will not trigger a type-checking failure and can silently desynchronize from the struct definition.

Nitpicks

  • Transient line numbers in documentation: In performance.md, attribution tables rely on specific line numbers from commit 2abc817. Line numbers in markdown documentation drift rapidly as surrounding code evolves; rely on durable symbol names or function signatures instead.

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

@doublegate

Copy link
Copy Markdown
Owner Author

Round sixteen. Both rejected, and the second has a direct answer that I should have given earlier.

1. "The implication may fire on a future toolchain or a valid target" — that is the guard, not a defect.

Yes, and deliberately: firing means re-measure before re-quoting, which is what its message says. What changed in round fourteen is the blast radius, which was your earlier objection and was fair — the universal no-padding assert now carries the property worth protecting on every ABI, and the 120 is claimed only where it is documented. Verified on five targets: x86_64, thumbv7em-none-eabihf, wasm32-unknown-unknown, armv7-linux-androideabi (all 120) and i686-linux-android (108, universal assert only).

A guard that cannot fire is decoration. The alternative — deleting it — is what leaves a stale layout claim in a document, which is the failure this whole PR is about.

2. "The manual type list can silently desynchronize if a field type changes" — it cannot be silent, and that is checkable.

Consider the case you name: word: u32 becomes u64. size_of::<Latch>() grows; scalars does not. So parts != size_of::<Latch>() and the assert fires. The desync produces a loud failure, not silence — the opposite of the concern.

Demonstrated rather than argued: I mutated + scalars to + scalars + 1 while testing this and it failed with Latch has acquired padding. A type change is the same class of mismatch.

You are right that it is not type-checked — Rust has no stable way to take size_of of a field without naming its type, and offset_of! would only prove the absence of trailing padding, not interior. The manual list is the best available construction, and its failure mode is loud, which is the property that matters.


This is where I stop on this PR: sixteen rounds, every comment adjudicated and answered, CI green. The last several have re-raised settled points or asked for guarantees Rust cannot express, while the earlier ones materially improved it — the two-assert redesign, the five-target verification, and the correction of a false claim I had made all came from this thread.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/performance.md (1)

420-429: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the stale assertion description.

The text still says pipeline.rs contains an unconditional size_of::<Latch>() == 120 assertion, but the implementation now has a universal component-size assertion plus a conditional 120-byte assertion based on align_of::<u64>(). Describe both assertions accurately.

🤖 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 `@docs/performance.md` around lines 420 - 429, Update the assertion description
in the performance documentation to accurately reflect the implementation in
pipeline.rs: mention the universal no-padding/component-size assertion and the
conditional size_of::<Latch>() == 120 assertion that applies when
align_of::<u64>() is 8, rather than describing the 120-byte check as
unconditional.

Sources: Coding guidelines, 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.

Outside diff comments:
In `@docs/performance.md`:
- Around line 420-429: Update the assertion description in the performance
documentation to accurately reflect the implementation in pipeline.rs: mention
the universal no-padding/component-size assertion and the conditional
size_of::<Latch>() == 120 assertion that applies when align_of::<u64>() is 8,
rather than describing the 120-byte check as unconditional.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9deaa04a-42a8-4ac1-8059-3aee0139a8e3

📥 Commits

Reviewing files that changed from the base of the PR and between 50125aa and f3cd85a.

📒 Files selected for processing (2)
  • crates/rustyn64-cpu/src/pipeline.rs
  • docs/performance.md

@doublegate
doublegate merged commit b6916d5 into main Jul 30, 2026
12 checks passed
@doublegate
doublegate deleted the docs/latch-copy-anatomy branch July 30, 2026 17:20
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