Skip to content

Cpu tests - #86

Merged
techomancer merged 9 commits into
techomancer:mainfrom
danifunker:cpu-tests
Aug 19, 2026
Merged

Cpu tests#86
techomancer merged 9 commits into
techomancer:mainfrom
danifunker:cpu-tests

Conversation

@danifunker

Copy link
Copy Markdown
Contributor
  • Add CPU test suite, can integrate with jitv2 for additional coverage

danifunker and others added 9 commits August 19, 2026 09:24
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>
@techomancer
techomancer merged commit f1a10b2 into techomancer:main Aug 19, 2026
2 of 6 checks passed
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