A sprite reading the buffer it draws into, linear, from beside where it draws, is a blur in place: its texture is held for the render pass and read as the buffer was before it, rather than the pass ending at every column whose page the last one wrote. The rule matched a UV box exactly two texels larger than the draw on both axes at a bias of one; a blur sampling an axis on texel centres, or at the texture's edge, has fewer extra texels there, and one reaching a few texels aside has a larger bias. The rule now takes zero to two extra texels an axis and a bias up to four. A game blooming through such a blur, a column of 32 at a time: 80 render passes a frame become 33, with 2130 of every 2400 passes before holding one to three primitives. tests/swdraw blur_in_place: a buffer blurred in place a column at a time every frame, brightened until white and read back on every renderer, in at most two render passes a frame on paraLLEl-GS.
What the tests under tests/ cover
Run everything with sh tests/run-all.sh. Six of the suites score the
emulator against console captures and want a checkout to compare against:
PS2AUTOTESTS=/path/to/ps2autotests ee, fpu, iop, mmi, vif, vu
PS1TESTS=/path/to/ps1-tests iop, for the GTE and MDEC oracles
git clone https://github.com/unknownbrackets/ps2autotests
git clone https://github.com/JaCzekanski/ps1-tests
Without them those suites skip rather than fail, so the script is still worth running with neither set.
SANITIZER=undefined sh tests/run-all.sh builds under UBSan, and
everything that links emulator code is clean under it. Two suites ignore
the variable on purpose: faultstress and spsc are concurrency tests
and build with -fsanitize=thread unconditionally, since that is what
they are for. fpaudit honours it but drops it for jit_stage, which
emits and runs machine code of its own that ASan's shadow mapping does
not survive.
Do a clean build between sanitizers with different define sets rather than reusing objects.
The two kinds of harness
Most ops are scored twice, and the pair is deliberate.
A transcribing harness copies the semantics out of the emulator and scores the copy against the capture. It is cheap, has no link dependencies, and catches a wrong answer.
A real-code harness links the emulator's own translation unit and calls the ops through their real entry points. It catches what a transcription cannot: drift between the copy and the code, undefined behaviour under a sanitizer, and mistakes in the parts a transcription quietly reimplements correctly.
Keeping both is what found the DIV_S bug -- hwfpu.c scored 36 of 36 on
a block where hwrealfpu.cpp scored 27, and only one of them could be
right. Where the two disagree, that disagreement is the finding.
Coverage
| Suite | Harness | Cases | Through |
|---|---|---|---|
| mmi | hwreal.cpp | 2690 across 101 ops | MMI.cpp |
| mmi | hwelem.c, hwpmfhl.c, hwsa.c | 1933 | transcription |
| mmi | hwscore.c | 428, not printed as a count | transcription |
| ee | hwrealee.cpp | 944 across 56 ops | R5900OpcodeImpl.cpp |
| ee | hwalu, hwload, hwstore, hwmuldiv, hwbranch | 1186 | transcription |
| ee | hwspr.cpp | 448 | SPR.cpp |
| ee | hwperf.c | 85 | transcription |
| ee | hwcpcond.cpp | 6 | COP0.cpp |
| ee | hwgslabel.cpp | 4 | Gif_Unit.cpp |
| fpu | hwrealfpu.cpp | 939 across 25 ops | FPU.cpp |
| fpu | hwfpu.c, hwfcr.c, hwbranch.c | 595 | ps2float.c + transcription |
| iop | hwrealiop.cpp | 563 across 33 ops | R3000AOpcodeTables.cpp |
| iop | hwalu, hwlsu, hwmuldiv, hwbranch | 722 | transcription |
| iop | hwgtefuzz.cpp | 1100 | IopGte.cpp |
| vu | hwrealvu.cpp | 479 across 28 ops | VUops.cpp, VU0.cpp |
| vu | hwclip, hwrandom, hwint, hwefu, hwfdiv, hwctc2 | 295 | transcription |
| vif | hwunpack.cpp | 42 formats + 16 write modes + 4 layout | Vif_Unpack.cpp |
| emitter | hwas.cpp | 9213 | c89ops.h vs GNU as |
What is deliberately not scored
Each of these was looked at and left, for a reason worth knowing before looking again.
Needs the emulator running, not a function of inputs. The kernel/
tree (24 captures: threads, semaphores, event flags), dma/dmac/tagintr,
dma/vif/basic and mskpath3, memory/ee/default, and the IOP libc
captures, which run real IRX code rather than anything this tree
implements. dma/dmac/tagintr got as far as a written scorer: driving the
tag chain needs the real hwDmacSrcChain, and linking Hw.cpp for it
pulls in sixty more symbols for four cases.
Needs instruction sequencing. ee/branchdelay, ee/lsudelay,
iop/branchdelay, iop/hilodelay, iop/lsudelay,
vu/lower/fdivdelay. These report what an instruction inside a delay
slot observes. The EE branchdelay properties were verified by reading
instead, which is how the IOP JALR bug was found.
Would be circular. dma/spr/normal -- its two claims are SADR
masking to 0x3FF0 and MADR dropping bit 31, both in Dmac.cpp's write
path. Reaching that path needs the hardware register file; restating the
masks in a test would compare two constants transcribed by hand, and
pass whatever the emulator did.
No oracle in the capture. cpu/cop and gpu/gp0-e1 in ps1-tests
print pass/fail summaries rather than values, so there is nothing to
score against.
Implemented but deferred. The PS1 MDEC status register: the oracle
exists in tests/iop/hwmdec.cpp and scores 0 of 1289, wired as a report
rather than a gate. Making it pass means restructuring rl2blk to expose
block-level state, which is a real change rather than a test.
Not reachable by any harness here. The arm64 EE recompiler decodes
with nested switches rather than tables -- about 190 case labels -- so
the dispatch audit in tests/ee/tabaudit.c and tests/iop/tabaudit.c
cannot reach it. A misplaced case there is the same class of bug as a
swapped table entry and nothing would catch it.
Known gaps inside passing suites
A clean score measures what the capture asks, not what the instruction does. These are the places where those differ, found by mutating the emulator and seeing what still passed:
- CLIP (vu): the fourteen cases pin the comparison but not the six-bit shift that accumulates the flag across calls, and no operand has a denormal w. Two thirds of the op is unexercised.
- VIF column masking: the
stmodscenarios use a mask that selects row and data only, so theMaskColarm never runs. SLTIUand the immediate split (ee, iop): no line in either capture uses a negative or high-bit immediate, so sign- and zero-extension are indistinguishable. Both suites now carry cases derived from the MIPS definition instead, marked as such.TQWC == 0(spr): no interleave case leaves it zero on its own.- VU EFU accuracy (vu): scored as floors, not targets, and the floors
depend on
vu0ExtraOverflow, which defaults off.hwefu.chardcodes the clamp and so models the other configuration; that is why its EATAN score differs fromhwrealvu's.
The traps this tree has hit
Recorded because each cost real time and each looks like an emulator bug first.
Silent under-coverage is the recurring one: a block whose name or operand
shape stops matching the capture contributes nothing and reports success.
Three blocks were absent that way -- mult and multu in the EE
harness, pcpyh and prevh in the MMI one -- and none was found by a
failing test. The harnesses now compare what they parsed against the
number of lines each block has, and a mismatch is a failure.
The others, in short: named constants missing from a table drop lines without failing; the variable shifts print value-first, not source-first, which has caught four separate harnesses; and several captures seed a register differently from their neighbours, so a value that looks like a result may be a source or a leftover.