Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,10 @@ __pycache__/
!/tests/roms/peterlemon-timing/CPUTIMINGNTSC.z64
!/tests/roms/peterlemon-timing/CP1TIMINGNTSC.z64

# First-party homebrew timing ROM (authored here, MIT OR Apache-2.0; 32 KiB, no
# Nintendo code). Re-included by exact filename. See tools/mrdram-timing-rom/.
# First-party homebrew timing ROMs (authored here, MIT OR Apache-2.0; no Nintendo
# code). Re-included by exact filename. See tools/mrdram-timing-rom/.
!/tools/mrdram-timing-rom/mrdram_timing.z64
!/tools/mrdram-timing-rom/icache_timing.z64

# ...then hard-exclude the external tier again, so no negation above can ever
# accidentally re-include a commercial dump. Order matters: this comes last.
Expand Down
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ Work toward `v0.8.0 "Breadth"` — the accuracy battery (Phase 7).
The I-cache stall is charged behind a `#[cfg(not(test))]` seam — active in real
execution and integration tests, skipped in the CPU crate's own pipeline units
(an every-fetch stall would confound their fixed-cycle interlock assertions).
- **A hardware timing ROM** (`tools/mrdram-timing-rom/`) that measures the real
D-cache fill cost on a console via a COP0-`Count` differential, so the fitted
values can be replaced with a measurement when hardware is available.
- **Two hardware timing ROMs** (`tools/mrdram-timing-rom/`) that measure the real
cache fill costs on a console, so the fitted values can be replaced with a
measurement when hardware is available: `mrdram_timing.z64` (D-cache fill, a
COP0-`Count` differential of cached loads that miss vs. hit) and
`icache_timing.z64` (I-cache fill, a straight-line instruction block larger than
the 16 KiB I-cache, base-subtracted). Both emit their result over ISViewer for a
flashcart to read, and each has an emulator runner asserting it reads back the
charged constant (`icache_timing_rom.rs` measures 46.09, the charged 46).
- `M(RDRAM)` as a true measurement and the RDRAM bank-state model (C-4) remain open.

### Added — the first CPU-timing differential measurement (gap-analysis Stage D)
Expand Down
71 changes: 71 additions & 0 deletions crates/rustyn64-test-harness/tests/icache_timing_rom.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//! Runner for the authored **I-cache-fill timing ROM**
//! (`tools/mrdram-timing-rom/icache_timing.asm`).
//!
//! The companion to `mrdram_timing_rom.rs` (which measures the D-cache fill).
//! That ROM measures the VR4300 **I-cache** line-fill cost on real hardware: it
//! runs a straight-line block of N one-PClock instructions LARGER than the
//! 16 KiB instruction cache, so every 32-byte fetch line misses, and times it
//! with COP0 `Count`. Run on an N64, the number it emits is the *real* fill cost
//! — the measurement that would replace the value currently FITTED from
//! ares/cen64 (accuracy ledger C-1).
//!
//! In the emulator it necessarily reads back **our** charged I-cache fill
//! (`M_ICACHE_FILL` = 46), so this test doubles as (a) proof the ROM's
//! measurement logic is correct end-to-end through a real machine boot, and
//! (b) a regression guard tying the ROM to the charged constant. On hardware the
//! same ROM yields the true number.

use rustyn64_core::System;
use rustyn64_test_harness::rom;

const ROM: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tools/mrdram-timing-rom/icache_timing.z64"
);

/// Result words the ROM writes to uncached RDRAM (phys `0x10000`).
fn word(sys: &System, phys: usize) -> u32 {
let r = &sys.bus.rdram;
u32::from_be_bytes([r[phys], r[phys + 1], r[phys + 2], r[phys + 3]])
}

#[test]
fn the_timing_rom_measures_the_charged_icache_fill() {
let image = std::fs::read(ROM).expect("assembled timing ROM (see tools/mrdram-timing-rom)");
let entry = rom::entry_point(&image).expect("ROM header entry point");
let mut sys = System::new(0);
rom::load_direct(&mut sys, &image, entry).expect("load the ROM");

// Run until the ROM writes its sentinel (the N word at phys 0x10004): the
// straight-line block runs, then it stores delta + N and spins. The cap is a
// generous backstop against a ROM that never writes.
let mut steps = 0u64;
while word(&sys, 0x10004) == 0 && steps < 20_000_000 {
sys.step_to_next_edge();
steps += 1;
}
assert!(
steps < 20_000_000,
"ROM never wrote its sentinel within the step cap — it hung or the entry \
point is wrong; results below would be garbage"
);

let delta = word(&sys, 0x10000);
let n = word(&sys, 0x10004);
assert_eq!(n, 8192, "the ROM ran its block and wrote its sentinel N");

// The block is N one-PClock `addiu`s; every 8-instruction (32 B) fetch line
// misses. Subtract the 1-PClock-per-instruction base, divide by the number
// of line fills (N/8). COP0 `Count` ticks once per 2 PClocks, so *2 gives
// PClocks: fill = (delta * 2 - N) / (N / 8).
let fill = (f64::from(delta) * 2.0 - f64::from(n)) / (f64::from(n) / 8.0);
println!(
"timing ROM: delta={delta} N={n} \
-> I-cache fill = {fill:.2} PClocks (our charged M_ICACHE_FILL)"
);
assert!(
(fill - 46.0).abs() < 1.0,
"the ROM measured {fill:.2} PClocks; in-emulator it must read the charged \
I-cache fill (46). If M_ICACHE_FILL changed, update this and ledger C-1."
);
}
11 changes: 10 additions & 1 deletion docs/accuracy-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,16 @@ block twice the 16 KiB I-cache (every line misses) and, subtracting the verified
measures **46.05 PClocks/fill**; the systemtest still completes (Phase-1 `Failed: 0`, 90 suite-wide,
`xioctl(EXIT)`, ~33 s vs ~31 s) and golden-log 0-diff / residue / determinism hold. The D-cache
fill, by contrast, fires only on a rare cached load and is charged unconditionally (two units
absorbed it). **`M(RDRAM)` as a true measurement, and the RDRAM bank-state model (C-4), remain
absorbed it). To make the eventual hardware measurement one console-run away, two bare-metal
timing ROMs are authored in `tools/mrdram-timing-rom/` (MIT OR Apache-2.0, blank IPL3):
`mrdram_timing.z64` measures the D-cache fill via a COP0-`Count` miss-vs-hit differential, and
`icache_timing.z64` measures the I-cache fill by timing a straight-line block larger than the
16 KiB I-cache and subtracting the verified 1-PClock base. Both emit their raw numbers over
ISViewer for a flashcart to read, and each has an emulator runner
(`mrdram_timing_rom.rs`, `icache_timing_rom.rs`) that boots the ROM through `load_direct` and
asserts it reads back the charged constant (the I-cache ROM measures 46.09 in-emulator, the
charged 46) — proof the measurement path is correct end-to-end, and a guard tying each ROM to
its constant. **`M(RDRAM)` as a true measurement, and the RDRAM bank-state model (C-4), remain
open.** No regression from the D-cache charge: golden-log 0-diff (it keys on retired instructions,
not stalls), the residue invariant, determinism, and the 950-test functional suite (Phase-1
`Failed: 0`, still 90 suite-wide, `Random` timing tests pass, runs to `xioctl(EXIT)`) are all
Expand Down
59 changes: 48 additions & 11 deletions tools/mrdram-timing-rom/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
# M(RDRAM) cached-load timing ROM
# VR4300 cache-fill timing ROMs

A bare-metal N64 ROM that **measures the VR4300 D-cache line-fill cost on real
hardware** — the memory latency `M(RDRAM)` that accuracy-ledger **C-1** currently
carries as a value *fitted* from ares/cen64 (no hardware oracle exists in the
emulation community's test corpus). Run this on a console and it yields the real
number.
Two bare-metal N64 ROMs that **measure the VR4300 cache line-fill costs on real
hardware** — the memory latencies that accuracy-ledger **C-1** currently carries
as values *fitted* from ares/cen64 (no hardware oracle exists in the emulation
community's test corpus). Run them on a console and they yield the real numbers.

## What it measures, and how
- **`mrdram_timing.z64`** — the **D-cache** line-fill cost (a differential of
cached loads that miss vs. hit).
- **`icache_timing.z64`** — the **I-cache** line-fill cost (a straight-line
instruction block larger than the 16 KiB I-cache, so every fetch line misses).

The D-cache ROM is described in full below; the I-cache ROM is its companion and
shares the build, header convention, and ISViewer output — see
[I-cache variant](#i-cache-variant) at the end.

## What the D-cache ROM measures, and how

The D-cache miss cost is `8..=9 + M(RDRAM)` PClocks (VR4300 User's Manual
Table 11-1). This ROM isolates it with a **differential**, timed by the COP0
Expand Down Expand Up @@ -36,10 +44,10 @@ either way. `fill_cost = word[2] / word[3] × 2` PClocks.
architecture-table placement. Then:

```sh
BASS=/path/to/bass sh build.sh # -> mrdram_timing.z64 (32 KiB)
BASS=/path/to/bass sh build.sh # -> mrdram_timing.z64 (32 KiB) + icache_timing.z64 (96 KiB)
```

The assembled `mrdram_timing.z64` is committed for convenience.
Both assembled ROMs are committed for convenience.

## Verify in the emulator

Expand Down Expand Up @@ -68,8 +76,37 @@ cargo test -p rustyn64-test-harness --release --test mrdram_timing_rom -- --noca
`0x2000`), compute `word[2] / word[3] × 2`, and that is the real
`M(RDRAM)`-inclusive D-cache fill in PClocks. Drop it into ledger C-1 and the
emulator's `Pipeline::M_DCACHE_FILL`, and the fitted value becomes a measured
one. (An I-cache variant — a straight-line block larger than the 16 KiB
I-cache — is the obvious follow-up for `M_ICACHE_FILL`.)
one.

## I-cache variant

`icache_timing.asm` → `icache_timing.z64` measures the **I-cache** line-fill cost
(`M_ICACHE_FILL`, ledger C-1, currently fitted at 46 PClocks). Rather than a
load differential it runs a **straight-line block of `N = 8192` `addiu`
instructions** (32 KiB, larger than the 16 KiB I-cache), so every 32-byte fetch
line (8 instructions) misses. Each `addiu` has no interlock, so its execute cost
is exactly one PClock — the base that is subtracted:

```text
fill_PClocks = (delta * 2 - N) / (N / 8)
```

(`delta` is the COP0-`Count` span of the block; `Count` ticks once per 2
PClocks; there are `N/8` line fills.) It writes `delta` and `N` to uncached
RDRAM at phys `0x10000` / `0x10004` (past the 32 KiB code block) and prints both
via ISViewer, exactly like the D-cache ROM. Header convention, blank IPL3, and
hardware-run steps are identical — just read `delta` and `N` and apply the
formula. Its emulator runner is
`crates/rustyn64-test-harness/tests/icache_timing_rom.rs`:

```sh
cargo test -p rustyn64-test-harness --release --test icache_timing_rom -- --nocapture
# -> I-cache fill = 46.09 PClocks (our charged M_ICACHE_FILL)
```

The residual 0.09 over the charged 46 is fixed jal/jr/pipeline-fill overhead not
captured by the `N × 1` base, diluted across 1024 fills; on hardware the block
dominates identically, so the measured number is the real fill cost.

## Licence

Expand Down
5 changes: 4 additions & 1 deletion tools/mrdram-timing-rom/build.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/bin/sh
# Assemble the M(RDRAM) cached-load timing ROM with bass (ARM9 fork).
# Assemble both timing ROMs with bass (ARM9 fork): the M(RDRAM) cached-load ROM
# (D-cache fill) and the I-cache-fill ROM.
#
# bass is not vendored. Fetch + build it once (it needs one modern-g++ fix), then
# point BASS at the binary. The n64 architecture tables must sit next to the
Expand All @@ -20,4 +21,6 @@ cd "$(dirname "$0")" # run from this directory regardless of caller
: "${BASS:=bass}"
"$BASS" mrdram_timing.asm
echo "built mrdram_timing.z64 ($(wc -c < mrdram_timing.z64) bytes)"
"$BASS" icache_timing.asm
echo "built icache_timing.z64 ($(wc -c < icache_timing.z64) bytes)"
echo "For hardware: fix the header CRC (e.g. chksum64) and load via your flashcart."
154 changes: 154 additions & 0 deletions tools/mrdram-timing-rom/icache_timing.asm
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// RustyN64 -- I-cache-fill timing ROM (bass, ARM9 fork syntax).
//
// The companion to mrdram_timing.asm (which measures the D-cache fill). This one
// measures the VR4300 **I-cache** line-fill cost on real hardware: it executes a
// straight-line block of N one-PClock instructions that is LARGER than the 16 KiB
// instruction cache, so every 32-byte fetch line (8 instructions) misses. Timed
// with COP0 Count:
// fill_PClocks = (delta * 2 - N) / (N / 8)
// -- subtract the verified 1-PClock-per-instruction base, divide by the number of
// line fills. (Count ticks once per 2 PClocks; a 32-byte I-cache line is 8 * 4 B.)
//
// Results go to fixed RDRAM words (phys 0x10000, past the code) and the ISViewer
// text channel, as in the D-cache ROM. See README.md; build with build.sh.

arch n64.cpu
endian msb
output "icache_timing.z64", create
fill 0x18000 // 96 KiB: header + blank IPL3 + setup + the 32 KiB block

// MIPS register aliases.
constant r0 = 0
constant at = 1
constant t0 = 8
constant t1 = 9
constant t2 = 10
constant t3 = 11
constant t7 = 15
constant t8 = 24
constant t9 = 25
constant sp = 29
constant a0 = 4
constant a1 = 5
constant a2 = 6
constant a3 = 7
constant ra = 31

constant COUNT = 9
constant N = 8192 // instructions in the straight block (32 KiB > 16 KiB I-cache)
// Result/scratch addresses must sit PAST the loaded code: the 32 KiB block runs
// from RDRAM 0x1000 to ~0x9100, so use phys 0x10000 (64 KiB) and up.
constant RESULTS = 0xA0010000 // uncached result words (phys 0x10000)
constant ISVLEN = 0xA0010100 // our running text length (uncached)
constant ISVIEWER_WLEN = 0xB3FF0014
constant ISVIEWER_BUF = 0xB3FF0020

// ---- ROM header ----
origin 0x00000000
base 0x80000000
dw 0x80371240
dw 0x0000000F
dw 0x80001000 // entry
dw 0x00001444
dw 0x00000000 // CRC1 (fix with chksum64 for hardware)
dw 0x00000000
dw 0x00000000
dw 0x00000000
db "RUSTYN64 ICACHE "
db "TIME"
dw 0x00000000
dw 0x0000004E

origin 0x00000040
fill 0xFC0, 0x00

// ---- Code ----
origin 0x00001000
base 0x80001000
Start:
lui sp, 0x8020

// zero the ISViewer length (RDRAM is not pre-zeroed on hardware)
lui t8, ISVLEN >> 16
ori t8, t8, ISVLEN & 0xFFFF
sw r0, 0(t8)

ori t0, r0, 0 // $t0 accumulates (the block increments it)
mtc0 r0, COUNT // Count = 0
nop
jal StraightBlock // run the cold straight-line block
nop
mfc0 t3, COUNT // t3 = delta (Count units)
nop

// results: delta and N (the runner / reader computes the fill)
lui t1, RESULTS >> 16
ori t1, t1, RESULTS & 0xFFFF
sw t3, 0(t1) // [0] = delta
ori t2, r0, N
sw t2, 4(t1) // [4] = N

ori a0, t3, 0
jal PrintHex
nop
ori a0, t2, 0
jal PrintHex
nop
jal IsvFlush
nop
Spin:
j Spin
nop

// ---- the straight-line block: N one-PClock instructions, all fetch-miss ----
// Emitted at assemble time. `addiu t0,t0,1` has no interlock (its result is not
// read by the next), so each is exactly 1 PClock -- the base the runner subtracts.
StraightBlock:
variable ii = 0
while ii < N {
addiu t0, t0, 1
ii = ii + 1
}
jr ra
nop

// ---- ISViewer helpers (same as mrdram_timing.asm) ----
PrintHex:
lui t7, ISVIEWER_BUF >> 16
ori t7, t7, ISVIEWER_BUF & 0xFFFF
lui t8, ISVLEN >> 16
ori t8, t8, ISVLEN & 0xFFFF
lw t9, 0(t8)
ori a1, r0, 8
PhLoop:
srl a2, a0, 28
andi a2, a2, 0xF
sltiu a3, a2, 10
bne a3, r0, PhDigit
addiu a2, a2, 0x30
addiu a2, a2, 7
PhDigit:
addu at, t7, t9
sb a2, 0(at)
addiu t9, t9, 1
sll a0, a0, 4
addiu a1, a1, -1
bne a1, r0, PhLoop
nop
addu at, t7, t9
ori a2, r0, 0x20
sb a2, 0(at)
addiu t9, t9, 1
sw t9, 0(t8)
jr ra
nop

IsvFlush:
lui t8, ISVLEN >> 16
ori t8, t8, ISVLEN & 0xFFFF
lw t9, 0(t8)
lui t7, ISVIEWER_WLEN >> 16
ori t7, t7, ISVIEWER_WLEN & 0xFFFF
sw t9, 0(t7)
jr ra
nop
Binary file added tools/mrdram-timing-rom/icache_timing.z64
Binary file not shown.