Cpu tests - #86
Merged
Merged
Conversation
danifunker
commented
Aug 19, 2026
Contributor
- Add CPU test suite, can integrate with jitv2 for additional coverage
A self-checking test suite that runs on the emulated CPU with no OS, prints
PASS/FAIL over serial, and reports the failure count as a process exit code.
34 tests / 87 checks, green on R4400.
Harness:
- start.S: self-relocation into KSEG0, CP0 init, .bss zeroing, vector
trampolines, and an exception dispatcher that records Status/Cause/EPC/
BadVAddr/EntryHi/Context/XContext and steps over the faulting instruction
(EPC += 8 when Cause.BD, since EPC points at the branch).
- testlib.{c,h}: CHECK/CHECK_EQ/CHECK_EXC, per-test accounting, the runner,
and per-test CPU gating so a MIPS IV test is skipped on an R4400 rather
than failing.
- console.c: Z85C30 via IOC2 at 0xBFBD9830, plus the test device's PUTC/DUMP/
EXIT when present (probed, so the same binary still runs on real hardware).
- excoff.h: exception-record offsets shared by C and asm, static-asserted in
testlib.c — the handler stores through them and cannot see the C struct.
Three findings, all documented rather than just worked around:
- Physical RAM starts at 0x08000000, not 0; only the bottom 512 KB is an
alias. KSEG0 0x80200000 is a hole, and UnmappedRam swallows writes to it
silently. The suite links at 0x88200000. See docs/memory-map.md.
- o32 holds a long long in a register PAIR, so "=r"(u64) captures half a
result — addu's 0x80000000 read back as 0x8000000000000000 and looked like
a CPU bug. Built n32 instead: still ELF32 MSB, 64-bit values in single
registers. See docs/toolchain.md.
- n32 has no $t4-$t7, so start.S uses numeric registers throughout.
The handler reads FCSR only when Status.CU1 is set: the coprocessor-unusable
tests run with CU1 clear, and an unguarded cfc1 would fault inside the
handler and never return.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
71 tests / 354 checks, all green on R4400. The entire unaligned family — LWL/LWR/SWL/SWR at all 4 offsets, LDL/LDR/SDL/SDR at all 8 — passes, which is the densest bug area in the ISA and was the main thing worth knowing. Each of the three failures this turn looked like an emulator bug and was not: - GAS expands `div rs, rt` into a MACRO: a branch plus `break 0x7` for a zero divisor and another plus `break 0x6` for 0x80000000/-1, wrapped around the real instruction. The div-by-zero and overflow tests were failing against traps the CPU never raised. Fixed by `.set nomacro` in a now-standard strict prologue (noreorder must precede it) plus the explicit `div $0, rs, rt` form. Pseudo-instructions are consequently unavailable in test asm. - A `u32` address passed to `cache` is zero-extended to 0x000000008821c000 — xkuseg, not KSEG0. A Hit_* op that misses is architecturally a no-op, so every flush in the harness was silently doing nothing. Added SEXT_PTR / K1_PTR and moved the range helpers to `char *`. - Hit_Writeback_Invalidate_D writes back to the SECONDARY cache, not to memory, so an uncached KSEG1 read still saw stale data. Real R4400 behaviour, not a bug: reaching memory needs a second op against the SD target. cache_detect() reads Config.SC at startup and the helpers now cascade PD -> SD. This is why IRIX flushes both levels. All three are written up in docs/gotchas.md with the probe output that settled the last one. HI/LO after divide-by-zero and after 0x80000000/-1 are architecturally UNPREDICTABLE, so those tests assert only "no trap fires" and print the values rather than comparing them — asserting would invent a requirement the spec does not make. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
branch/: every conditional branch taken and not-taken, branch-likely nullification (the delay slot runs when taken, is nullified when not), BAL/JALR linking PC+8, a delay slot that overwrites $ra, a load in a delay slot, and exceptions in a delay slot (Cause.BD set, EPC pointing at the branch, and the branch still taken). excep/: SYSCALL, BREAK, all six T*/T*I traps, reserved instruction, coprocessor usability, EXL set on entry and cleared by ERET, EPC pointing at the faulting instruction, vector selection, and a check that the handler leaves the whole register file untouched — which is what lets the rest of the suite assert "the destination register was not written". Two more test-side findings, both in docs/gotchas.md: - 0xFC000000 is not a reserved opcode, it is `sd $zero, 0($zero)`: it stored to address 0 and reported TLBS via the XTLB vector. The real holes on these parts are primary opcodes 0x1C..0x1F, and the suite skips 0x1E because IRIS's jitv2 uses it as a region-boundary sentinel (src/mips_isa.rs:64). - The EXL test hung the suite: an exception taken with EXL already set does not update EPC, so the default handler ERETed to the test's sentinel and faulted forever. Added exl_resume_handler to start.S — a handler a test can install that resumes at a caller-chosen PC rather than through EPC. COP2 accepts either Coprocessor Unusable or Reserved Instruction and reports which: for a coprocessor that is absent rather than merely disabled, both are defensible, and pinning one would be inventing a requirement. IRIS reports RI with CE=1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
172 tests / 804 checks. R4400: 783 pass, 21 fail — 19 of those are one real
finding, and the other two are a second.
New groups:
- cp0/ read-only registers, reserved-bit masks, 64-bit CP0 access, Count/
Compare driving IP7, ERET clearing the LL bit.
- tlb/ TLBWI/TLBR round-trip over all 48 entries, TLBP hit and miss, every
page size, real translation through a mapped page, V and D bits
raising TLBL and Mod, ASID match/mismatch, refill vector + Context.
Safe only because the suite runs unmapped from KSEG0.
- fpu/ arithmetic in both formats, signed zeros, infinities, NaN
propagation, all four rounding modes across CVT.W, the fixed-mode
conversions, every comparison predicate, BC1T/BC1F/BC1TL, sticky
FCSR flags, and FR=0 pair aliasing vs FR=1 independence.
- cache/ geometry read from Config (so it works on either part), TagLo
round-trip via Index_Store/Load_Tag, cached-vs-uncached views,
Hit_Invalidate discarding without writeback, and I-cache coherency
for self-modifying code.
- mips4/ every MIPS IV addition, registered for BOTH CPUs and branching
internally: compute on R5000, raise Reserved Instruction on R4400.
mips4/mips3_control is the negative control.
Infrastructure: run/matrix.sh (R4400/R5000 x interp/JIT), run/run-prom.sh
(boot through the PROM from an mkvh image, the path the CD will use), and a
CI workflow that builds the guest binary once and runs the same binary in
every cell — which is what makes the matrix a differential test.
docs/findings.md records what the suite found and what each finding costs in
practice. docs/oracle.md is the standing rule for where expected values come
from, including the cases the suite deliberately declines to assert (HI/LO
after divide-by-zero, COP2's exception class, MTC0 width) because no spec
pins them down. docs/gotchas.md gains two more test-side mistakes: 0xFC000000
is SD rather than a reserved opcode, and (u64)(unsigned long)ptr zero-extends
under n32 — which produced a TLB miss that read exactly like a broken LWXC1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cp0/compare_sets_ip7 set Count to 0 and Compare to a small constant, which raced the wallclock-anchored counter: it passed from a cold --load-elf start and failed after a PROM boot, where Count had already run far ahead. IP7 fires on Count becoming *equal* to Compare, so a Compare written "in the past" does not fire until Count wraps through 2^32 — IRIS models that faithfully (schedule_compare_timer in src/mips_core.rs, which says so). The test now sets Compare relative to a freshly-read Count, the way a kernel does, and reports rather than asserts when the deadline never arrives. PROM boot verified end to end: `mkvh` builds an image whose volume header names the binary, and `boot -f dksc(0,2,8)cputest` at the PROM prompt loads and runs the whole suite — 784 checks. That is the mechanism the bootable CD needs, proven on a disk image. run/run-prom.sh automates it. Note the image's partition 8 spans the whole file rather than the conventional 8 sectors, matching what the IRIX 6.5.22 install CD does. docs/findings.md gains the discovery that IRIS's own multi-CC test had the same off-by-two in its encoder, so the two errors cancelled. PLAN.md phase table updated: 0-4 done, 5 partly — an EFS writer is what remains. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An "r4400" cell silently ran an R5000 earlier in this session: an --features r5k build had overwritten target/release/iris between the copy and the run. Every mips4 expectation inverted — the RI-refusal half "failed" and the compute half "passed" — with nothing in the output saying why. It took reading the banner to notice. Both run/matrix.sh and the CI workflow now check the banner (which the guest reads out of PRId) against the cell name before trusting any result. In CI that also catches a build whose cargo features silently did not take. Also adds docs/status.md: coverage per group, current results per cell, and what is deliberately not done yet — the EFS writer, the JIT cells, an FP vector generator, interrupt delivery (everything runs with IE clear, so the suite tests that interrupts become pending but never that one is taken), and supervisor/user mode. findings.md gains a fifth entry: Count can skip past Compare without IP7 firing. IP7 fires on equality, which IRIS models faithfully, but a wallclock-anchored Count advances in jumps and one jump stepped over the deadline by 3. Filed as an observation rather than a bug — it follows from the timing model, and `ci_clock` exists for cases that need determinism — but the consequence is real enough to write down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
R4400 783/19, R5000 801/2, PROM boot 784/20. Every failure maps to a recorded finding: 17 of the 19 R4400 failures are the single MIPS IV-executes-on-R4400 finding, and the remaining 2 on both CPUs are the Wired/FCSR reserved-bit masking. The whole mips4 group now passes on R5000, including multi_fp_cc, which is what confirms the c.cond.fmt fix from both directions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two src/ commits are gone from this branch and live on fix/fp-condition-code. This branch now changes nothing outside cpu-tests/, so the suite reports every finding instead of hiding one. Re-measured against an IRIS built from this branch: R4400, interpreter 783 pass / 19 fail (7 tests) R5000, interpreter 802 pass / 3 fail (3 tests) R4400, PROM boot from a mkvh image 784 pass / 20 fail (7 tests) mips4/multi_fp_cc now fails on R5000 again, with FCSR reading 0x00000000 — exactly what the condition-code bug predicts, since both compares land on cc4 and the second overwrites the first. That is the correct state here. The R4400/R5000 split is the interesting part: the mips4 group fails on R4400 and passes on R5000 for the MIPS IV-executes-when-it-should-trap finding, while multi_fp_cc does the reverse — it passes on R4400, where the test only reports because a non-zero cc is not a defined MIPS III encoding, and fails on R5000 where the instruction really executes. docs/findings.md §1 and README now say where the fix lives. status.md gains a per-test finding table and a note on why the total check count drifts by one or two between runs (compare_sets_ip7 runs two checks when the timer fires and one when it reports a skip). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…iles - run/matrix.sh and the CI workflow now build --features jitv2 instead of the v1 jit. v1 is the speculative tiered block compiler being replaced and is not what this project runs. jitv2 needs no runtime switch — it is active as soon as the feature is compiled in — so IRIS_JIT drops out entirely, and each cell now caches its own build/iris-<cpu>-<engine>. - README rewritten as a build-and-run guide: install the toolchain, make, make run, what the output means, how to get the other CPU/engine combinations, and how to add a test. The reference material stays in docs/ and is linked rather than inlined. - EMULATOR-SUPPORT-PROMPT.md and EMULATOR-SUPPORT-UPDATE-1.md deleted (347 lines). They were handoff prompts for the session that built --load-elf, the test device, mkvh and the TFTP server; all four landed, so the prompts are spent. PLAN.md's two links to them now describe what shipped instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.