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
12 changes: 12 additions & 0 deletions .github/workflows/cpu-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ jobs:
sudo apt-get update
sudo apt-get install -y gcc-mips-linux-gnu binutils-mips-linux-gnu

# The FP expectation tables are generated by gen/fpvectors.py and checked
# in, so that building the suite needs nothing but the cross toolchain.
# Regenerating them here and failing on a diff is what stops the script
# and its output from drifting apart. `make vectors` also runs the
# generator's --check pass, which cross-checks its exact rational
# arithmetic against the runner's own FPU before writing anything.
- name: Verify the generated FP vectors are up to date
run: |
make -C cpu-tests vectors
git diff --exit-code cpu-tests/tests/fpu/fpvectors.c \
cpu-tests/tests/fpu/fpvectors.h

- name: Build cputest.elf
run: make -C cpu-tests

Expand Down
4 changes: 4 additions & 0 deletions cpu-tests/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Build products: the guest binary and its objects, the bootable image, the
# per-cell IRIS builds the matrix runner caches, and run logs.
build/

# The generator is run in place; its bytecode cache is not a build product
# anyone wants.
__pycache__/
10 changes: 9 additions & 1 deletion cpu-tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,17 @@ DEPS := $(OBJS:.o=.d)
# ever does appear, the link fails loudly rather than pulling in the wrong ABI.
LIBGCC :=

.PHONY: all clean run dis syms image check-size
.PHONY: all clean run dis syms image check-size vectors
all: $(TARGET)

# Regenerate the IEEE-754 expectation tables in tests/fpu/. Needs python3, and
# nothing else does — the generated files are checked in so that building the
# suite stays a cross-toolchain-only affair. --check cross-checks the exact
# rational arithmetic against the host FPU before anything is written.
vectors:
cd $(CURDIR) && python3 gen/fpvectors.py --check
cd $(CURDIR) && python3 gen/fpvectors.py

$(BUILD)/%.o: %.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -MMD -MP -c -o $@ $<
Expand Down
2 changes: 1 addition & 1 deletion cpu-tests/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ cpu-tests/
| **0** | Toolchain, `hello.elf`, load address, boot syntax | **done** — n32 cross GCC, links and relocates to `0x88200000` |
| **1** | Harness + `alu`, `muldiv`, `mem`, `branch` | **done** — 85 tests green |
| **2** | `excep`, `cp0`, `tlb` | **done** |
| **3** | `fpu`, `mips4` with the R4400-must-RI differential | **done** — the differential found two bugs |
| **3** | `fpu`, `mips4` with the R4400-must-RI differential | **done** — the differential found two bugs; `fpu` has since grown to 88 tests across eight files and `gen/fpvectors.py` now generates its expectation tables, which is where findings 6-10 came from |
| **4** | `cache`; JIT-vs-interp matrix in CI | **done** — `run/matrix.sh`, `.github/workflows/cpu-tests.yml` |
| **5** | Volume header → bootable disk → **EFS CD** | **partly** — `mkvh` image boots through the PROM end to end; the EFS partition is the remaining piece |

Expand Down
38 changes: 31 additions & 7 deletions cpu-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,20 @@ cargo build --release # in the repo root, first
make run # loads the ELF straight into RAM and runs it
```

`make run` takes about a minute. You get a line per test and a summary:
`make run` takes a few minutes — 240 tests, some of which take real exceptions
or sweep the caches. You get a line per test and a summary:

```
alu/addu_sign_extends ...................... PASS
...
RESULT: 783 checks passed, 19 failed (172 tests)
IRIS-CPUTEST-DONE rc=19
RESULT: 2041 checks passed, 121 failed (240 tests)
IRIS-CPUTEST-DONE rc=100
```

The exit code **is** the failure count. `IRIS-CPUTEST-DONE` is the token to
match on if you are scripting it.
The exit code **is** the failure count, saturated at 100 so that it can never
collide with the harness's own 127 ("unknown CPU, refusing to run"). The R4400
cell exceeds that today, so read the `RESULT:` line for the real number.
`IRIS-CPUTEST-DONE` is the token to match on if you are scripting it.

### Other CPUs and engines

Expand Down Expand Up @@ -72,8 +75,8 @@ the ELF, and jumps to it. This is what the bootable CD will use.

| | pass | fail | failing tests |
|---|---:|---:|---|
| R4400 | 783 | 19 | 7 |
| R5000 | 802 | 3 | 3 |
| R4400 | 2041 | 121 | 29 |
| R5000 | 2095 | 37 | 13 |

Every failure is a known finding, listed in [docs/findings.md](docs/findings.md).
Anything else is new — start with [docs/gotchas.md](docs/gotchas.md), which
Expand Down Expand Up @@ -106,15 +109,36 @@ use them, or the assembler will quietly rewrite your instructions (see
A whole new area also needs a `struct test_group` and one line in
`harness/tests.c`.

FP tests have their own conventions — `AF` rather than `A` for any block with
an FP instruction, never a `$f*` clobber, values crossing through memory — all
of them collected in `tests/fpu/fpu_common.h`, which also provides the
`observe_s`/`observe_d` helpers that run one operation and report what the FPU
and the CPU each did about it.

## Layout

```
harness/ startup, exception vectors, CHECK macros, console
tests/ identity alu muldiv mem branch excep cp0 tlb fpu cache mips4
gen/ fpvectors.py — computes the FP expectation tables
run/ run-local.sh matrix.sh run-prom.sh bare.toml boot.toml
docs/ findings gotchas status oracle memory-map toolchain
```

`tests/fpu/` is eight files rather than one — arithmetic, traps, denormals,
comparisons, generated vectors, double precision, the FR=0 register file, and
the odd corners — sharing `fpu_common.h` for the conventions that make FP tests
work at all under `-msoft-float`. [docs/status.md](docs/status.md) has the
breakdown.

### Generated expectations

`make vectors` regenerates `tests/fpu/fpvectors.{c,h}` from `gen/fpvectors.py`,
which computes IEEE-754 results with exact rational arithmetic and cross-checks
them against the host FPU before writing anything. The generated files are
checked in, so building the suite needs only the cross toolchain; python3 is
needed only to change them.

## More

- [docs/findings.md](docs/findings.md) — what the suite found, and what each
Expand Down
182 changes: 180 additions & 2 deletions cpu-tests/docs/findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ This is also the case the whole R4400-vs-R5000 axis exists for.

## 2. MIPS IV instructions execute on the R4400 — **open**

*Found by the whole `mips4/` group. 19 of the 21 R4400 failures.*
*Found by the whole `mips4/` group, and by `mips4_fp.c` in more detail.*

Every MIPS IV instruction the suite tries runs to completion on an R4400 build
instead of raising Reserved Instruction:
Expand All @@ -83,7 +83,10 @@ instead of raising Reserved Instruction:
| `PREF` | RI | executes |
| `RECIP.fmt` / `RSQRT.fmt` | RI | executes |
| `MOVF` / `MOVT` (MOVCI) | RI | executes |
| COP1X (`LWXC1`, `MADD.fmt`, …) | RI | executes |
| COP1X (`LWXC1`, `LDXC1`, `SWXC1`, `SDXC1`, `PREFX`) | RI | executes |
| `MADD`/`MSUB`/`NMADD`/`NMSUB`, both formats | RI | executes |
| `MOVF.fmt`/`MOVT.fmt`/`MOVN.fmt`/`MOVZ.fmt` | RI | executes |
| `RECIP.D` / `RSQRT.D` | RI | executes |

IRIS decodes the MIPS IV additions unconditionally: nothing consults `PRId`, and
the `r5k` cargo feature changes cache geometry and CPU identity but not which
Expand Down Expand Up @@ -180,6 +183,181 @@ a normal workload is not something this suite can answer.
The test now sets `Compare` relative to a freshly-read `Count` — the way a
kernel does — and reports the skip instead of failing on it.


---

## 6. A trapped FP exception still writes its result and its Flag bit — **open**

*Found by the whole `fpu/trap_*` group. Two rules, one root cause.*

The R4000 manual states both halves of the rule outright:

> When a floating-point exception is taken, no results are stored, and the only
> state affected is the Cause bit.

and, for the Flag field specifically:

> When a floating-point exception is taken, the flag bits are not set by the
> hardware; floating-point exception software is responsible for setting these
> bits before invoking a user handler.

IRIS does the arithmetic, writes the destination register, then decides whether
to trap:

```rust
let result = f32::from_bits(...) + f32::from_bits(...);
(self.fpr_write_w)(&mut self.core, fd_reg, result.to_bits());
self.fpu_update_fcsr() // <- the trap decision happens here
```

and `fpu_update_fcsr` ORs the host's exception flags into the Flag field before
testing them against the Enable field, so both happen unconditionally. Every
trapped case shows it: with Invalid enabled, `div.s $f4, 0.0, 0.0` traps *and*
leaves a quiet NaN in `$f4` *and* sets Flag.V.

**How much it matters.** The destination half is the one with teeth. IEEE 754
trap handlers exist to substitute a result, and a handler that declines to
substitute one — because it only counts the event, or because it decides the
default was fine — leaves the register holding a value the architecture says
was never written. Under IRIS that value is the IEEE default result, which is
usually what the handler would have supplied anyway; on hardware it is the
destination's old contents. The Flag half is more benign: software that sets
the flag itself, as the manual tells it to, simply sets a bit that is already
set.

`c.cond.fmt` is half an exception: `exec_fcc_s` returns before writing the
condition bit when it traps, so the *result* rule holds there — but it sets
Cause.V and Flag.V together on the way out, so the *flag* rule does not.
`fpu/cmp_trap_on_signal` checks both and reports the second.

---

## 7. FCSR Cause bits accumulate instead of being rewritten — **open**

*Found by `fpu/cause_per_instruction`.*

> The Cause bits are written by each floating-point operation... they identify
> the exceptions raised by the last floating-point operation.

and, in the state-saving section:

> The Cause field of the Control/Status register holds the results of only one
> instruction.

That is the entire difference between the Cause field and the Flag field: Cause
is what the *last* operation did, Flags are what *every* operation since the
last clear did. IRIS ORs into both:

```rust
self.core.fpu_fcsr |= causes;
self.core.fpu_fcsr |= flags & FCSR_FM;
```

so Cause is a second, redundant copy of Flags. A divide by zero followed by an
exact addition leaves Cause.Z set, where hardware clears it.

**How much it matters.** This is the field an FP exception handler reads to
find out what it is being asked to fix, and the field a program reads to ask
"did *this* operation raise anything". Under IRIS the answer is always "this
operation, or any operation since the last CTC1". Nothing in IRIX depends on it
today — the kernel's FP assist path is only entered by exceptions IRIS does not
generate (finding 8) — but it is the sort of thing a numerical program that
polls Cause after each step would get wrong, silently.

---

## 8. Denormals never trap, and FCSR.FS is inert — **open**

*Found by the `fpu/denorm_*` group.*

An R4400 does not compute with denormalized numbers. It refuses them, in both
directions, and lets software finish the job — R4000 manual, chapter 7:

| condition | R4400 |
|---|---|
| denormalized operand (except to a Compare) | Unimplemented Operation (Cause.E) |
| quiet NaN operand (except to a Compare) | Unimplemented Operation (Cause.E) |
| denormalized result, FS clear | Unimplemented Operation (Cause.E) |
| denormalized result, FS set, U and I disabled | flushed to a signed zero, Cause.U and Cause.I set |
| denormalized result, FS set, U or I enabled | Unimplemented Operation (Cause.E) |

Cause.E has no Enable bit and no Flag bit: "whenever this exception occurs, an
unimplemented exception trap is taken".

IRIS raises E in exactly one situation — an underflow with the Underflow trap
enabled — and computes everything else on the host FPU, which handles
denormals in hardware. `FCSR.FS` is storable and readable (`fpu/fs_bit_round_trip`
passes) but no execution path consults bit 24 at all, so setting it changes
nothing: `2^-126 * 0.5` yields the denormal `0x00400000` with FS set or clear,
where hardware gives `+0` with Cause.U and Cause.I in the first case and a trap
in the second.

**How much it matters.** For the *numbers*, IRIS is arguably nicer than the
hardware: it delivers the correctly-rounded denormal result that the R4400's
software handler would have had to compute, and does it without the trap. What
is lost is the ability to see the FP-assist path at all — a kernel's
Unimplemented Operation handler is dead code under IRIS, and an IRIX
installation whose libc sets FS for flush-to-zero gets denormals anyway.

The R5000 side of each of these tests is reported rather than asserted: the
R4000 manual is in-repo and pins the R4400, the VR5000's is not, and denormal
handling is exactly the kind of thing two implementations of one architecture
are permitted to differ on. See [oracle.md](oracle.md).

---

## 9. A signalling NaN raises Invalid only where a quiet one would — **open**

*Found by `fpu/cmp_snan_any_pred`.*

Two rules govern Invalid on a compare, and IRIS implements one of them:

1. If either operand is **any** NaN and the predicate's high cond bit is set
(the eight signalling predicates, `SF NGLE SEQ NGL LT NGE LE NGT`), Invalid
is raised. IRIS does this correctly — `fpu/cmp_signalling_qnan` passes for
all sixteen predicates.
2. If either operand is a **signalling** NaN, Invalid is raised whatever the
predicate is: Table 7-2 lists "Signaling NaN source" as V with the trap
enabled and V with it disabled, and the Invalid Operation list names
"Comparison or a Convert From Floating-point Operation on a signaling NaN".
IRIS does not do this.

```rust
if (funct_val & 0x8) != 0 && (fs_val.is_nan() || ft_val.is_nan()) {
```

`is_nan()` does not distinguish the two kinds, and the predicate test gates the
whole check — so `c.eq.s` against a signalling NaN is silent, in both formats.

**How much it matters.** Little in practice: signalling NaNs only exist where a
program deliberately creates them, which is a debugging technique rather than
something IRIX or its applications do. It is a precise, cheap-to-fix deviation
in code that is otherwise exactly right, which is the main reason to record it.

---

## 10. ABS.fmt and NEG.fmt never raise Invalid — **open**

*Found by `fpu/snan_operands`.*

The manual is unusually explicit that these two are not the bit-twiddling
operations they look like:

> A move (MOV) operation is not considered to be an arithmetic operation, but
> absolute value (ABS) and negate (NEG) are considered to be arithmetic
> operations and cause this exception if one or both operands is a signaling
> NaN.

`exec_fabs_s` and `exec_fneg_s` compute the result and end with
`handle_exec_complete()` — they never reach `fpu_update_fcsr`, so no FCSR field
is touched by either instruction under any circumstances. `ADD.S` with the same
signalling NaN operand *does* set Invalid, so the two instructions disagree
with each other about the same operand.

**How much it matters.** The same as finding 9, and for the same reason. Worth
recording because the fix is one line each and because the manual singles these
two instructions out precisely because they are easy to get wrong.

---

## Handled correctly
Expand Down
Loading
Loading