Skip to content

feat(cpu): loads, stores, and the unaligned family - #6

Merged
doublegate merged 2 commits into
mainfrom
feat/loads-and-stores
Jul 20, 2026
Merged

feat(cpu): loads, stores, and the unaligned family#6
doublegate merged 2 commits into
mainfrom
feat/loads-and-stores

Conversation

@doublegate

Copy link
Copy Markdown
Owner

T-11-003. The CPU can now reach memory.

What landed

  • mem.rs — data shaping as pure byte-level transforms. Width/signedness rules, alignment requirements, and the LWL/LWR/LDL/LDR + SWL/SWR/SDL/SDR merges.
  • EX resolves the effective address; DC performs the access. That split is the point of the pipeline — DC is the cycle the scheduler interleaves the RCP around (ADR 0007).
  • Unaligned access in an aligned form raises AddressError. The LWL/LWR family is exempt by construction.

The unaligned family is the hard part

MIPS has no unaligned load — LWL/LWR are used as a pair to assemble a value from two aligned accesses. Every shift direction is decided by the N64 being big-endian, and getting one backwards yields plausible values that are wrong only at unaligned addresses.

So it's tested as a pair, plus a store-then-load round-trip across every byte offset. That round-trip is what told me my SWR assertion was wrong rather than the implementation — I'd asserted it merges rt's low bytes into memory's top bytes, when it fills backward from the addressed byte to the word start. Verified by hand against the byte layout before touching anything.

The load-delay interlock is live

It's had a load_interlocks predicate since #2 with nothing to interlock against. Loads give it one, and the integration test immediately found a real bug: I compared against the rf_ex latch, but in the reverse cascade EX runs before RF — so by the time rf_stage executes, that instruction has moved to ex_dc and rf_ex is vacated. The check silently never fired. Now compares ex_dc, with the reasoning recorded at the site. Mutation-tested.

Two tests repointed, not deleted

They asserted LW and opcode 0o77 decode to Reserved — no longer true once loads and stores exist. Repointed at genuinely unimplemented encodings (BEQ, arriving in T-11-004, and the unused SPECIAL funct 0o01).

Gate

fmt, clippy -D warnings (0), 106 tests (was 94), rustdoc -D warnings, no_std cross-build.

T-11-003. The CPU can now reach memory.

mem.rs holds the data shaping as pure byte-level transforms -- no bus, no
registers -- so the fiddly parts are testable without a machine.

The LW/LWU distinction is called out because it is easy to lose: LW
sign-extends its 32-bit result into the 64-bit register and LWU does not.
Confusing them silently breaks any address above 2 GiB and nothing else.

The unaligned family is where the real difficulty is. MIPS has no unaligned
load; LWL/LWR (and LDL/LDR) are used AS A PAIR to assemble an unaligned value
from two aligned accesses, each merging part of the addressed word into the
destination while preserving the rest. Every shift direction here is decided by
the N64 being big-endian, and getting one backwards yields plausible-looking
values that are wrong only at unaligned addresses -- a miserable bug to find
later.

So they are tested as a PAIR, the way they are actually used, plus a
store-then-load round trip across every byte offset. That round trip is the
strongest available statement that the shift directions agree with each other,
and it is what told me my SWR test assertion was wrong rather than the
implementation: I had asserted SWR merges rt's low bytes into memory's TOP
bytes, when it fills backward from the addressed byte to the word start. Checked
by hand against the byte layout before changing anything.

EX resolves the effective address (base + SIGN-extended offset) and DC performs
the access. That split is the whole point of the pipeline -- DC is the cycle the
scheduler interleaves the RCP around (ADR 0007) -- and it is why the address
computation and the access are in different modules.

An unaligned access in an aligned form raises AddressError. The LWL/LWR family is
exempt by construction, since being usable at any byte offset is precisely why it
exists.

THE LOAD-DELAY INTERLOCK IS LIVE. It has had a `load_interlocks` predicate since
PR #2 with nothing to interlock against; loads give it one. It reproduces the
hardware's documented imprecision -- matching the rs or rt encoded field whether
or not it is used as a source, exempting $zero, not crossing GPR/FPR.

One real bug found by its integration test: I compared against the `rf_ex` latch,
but in the reverse cascade EX runs BEFORE RF, so by the time rf_stage executes
the instruction that was in EX has already moved to `ex_dc` and `rf_ex` has been
vacated. The check therefore silently never fired. Now compares `ex_dc`, with the
reasoning recorded at the site so it does not get "simplified" back.
Mutation-tested: disabling the load check fails the test.

Two existing tests had premises invalidated -- they asserted LW and opcode 0o77
decode to Reserved, which stopped being true the moment loads and stores landed.
Repointed at genuinely unimplemented encodings (BEQ, which arrives in T-11-004,
and the unused SPECIAL funct 0o01) rather than deleted.

Gate: fmt, clippy -D warnings (0), 106 tests (was 94), rustdoc -D warnings,
no_std cross-build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 20, 2026 07:50

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements memory load and store instructions (T-11-003), including aligned loads/stores, the unaligned load/store family (LWL/LWR/LDL/LDR/SWL/SWR/SDL/SDR), and the load-delay interlock. It introduces a new mem module for data shaping and integrates memory operations into the pipeline's EX and DC stages, supported by comprehensive unit and integration tests. Feedback suggests optimizing the read_width and write_width helper functions in pipeline.rs to utilize the bus's 32-bit read/write paths for 4-byte and 8-byte accesses instead of performing byte-by-byte loops, which introduces a performance bottleneck.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/rustyn64-cpu/src/pipeline.rs

Copilot AI 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.

Pull request overview

This PR implements the VR4300’s first end-to-end memory reachability: decoding/exec wiring for loads/stores (including the unaligned LWL/LWR/LDL/LDR + SWL/SWR/SDL/SDR family), a byte-precise shaping layer, and DC-stage bus access consistent with the five-stage pipeline split (ADR 0007). It also activates the load-delay interlock using the correct latch under the reverse cascade.

Changes:

  • Add mem.rs to centralize load/store width/signedness shaping and unaligned merge transforms as pure byte-level operations.
  • Introduce MemOp from EXDC, and implement DC-stage memory accesses plus address-error behavior for misaligned aligned-form accesses.
  • Extend decode/execute and add integration tests covering loads/stores, unaligned pairs, and the load-delay interlock; update changelog accordingly.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
crates/rustyn64-cpu/src/pipeline.rs Adds MemOp plumbing, DC-stage memory access, load-delay interlock latch fix, and end-to-end RAM-backed tests.
crates/rustyn64-cpu/src/mem.rs Implements pure byte-level shaping + unaligned load/store merge transforms with unit tests.
crates/rustyn64-cpu/src/lib.rs Exposes the new mem module and re-exports LoadKind/StoreKind.
crates/rustyn64-cpu/src/exec.rs Adds MemOp and wires loads/stores/unaligned ops to emit EXDC memory requests.
crates/rustyn64-cpu/src/decode.rs Adds opcode decoding for loads/stores and marks loads for the load-delay interlock.
CHANGELOG.md Documents the new CPU memory reachability and the now-live load-delay interlock.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/rustyn64-cpu/src/pipeline.rs
Comment thread crates/rustyn64-cpu/src/exec.rs
Comment thread crates/rustyn64-cpu/src/pipeline.rs
Comment thread crates/rustyn64-cpu/src/pipeline.rs
Adopts Gemini's comment on PR #6, with its signature change declined.

read_width/write_width looped byte-by-byte for every access, so a 4-byte load
issued four read_u8 calls and an 8-byte load eight -- bypassing Bus::read_u32
entirely, which rustyn64-core overrides with a fast RDRAM path. Those are the
two most common memory operations, and memory access is the hot path for a core
whose stated goal is sustained full speed (docs/performance.md). 4-8x more bus
calls than necessary on the common case is worth fixing before the pattern
spreads.

Now dispatched on width: read_u8 for 1-2 bytes, read_u32 for 4, and two
read_u32 calls for 8 (big-endian, high word at the lower address).

NOT adopted: the suggestion also moved alignment checking into these helpers,
returning Option/bool.  already validates alignment against the specific
LoadKind/StoreKind before calling them, and the unaligned family passes an
address it aligned down itself. Adding a second check would put the rule in two
places where it can drift, and the width-based check proposed is weaker than the
kind-based one that already exists -- LoadKind::SignedByte and LWL want different
answers for the same width. The reasoning is recorded at the function so the
absence of a check there does not read as an oversight.

Verified by the existing round-trip tests, which would catch an endianness error
in the new 8-byte path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate
doublegate merged commit 27be7f1 into main Jul 20, 2026
7 checks passed
@doublegate
doublegate deleted the feat/loads-and-stores branch July 20, 2026 07:59
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.

2 participants