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
464 changes: 464 additions & 0 deletions .coderabbit.yaml

Large diffs are not rendered by default.

15 changes: 11 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ Architecture (the load-bearing facts — read `docs/architecture.md`):
**Phase 1 in progress; tagged release still v0.1.0.** The **VR4300 executes instructions**: the
canonical 187.5 MHz clock (ADR 0006), the five-stage pipeline (ADR 0007), the MIPS III integer
set, COP0, the TLB + micro-ITLB, the exception model, interrupts, `CACHE`, COP1 (control,
register file, `ADD`/`SUB`/`MUL`/`DIV`, `ABS`/`MOV`/`NEG`, and enabled FP traps), and **PI DMA**
— the last pulled forward from Phase 5 because n64-systemtest loads its own ELF through it.
register file, `ADD`/`SUB`/`MUL`/`DIV`, `ABS`/`MOV`/`NEG`, the compares, the conversions and
enabled FP traps), and **PI DMA** — the last pulled forward from Phase 5 because n64-systemtest
loads its own ELF through it.

FP arithmetic runs on a **soft-float core** (`crates/rustyn64-cpu/src/softfloat.rs`), not on
Rust's `f32`/`f64` operators. That is not gratuitous: the native operators discard the exact
Expand All @@ -76,10 +77,11 @@ the VR4300's refusal to produce subnormals as a separate layer.
executes anything. A green `cargo test` still does not mean a subsystem works — check
`docs/STATUS.md`.

**Phase 1's exit criterion is not met**: n64-systemtest reports **2,682 failing assertions**
**Phase 1's exit criterion is not met**: n64-systemtest reports **1,098 failing assertions**
(it does now run its whole corpus and report). Do **not** tag v0.2.0 until it is `Failed: 0` —
the criterion is an oracle number, and that is the point of it. The dominant remaining block is
the still-undecoded COP1 funct space (`C.cond.fmt` and the conversions), roughly 1,700 of them.
the unmaskable **unimplemented-operation** cause (bit 17), which the VR4300 raises for subnormal
operands/results and for a quiet-NaN operand to an arithmetic operation.

## Where things live

Expand Down Expand Up @@ -289,6 +291,11 @@ in this repo, which is worth fixing even when the suggested wording is not.
`docs/accuracy-ledger.md` with its provenance. Adjusting one until a ROM passes makes every
later timing result unfalsifiable. Currently unmeasured: `M` (memory access time), the
exception-epilogue cost, CP0I, RDRAM bank-state costs.
- **NaN classification on the VR4300 is INVERTED from IEEE-754:2008**: significand MSB **set**
means *signalling*, so `f32::NAN` (`0x7FC0_0000`) raises Invalid here. This looks like a bug on
every reading and is not — it is the legacy MIPS convention, corroborated by the processor's own
default NaN result (`0x7FBF_FFFF`, MSB clear) being quiet only under it. Never "correct" it back
to IEEE; see ledger **C-12** and the test that asserts `is_snan_f32(f32::NAN)` on purpose.
- Say "master clock" only with its rate. The sources use **MasterClock = 62.5 MHz**; this
project's master tick is **187.5 MHz**; ADR 0001 used it for 93.75 MHz. See `docs/glossary.md`.
- `unsafe` is allowed only in the frontend and FFI. Enforced: every chip crate and `-core` carry
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@ All notable changes to RustyN64 are documented here. The format is based on
The next rung is `v0.2.0 "Interpreter"` — the VR4300 (see
[`to-dos/VERSION-PLAN.md`](to-dos/VERSION-PLAN.md)).

### Added — the COP1 compares and conversions, and a corrected NaN convention

`C.cond.fmt`, the `CVT` family, and `ROUND`/`TRUNC`/`CEIL`/`FLOOR` to `.W`/`.L` now decode and
execute. They were implemented in `fpu.rs` all along but unreachable: decode admitted only
`funct 0..=3` and `5..=7` in the `.S`/`.D` formats, and never admitted the **integer** source
formats `.W`/`.L` at all — so every integer-to-float conversion was a silent no-op too.

`ROUND`/`TRUNC`/`CEIL`/`FLOOR` take their rounding mode from the **opcode** and ignore `FCSR.RM`;
`CVT.W`/`CVT.L` consult it. That is the entire difference between the two families, and getting it
wrong would be invisible whenever `RM` happened to match.

**n64-systemtest: 2,682 → 1,468.**

### Fixed — the VR4300 NaN convention is inverted from IEEE-754:2008

A NaN is **signalling** when its significand's MSB is **set** — the legacy MIPS convention, the
opposite of IEEE-754:2008. `0x7FC0_0000`, which Rust produces as `f32::NAN` and everything else
calls quiet, raises Invalid on this processor.

Established from the oracle's own expectations, which name their constants the IEEE way and then
assert the opposite behaviour; corroborated independently by the fact that the VR4300's default
NaN *result* is `0x7FBF_FFFF` (MSB clear), which under IEEE would be a signalling NaN that
re-traps on first use, and under this convention is an ordinary quiet one.

**n64-systemtest: 1,468 → 1,098**, taking the compare block from 42 failures apiece to **zero
across all sixteen tests**. Accuracy ledger **C-12**.

### Added — soft-float arithmetic with exact IEEE flags and all four rounding modes

New `crates/rustyn64-cpu/src/softfloat.rs`. Both formats and all four arithmetic operations are
Expand Down
91 changes: 90 additions & 1 deletion crates/rustyn64-cpu/src/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,37 @@ pub const fn decode(word: u32) -> Decoded {
// result never left the callee. That accounted for the whole
// `Result after <op>` failure block, which had been read as an
// FPU arithmetic fault for nine rounds (ledger C-10).
0o20 | 0o21 if matches!(word & 0o77, 0..=3 | 5..=7) => Decoded {
// `funct` 4 is `SQRT`, which has no implementation yet and so
// stays `Cop1Unimplemented` rather than becoming a wrong
// result. Everything else in the S/D formats is wired:
//
// | `funct` | Operation |
// | --- | --- |
// | `0..=3` | `ADD` / `SUB` / `MUL` / `DIV` |
// | `5..=7` | `ABS` / `MOV` / `NEG` |
// | `0o10..=0o17` | `ROUND`/`TRUNC`/`CEIL`/`FLOOR` to `.L` then `.W` |
// | `0o40`/`0o41`/`0o44`/`0o45` | `CVT.S` / `CVT.D` / `CVT.W` / `CVT.L` |
// | `0o60..=0o77` | `C.cond.fmt`, the low 4 bits being the condition |
0o20 | 0o21
if matches!(
word & 0o77,
0..=3 | 5..=7 | 0o10..=0o17 | 0o40 | 0o41 | 0o44 | 0o45 | 0o60..=0o77
) =>
{
Decoded {
op: Op::FpArith,
..base
}
}
// The **integer** source formats, `.W` (20) and `.L` (21).
//
// Easy to miss: `CVT.S.W` carries its source format in the same
// `fmt` field, so a decoder that only admits 16/17 leaves every
// integer-to-float conversion a silent no-op — the same shape of
// gap that made `MOV.fmt` cost nine rounds. Only `CVT.S` and
// `CVT.D` are defined from these formats; converting an integer
// to an integer is not an instruction.
0o24 | 0o25 if matches!(word & 0o77, 0o40 | 0o41) => Decoded {
op: Op::FpArith,
..base
},
Expand Down Expand Up @@ -1251,6 +1281,65 @@ mod tests {
assert_eq!(decode(enc(0o20, 4)).op, Op::Cop1Unimplemented);
}

/// **The compares and conversions must decode.** They are implemented in
/// `fpu.rs` and were unreachable for the same reason `MOV` was: the decode
/// arm admitted only `funct 0..=3` and `5..=7`.
///
/// Enumerated rather than spot-checked. The failure mode here is a *gap* in
/// a range, and a gap is exactly what a single representative encoding does
/// not find.
#[test]
fn the_compares_and_conversions_decode_rather_than_no_op() {
let enc = |fmt: u32, funct: u32| {
(0o21 << 26) | (fmt << 21) | (2 << 16) | (3 << 11) | (4 << 6) | funct
};
for fmt in [0o20u32, 0o21] {
// ROUND/TRUNC/CEIL/FLOOR to .L (8..=11) then to .W (12..=15).
for funct in 0o10..=0o17u32 {
assert_eq!(
decode(enc(fmt, funct)).op,
Op::FpArith,
"fmt {fmt:#o} funct {funct:#o}"
);
}
// CVT.S / CVT.D / CVT.W / CVT.L.
for funct in [0o40u32, 0o41, 0o44, 0o45] {
assert_eq!(
decode(enc(fmt, funct)).op,
Op::FpArith,
"CVT funct {funct:#o}"
);
}
// All sixteen C.cond.fmt forms.
for funct in 0o60..=0o77u32 {
assert_eq!(
decode(enc(fmt, funct)).op,
Op::FpArith,
"C.cond funct {funct:#o}"
);
}
}
// The INTEGER source formats. Easy to miss, because `CVT.S.W` carries
// its source format in the same field as `.S`/`.D` — a decoder that
// admits only 16/17 leaves every integer-to-float conversion a no-op.
for fmt in [0o24u32, 0o25] {
for funct in [0o40u32, 0o41] {
assert_eq!(
decode(enc(fmt, funct)).op,
Op::FpArith,
"fmt {fmt:#o} funct {funct:#o}"
);
}
}
// `SQRT` has no implementation, so it must stay unimplemented rather
// than being swept in by a too-wide range.
assert_eq!(
decode(enc(0o20, 4)).op,
Op::Cop1Unimplemented,
"SQRT is still unwired"
);
}

/// **`MOV.fmt` (funct 6) must decode.** With the arm admitting only
/// `funct <= 3` it did not, and executed as a silent no-op.
///
Expand Down
116 changes: 84 additions & 32 deletions crates/rustyn64-cpu/src/fpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,26 +238,49 @@ impl<T> Outcome<T> {
}
}

/// Is this `f32` a **signalling** NaN?
/// Is this `f32` a **signalling** NaN *as the VR4300 classifies one*?
///
/// The distinction matters: a signalling NaN raises Invalid, a quiet one does
/// not. IEEE-754 puts the quiet bit at the top of the mantissa, so an
/// `is_nan()` check alone cannot tell them apart — and treating every NaN as
/// signalling raises Invalid on ordinary quiet-NaN propagation.
/// # The convention is inverted from IEEE-754:2008
///
/// IEEE-754:2008 says the significand's MSB **set** means *quiet*. The VR4300
/// predates that edition and uses the **legacy MIPS convention**, where the
/// significand MSB **set** means *signalling*. So `0x7FC0_0000` — the pattern
/// every modern language calls a quiet NaN, and what Rust's `f32::NAN` is — is
/// a **signalling** NaN to this processor, and raises Invalid.
///
/// # How this was established
///
/// Not from a manual: from n64-systemtest's own expectations, which name their
/// constants by the IEEE convention and then assert the opposite behaviour.
/// For a non-signalling compare (`C.EQ`, `C.F`, …) it expects
/// `QUIET_NAN_START_32` (`0x7FC0_0000`, MSB set) to raise Invalid and
/// `SIGNALLING_NAN_END_32` (`0x7FBF_FFFF`, MSB clear) to raise nothing. The
/// signalling compare forms (`C.SF`, `C.SEQ`, …) raise Invalid for both, which
/// is the ordinary IEEE rule for those forms and so does not distinguish them.
///
/// The corroboration that makes this more than a curve fit: the VR4300's own
/// default NaN result is `0x7FBF_FFFF`, MSB **clear**. Under IEEE that would be
/// a processor whose invalid-operation result is a *signalling* NaN — absurd,
/// since it would re-trap on first use. Under this convention it is exactly
/// what it should be, a quiet one.
///
/// Accuracy ledger **C-12**.
#[must_use]
pub const fn is_snan_f32(v: f32) -> bool {
let b = v.to_bits();
// NaN with the quiet bit (mantissa MSB) CLEAR, and a non-zero payload.
b & 0x7F80_0000 == 0x7F80_0000 && b & 0x0040_0000 == 0 && b & 0x003F_FFFF != 0
// Exponent all ones, significand MSB SET. No payload check is needed: the
// MSB being set already makes the significand non-zero, so this cannot
// catch an infinity.
b & 0x7F80_0000 == 0x7F80_0000 && b & 0x0040_0000 != 0
}

/// Is this `f64` a **signalling** NaN?
/// Is this `f64` a **signalling** NaN as the VR4300 classifies one?
///
/// See [`is_snan_f32`] — the convention is inverted from IEEE-754:2008.
#[must_use]
pub const fn is_snan_f64(v: f64) -> bool {
let b = v.to_bits();
b & 0x7FF0_0000_0000_0000 == 0x7FF0_0000_0000_0000
&& b & 0x0008_0000_0000_0000 == 0
&& b & 0x0007_FFFF_FFFF_FFFF != 0
b & 0x7FF0_0000_0000_0000 == 0x7FF0_0000_0000_0000 && b & 0x0008_0000_0000_0000 != 0
}

///
Expand Down Expand Up @@ -750,34 +773,61 @@ pub fn to_i64(v: f64, mode: Rounding) -> Outcome<i64> {
mod tests {
use super::*;

/// A **signalling** NaN raises Invalid; a **quiet** one does not. Treating
/// every NaN as signalling raises Invalid on ordinary NaN propagation, which
/// is wrong and noisy.
/// **The VR4300 NaN convention is inverted from IEEE-754:2008**: the
/// significand MSB **set** means *signalling*, not quiet.
///
/// So `0x7FC0_0001` — what every modern language calls a quiet NaN, and
/// what Rust produces — raises Invalid here, and `0x7F80_0001` does not.
/// The bit patterns are named for what they are *on this processor*, since
/// naming them the IEEE way is what made the original implementation
/// backwards. Accuracy ledger C-12.
#[test]
fn only_a_signalling_nan_raises_invalid() {
let snan = f32::from_bits(0x7F80_0001);
let qnan = f32::from_bits(0x7FC0_0001);
assert!(is_snan_f32(snan), "quiet bit clear, payload non-zero");
assert!(!is_snan_f32(qnan), "quiet bit set");
fn the_signalling_nan_is_the_one_with_the_significand_msb_set() {
let signals_here = f32::from_bits(0x7FC0_0001); // IEEE would call this quiet
let quiet_here = f32::from_bits(0x7F80_0001); // IEEE would call this signalling
assert!(
is_snan_f32(signals_here),
"MSB set is SIGNALLING on the VR4300"
);
assert!(!is_snan_f32(quiet_here), "MSB clear is quiet");
assert!(!is_snan_f32(f32::INFINITY), "infinity is not a NaN");

assert!(add_s(snan, 1.0, Rounding::Nearest).flags.invalid);
assert!(add_s(signals_here, 1.0, Rounding::Nearest).flags.invalid);
assert!(
!add_s(qnan, 1.0, Rounding::Nearest).flags.invalid,
!add_s(quiet_here, 1.0, Rounding::Nearest).flags.invalid,
"a quiet NaN propagates quietly"
);

// Rust's own NaN is signalling to this processor. Stated explicitly
// because it is the case most likely to be reintroduced by someone
// "fixing" the convention back to IEEE.
assert!(is_snan_f32(f32::NAN), "even f32::NAN signals here");
}

/// The same, for doubles — the quiet bit sits at a different position, so
/// this is not a free consequence of the `f32` case.
/// The same, for doubles — the bit sits at a different position, so this is
/// not a free consequence of the `f32` case.
#[test]
fn the_double_precision_signalling_bit_is_at_bit_51() {
let signals_here = f64::from_bits(0x7FF8_0000_0000_0001);
let quiet_here = f64::from_bits(0x7FF0_0000_0000_0001);
assert!(is_snan_f64(signals_here));
assert!(!is_snan_f64(quiet_here));
assert!(add_d(signals_here, 1.0, Rounding::Nearest).flags.invalid);
assert!(!add_d(quiet_here, 1.0, Rounding::Nearest).flags.invalid);
}

/// The processor's own default NaN result must be **quiet by its own
/// convention**, or every invalid operation would produce a value that
/// re-traps the moment anything touches it.
///
/// This is the corroboration that the inverted convention is real rather
/// than a curve fit to the compare tests: `0x7FBF_FFFF` is the value
/// hardware delivers, and it is only sane under the VR4300 reading.
#[test]
fn the_double_precision_quiet_bit_is_at_bit_51() {
let snan = f64::from_bits(0x7FF0_0000_0000_0001);
let qnan = f64::from_bits(0x7FF8_0000_0000_0001);
assert!(is_snan_f64(snan));
assert!(!is_snan_f64(qnan));
assert!(add_d(snan, 1.0, Rounding::Nearest).flags.invalid);
assert!(!add_d(qnan, 1.0, Rounding::Nearest).flags.invalid);
fn the_default_nan_result_is_quiet_by_the_vr4300_convention() {
use crate::softfloat::{F32, F64};
assert!(!is_snan_f32(f32::from_bits(F32.default_nan() as u32)));
assert!(!is_snan_f64(f64::from_bits(F64.default_nan())));
}

/// **`x/0` is `DivByZero`; `0/0` is Invalid.** They are different flags, and a
Expand Down Expand Up @@ -985,7 +1035,8 @@ mod tests {
/// on its own here.
#[test]
fn the_signalling_compare_forms_raise_on_a_quiet_nan() {
let qnan = f32::from_bits(0x7FC0_0001);
// Quiet **by the VR4300 convention**: significand MSB clear (C-12).
let qnan = f32::from_bits(0x7F80_0001);
assert!(!is_snan_f32(qnan), "it really is quiet");

let out = compare_s(qnan, 1.0, 2); // C.EQ
Expand All @@ -1001,7 +1052,8 @@ mod tests {
/// non-signalling forms.
#[test]
fn a_signalling_nan_operand_raises_for_every_condition() {
let snan = f32::from_bits(0x7F80_0001);
// Signalling **by the VR4300 convention**: significand MSB set (C-12).
let snan = f32::from_bits(0x7FC0_0001);
for cond in 0..16u8 {
assert!(
compare_s(snan, 1.0, cond).flags.invalid,
Expand Down
Loading