Tests: EE loads and stores against PS2 hardware captures

All 21 load/store opcodes over a fixed byte pattern at three offsets,
plus a misaligned +11 for the quad ops. ee_rec_loadstore_tests.cpp
covers the same ground as a JIT-vs-interp differential, which cannot see
a mistake the two engines share - the likeliest place for one being the
eight unaligned ops.

The capture prints the full 128-bit destination on every load, so "loads
leave bits 127:64 alone" is asserted on all twelve of them (LQ being the
one that must not), and the `-> $0` rows load into $0 then read it back,
which catches a load that wrote the zero register.

One case of 164 diverges: the interpreter's LD writes GPR.r[_Rt_].UD[0]
with no !_Rt_ guard, unlike all eleven of its neighbours. Recorded as a
row that fails if it is ever fixed, with a DISABLED tripwire showing the
end state.
This commit is contained in:
pstef
2026-07-26 09:16:39 +02:00
parent 75e5099197
commit 6383acdb6e
3 changed files with 495 additions and 0 deletions
@@ -115,6 +115,7 @@ add_pcsx2_test(recompiler_tests
# from the captures and are not edited by hand.
ee_mmi_console_conformance_tests.cpp
ee_fpu_fcr_console_conformance_tests.cpp
ee_lsu_console_conformance_tests.cpp
)
target_include_directories(recompiler_tests PRIVATE
@@ -0,0 +1,181 @@
// GENERATED from unknownbrackets/ps2autotests tests/cpu/ee/lsu.expected
// (real PS2 hardware). Do not edit by hand.
#pragma once
#include <common/Pcsx2Types.h>
namespace ps2auto_eelsu {
// C_PATTERN, 3 x 16 bytes. Loads read it in place (base = +16);
// stores get a fresh copy per case.
inline constexpr u32 kPattern[12] = {
0x45678123u, 0x9ABCDEF0u, 0xDEADBEEFu, 0xC0DE1337u,
0x23456789u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu,
0x8899AABBu, 0xCCDDEEFFu, 0x00112233u, 0x44556677u,
};
// C_GARBAGE1, the quad register preset.
inline constexpr u32 kGarbage1[4] = {0x00001337u, 0x00001338u, 0x00001339u, 0x0000133Au};
// The lui/ori register preset, before sign extension.
inline constexpr u32 kImmPreset = 0xABCD4321u;
// `into_zero` cases load into $0 and read it back through
// `ori rt, $0, 0`, so lo must be 0 and hi must survive.
struct LoadCase {
const char* label; const char* op1; int off1;
const char* op2; int off2; // op2 null for single-op cases
bool imm_preset; bool into_zero;
u64 lo, hi; // expected 128-bit rt
};
inline constexpr LoadCase kLoadCases[] = {
{"lb +0 [imm]", "lb", 0, nullptr, 0, true, false, 0xFFFFFFFFFFFFFF89ull, 0x0000133A00001339ull},
{"lb +16 [imm]", "lb", 16, nullptr, 0, true, false, 0xFFFFFFFFFFFFFFBBull, 0x0000133A00001339ull},
{"lb -16 [imm]", "lb", -16, nullptr, 0, true, false, 0x0000000000000023ull, 0x0000133A00001339ull},
{"lb +0 [quad]", "lb", 0, nullptr, 0, false, false, 0xFFFFFFFFFFFFFF89ull, 0x0000133A00001339ull},
{"lb +16 [quad]", "lb", 16, nullptr, 0, false, false, 0xFFFFFFFFFFFFFFBBull, 0x0000133A00001339ull},
{"lb -16 [quad]", "lb", -16, nullptr, 0, false, false, 0x0000000000000023ull, 0x0000133A00001339ull},
{"lb -> $0", "lb", 0, nullptr, 0, false, true, 0x0000000000000000ull, 0x0000133A00001339ull},
{"lbu +0 [imm]", "lbu", 0, nullptr, 0, true, false, 0x0000000000000089ull, 0x0000133A00001339ull},
{"lbu +16 [imm]", "lbu", 16, nullptr, 0, true, false, 0x00000000000000BBull, 0x0000133A00001339ull},
{"lbu -16 [imm]", "lbu", -16, nullptr, 0, true, false, 0x0000000000000023ull, 0x0000133A00001339ull},
{"lbu +0 [quad]", "lbu", 0, nullptr, 0, false, false, 0x0000000000000089ull, 0x0000133A00001339ull},
{"lbu +16 [quad]", "lbu", 16, nullptr, 0, false, false, 0x00000000000000BBull, 0x0000133A00001339ull},
{"lbu -16 [quad]", "lbu", -16, nullptr, 0, false, false, 0x0000000000000023ull, 0x0000133A00001339ull},
{"lbu -> $0", "lbu", 0, nullptr, 0, false, true, 0x0000000000000000ull, 0x0000133A00001339ull},
{"ld +0 [imm]", "ld", 0, nullptr, 0, true, false, 0xABCDEF0123456789ull, 0x0000133A00001339ull},
{"ld +16 [imm]", "ld", 16, nullptr, 0, true, false, 0xCCDDEEFF8899AABBull, 0x0000133A00001339ull},
{"ld -16 [imm]", "ld", -16, nullptr, 0, true, false, 0x9ABCDEF045678123ull, 0x0000133A00001339ull},
{"ld +0 [quad]", "ld", 0, nullptr, 0, false, false, 0xABCDEF0123456789ull, 0x0000133A00001339ull},
{"ld +16 [quad]", "ld", 16, nullptr, 0, false, false, 0xCCDDEEFF8899AABBull, 0x0000133A00001339ull},
{"ld -16 [quad]", "ld", -16, nullptr, 0, false, false, 0x9ABCDEF045678123ull, 0x0000133A00001339ull},
{"ld -> $0", "ld", 0, nullptr, 0, false, true, 0x0000000000000000ull, 0x0000133A00001339ull},
{"ldl +0 [imm]", "ldl", 0, nullptr, 0, true, false, 0x89FFFFFFABCD4321ull, 0x0000133A00001339ull},
{"ldr +0 [imm]", "ldr", 0, nullptr, 0, true, false, 0xABCDEF0123456789ull, 0x0000133A00001339ull},
{"ldl +1 [imm]", "ldl", 1, nullptr, 0, true, false, 0x6789FFFFABCD4321ull, 0x0000133A00001339ull},
{"ldr +1 [imm]", "ldr", 1, nullptr, 0, true, false, 0xFFABCDEF01234567ull, 0x0000133A00001339ull},
{"ldl +0/ldr +7 [imm]", "ldl", 0, "ldr", 7, true, false, 0x89FFFFFFABCD43ABull, 0x0000133A00001339ull},
{"ldl +0 [quad]", "ldl", 0, nullptr, 0, false, false, 0x8900133800001337ull, 0x0000133A00001339ull},
{"ldr +0 [quad]", "ldr", 0, nullptr, 0, false, false, 0xABCDEF0123456789ull, 0x0000133A00001339ull},
{"ldl +1 [quad]", "ldl", 1, nullptr, 0, false, false, 0x6789133800001337ull, 0x0000133A00001339ull},
{"ldr +1 [quad]", "ldr", 1, nullptr, 0, false, false, 0x00ABCDEF01234567ull, 0x0000133A00001339ull},
{"ldl +0/ldr +7 [quad]", "ldl", 0, "ldr", 7, false, false, 0x89001338000013ABull, 0x0000133A00001339ull},
{"ldl -> $0", "ldl", 0, nullptr, 0, false, true, 0x0000000000000000ull, 0x0000133A00001339ull},
{"ldr -> $0", "ldr", 0, nullptr, 0, false, true, 0x0000000000000000ull, 0x0000133A00001339ull},
{"lh +0 [imm]", "lh", 0, nullptr, 0, true, false, 0x0000000000006789ull, 0x0000133A00001339ull},
{"lh +16 [imm]", "lh", 16, nullptr, 0, true, false, 0xFFFFFFFFFFFFAABBull, 0x0000133A00001339ull},
{"lh -16 [imm]", "lh", -16, nullptr, 0, true, false, 0xFFFFFFFFFFFF8123ull, 0x0000133A00001339ull},
{"lh +0 [quad]", "lh", 0, nullptr, 0, false, false, 0x0000000000006789ull, 0x0000133A00001339ull},
{"lh +16 [quad]", "lh", 16, nullptr, 0, false, false, 0xFFFFFFFFFFFFAABBull, 0x0000133A00001339ull},
{"lh -16 [quad]", "lh", -16, nullptr, 0, false, false, 0xFFFFFFFFFFFF8123ull, 0x0000133A00001339ull},
{"lh -> $0", "lh", 0, nullptr, 0, false, true, 0x0000000000000000ull, 0x0000133A00001339ull},
{"lhu +0 [imm]", "lhu", 0, nullptr, 0, true, false, 0x0000000000006789ull, 0x0000133A00001339ull},
{"lhu +16 [imm]", "lhu", 16, nullptr, 0, true, false, 0x000000000000AABBull, 0x0000133A00001339ull},
{"lhu -16 [imm]", "lhu", -16, nullptr, 0, true, false, 0x0000000000008123ull, 0x0000133A00001339ull},
{"lhu +0 [quad]", "lhu", 0, nullptr, 0, false, false, 0x0000000000006789ull, 0x0000133A00001339ull},
{"lhu +16 [quad]", "lhu", 16, nullptr, 0, false, false, 0x000000000000AABBull, 0x0000133A00001339ull},
{"lhu -16 [quad]", "lhu", -16, nullptr, 0, false, false, 0x0000000000008123ull, 0x0000133A00001339ull},
{"lhu -> $0", "lhu", 0, nullptr, 0, false, true, 0x0000000000000000ull, 0x0000133A00001339ull},
{"lw +0 [imm]", "lw", 0, nullptr, 0, true, false, 0x0000000023456789ull, 0x0000133A00001339ull},
{"lw +16 [imm]", "lw", 16, nullptr, 0, true, false, 0xFFFFFFFF8899AABBull, 0x0000133A00001339ull},
{"lw -16 [imm]", "lw", -16, nullptr, 0, true, false, 0x0000000045678123ull, 0x0000133A00001339ull},
{"lw +0 [quad]", "lw", 0, nullptr, 0, false, false, 0x0000000023456789ull, 0x0000133A00001339ull},
{"lw +16 [quad]", "lw", 16, nullptr, 0, false, false, 0xFFFFFFFF8899AABBull, 0x0000133A00001339ull},
{"lw -16 [quad]", "lw", -16, nullptr, 0, false, false, 0x0000000045678123ull, 0x0000133A00001339ull},
{"lw -> $0", "lw", 0, nullptr, 0, false, true, 0x0000000000000000ull, 0x0000133A00001339ull},
{"lwl +0 [imm]", "lwl", 0, nullptr, 0, true, false, 0xFFFFFFFF89CD4321ull, 0x0000133A00001339ull},
{"lwr +0 [imm]", "lwr", 0, nullptr, 0, true, false, 0x0000000023456789ull, 0x0000133A00001339ull},
{"lwl +1 [imm]", "lwl", 1, nullptr, 0, true, false, 0x0000000067894321ull, 0x0000133A00001339ull},
{"lwr +1 [imm]", "lwr", 1, nullptr, 0, true, false, 0xFFFFFFFFAB234567ull, 0x0000133A00001339ull},
{"lwl +0/lwr +3 [imm]", "lwl", 0, "lwr", 3, true, false, 0xFFFFFFFF89CD4323ull, 0x0000133A00001339ull},
{"lwl +0 [quad]", "lwl", 0, nullptr, 0, false, false, 0xFFFFFFFF89001337ull, 0x0000133A00001339ull},
{"lwr +0 [quad]", "lwr", 0, nullptr, 0, false, false, 0x0000000023456789ull, 0x0000133A00001339ull},
{"lwl +1 [quad]", "lwl", 1, nullptr, 0, false, false, 0x0000000067891337ull, 0x0000133A00001339ull},
{"lwr +1 [quad]", "lwr", 1, nullptr, 0, false, false, 0x0000133800234567ull, 0x0000133A00001339ull},
{"lwl +0/lwr +3 [quad]", "lwl", 0, "lwr", 3, false, false, 0xFFFFFFFF89001323ull, 0x0000133A00001339ull},
{"lwl -> $0", "lwl", 0, nullptr, 0, false, true, 0x0000000000000000ull, 0x0000133A00001339ull},
{"lwr -> $0", "lwr", 0, nullptr, 0, false, true, 0x0000000000000000ull, 0x0000133A00001339ull},
{"lwu +0 [imm]", "lwu", 0, nullptr, 0, true, false, 0x0000000023456789ull, 0x0000133A00001339ull},
{"lwu +16 [imm]", "lwu", 16, nullptr, 0, true, false, 0x000000008899AABBull, 0x0000133A00001339ull},
{"lwu -16 [imm]", "lwu", -16, nullptr, 0, true, false, 0x0000000045678123ull, 0x0000133A00001339ull},
{"lwu +0 [quad]", "lwu", 0, nullptr, 0, false, false, 0x0000000023456789ull, 0x0000133A00001339ull},
{"lwu +16 [quad]", "lwu", 16, nullptr, 0, false, false, 0x000000008899AABBull, 0x0000133A00001339ull},
{"lwu -16 [quad]", "lwu", -16, nullptr, 0, false, false, 0x0000000045678123ull, 0x0000133A00001339ull},
{"lwu -> $0", "lwu", 0, nullptr, 0, false, true, 0x0000000000000000ull, 0x0000133A00001339ull},
{"lq +0 [imm]", "lq", 0, nullptr, 0, true, false, 0xABCDEF0123456789ull, 0xC0DEC0DEBEEFDEADull},
{"lq +16 [imm]", "lq", 16, nullptr, 0, true, false, 0xCCDDEEFF8899AABBull, 0x4455667700112233ull},
{"lq -16 [imm]", "lq", -16, nullptr, 0, true, false, 0x9ABCDEF045678123ull, 0xC0DE1337DEADBEEFull},
{"lq +11 [imm]", "lq", 11, nullptr, 0, true, false, 0xABCDEF0123456789ull, 0xC0DEC0DEBEEFDEADull},
{"lq +0 [quad]", "lq", 0, nullptr, 0, false, false, 0xABCDEF0123456789ull, 0xC0DEC0DEBEEFDEADull},
{"lq +16 [quad]", "lq", 16, nullptr, 0, false, false, 0xCCDDEEFF8899AABBull, 0x4455667700112233ull},
{"lq -16 [quad]", "lq", -16, nullptr, 0, false, false, 0x9ABCDEF045678123ull, 0xC0DE1337DEADBEEFull},
{"lq +11 [quad]", "lq", 11, nullptr, 0, false, false, 0xABCDEF0123456789ull, 0xC0DEC0DEBEEFDEADull},
{"lq -> $0", "lq", 0, nullptr, 0, false, true, 0x0000000000000000ull, 0x0000133A00001339ull},
};
// `block` is the byte delta from the base register to the
// 16-byte block the capture printed.
struct StoreCase {
const char* label; const char* op1; int off1;
const char* op2; int off2;
bool imm_preset; int block;
u32 mem[4]; // word0..word3 of that block
};
inline constexpr StoreCase kStoreCases[] = {
{"sb +0 [imm]", "sb", 0, nullptr, 0, true, 0, {0x23456721u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sb +16 [imm]", "sb", 16, nullptr, 0, true, 16, {0x8899AA21u, 0xCCDDEEFFu, 0x00112233u, 0x44556677u}},
{"sb -16 [imm]", "sb", -16, nullptr, 0, true, -16, {0x45678121u, 0x9ABCDEF0u, 0xDEADBEEFu, 0xC0DE1337u}},
{"sb +0 [quad]", "sb", 0, nullptr, 0, false, 0, {0x23456737u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sb +16 [quad]", "sb", 16, nullptr, 0, false, 16, {0x8899AA37u, 0xCCDDEEFFu, 0x00112233u, 0x44556677u}},
{"sb -16 [quad]", "sb", -16, nullptr, 0, false, -16, {0x45678137u, 0x9ABCDEF0u, 0xDEADBEEFu, 0xC0DE1337u}},
{"sd +0 [imm]", "sd", 0, nullptr, 0, true, 0, {0xABCD4321u, 0xFFFFFFFFu, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sd +16 [imm]", "sd", 16, nullptr, 0, true, 16, {0xABCD4321u, 0xFFFFFFFFu, 0x00112233u, 0x44556677u}},
{"sd -16 [imm]", "sd", -16, nullptr, 0, true, -16, {0xABCD4321u, 0xFFFFFFFFu, 0xDEADBEEFu, 0xC0DE1337u}},
{"sd +0 [quad]", "sd", 0, nullptr, 0, false, 0, {0x00001337u, 0x00001338u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sd +16 [quad]", "sd", 16, nullptr, 0, false, 16, {0x00001337u, 0x00001338u, 0x00112233u, 0x44556677u}},
{"sd -16 [quad]", "sd", -16, nullptr, 0, false, -16, {0x00001337u, 0x00001338u, 0xDEADBEEFu, 0xC0DE1337u}},
{"sdl +0 [imm]", "sdl", 0, nullptr, 0, true, 0, {0x234567FFu, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sdr +0 [imm]", "sdr", 0, nullptr, 0, true, 0, {0xABCD4321u, 0xFFFFFFFFu, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sdl +1 [imm]", "sdl", 1, nullptr, 0, true, 0, {0x2345FFFFu, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sdr +1 [imm]", "sdr", 1, nullptr, 0, true, 0, {0xCD432189u, 0xFFFFFFABu, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sdl +0/sdr +7 [imm]", "sdl", 0, "sdr", 7, true, 0, {0x234567FFu, 0x21CDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sdl +0 [quad]", "sdl", 0, nullptr, 0, false, 0, {0x23456700u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sdr +0 [quad]", "sdr", 0, nullptr, 0, false, 0, {0x00001337u, 0x00001338u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sdl +1 [quad]", "sdl", 1, nullptr, 0, false, 0, {0x23450000u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sdr +1 [quad]", "sdr", 1, nullptr, 0, false, 0, {0x00133789u, 0x00133800u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sdl +0/sdr +7 [quad]", "sdl", 0, "sdr", 7, false, 0, {0x23456700u, 0x37CDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sh +0 [imm]", "sh", 0, nullptr, 0, true, 0, {0x23454321u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sh +16 [imm]", "sh", 16, nullptr, 0, true, 16, {0x88994321u, 0xCCDDEEFFu, 0x00112233u, 0x44556677u}},
{"sh -16 [imm]", "sh", -16, nullptr, 0, true, -16, {0x45674321u, 0x9ABCDEF0u, 0xDEADBEEFu, 0xC0DE1337u}},
{"sh +0 [quad]", "sh", 0, nullptr, 0, false, 0, {0x23451337u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sh +16 [quad]", "sh", 16, nullptr, 0, false, 16, {0x88991337u, 0xCCDDEEFFu, 0x00112233u, 0x44556677u}},
{"sh -16 [quad]", "sh", -16, nullptr, 0, false, -16, {0x45671337u, 0x9ABCDEF0u, 0xDEADBEEFu, 0xC0DE1337u}},
{"sw +0 [imm]", "sw", 0, nullptr, 0, true, 0, {0xABCD4321u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sw +16 [imm]", "sw", 16, nullptr, 0, true, 16, {0xABCD4321u, 0xCCDDEEFFu, 0x00112233u, 0x44556677u}},
{"sw -16 [imm]", "sw", -16, nullptr, 0, true, -16, {0xABCD4321u, 0x9ABCDEF0u, 0xDEADBEEFu, 0xC0DE1337u}},
{"sw +0 [quad]", "sw", 0, nullptr, 0, false, 0, {0x00001337u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sw +16 [quad]", "sw", 16, nullptr, 0, false, 16, {0x00001337u, 0xCCDDEEFFu, 0x00112233u, 0x44556677u}},
{"sw -16 [quad]", "sw", -16, nullptr, 0, false, -16, {0x00001337u, 0x9ABCDEF0u, 0xDEADBEEFu, 0xC0DE1337u}},
{"swl +0 [imm]", "swl", 0, nullptr, 0, true, 0, {0x234567ABu, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"swr +0 [imm]", "swr", 0, nullptr, 0, true, 0, {0xABCD4321u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"swl +1 [imm]", "swl", 1, nullptr, 0, true, 0, {0x2345ABCDu, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"swr +1 [imm]", "swr", 1, nullptr, 0, true, 0, {0xCD432189u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"swl +0/swr +3 [imm]", "swl", 0, "swr", 3, true, 0, {0x214567ABu, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"swl +0 [quad]", "swl", 0, nullptr, 0, false, 0, {0x23456700u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"swr +0 [quad]", "swr", 0, nullptr, 0, false, 0, {0x00001337u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"swl +1 [quad]", "swl", 1, nullptr, 0, false, 0, {0x23450000u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"swr +1 [quad]", "swr", 1, nullptr, 0, false, 0, {0x00133789u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"swl +0/swr +3 [quad]", "swl", 0, "swr", 3, false, 0, {0x37456700u, 0xABCDEF01u, 0xBEEFDEADu, 0xC0DEC0DEu}},
{"sq +0 [imm]", "sq", 0, nullptr, 0, true, 0, {0xABCD4321u, 0xFFFFFFFFu, 0x00001339u, 0x0000133Au}},
{"sq +16 [imm]", "sq", 16, nullptr, 0, true, 16, {0xABCD4321u, 0xFFFFFFFFu, 0x00001339u, 0x0000133Au}},
{"sq -16 [imm]", "sq", -16, nullptr, 0, true, -16, {0xABCD4321u, 0xFFFFFFFFu, 0x00001339u, 0x0000133Au}},
{"sq +11 [imm]", "sq", 11, nullptr, 0, true, 0, {0xABCD4321u, 0xFFFFFFFFu, 0x00001339u, 0x0000133Au}},
{"sq +0 [quad]", "sq", 0, nullptr, 0, false, 0, {0x00001337u, 0x00001338u, 0x00001339u, 0x0000133Au}},
{"sq +16 [quad]", "sq", 16, nullptr, 0, false, 16, {0x00001337u, 0x00001338u, 0x00001339u, 0x0000133Au}},
{"sq -16 [quad]", "sq", -16, nullptr, 0, false, -16, {0x00001337u, 0x00001338u, 0x00001339u, 0x0000133Au}},
{"sq +11 [quad]", "sq", 11, nullptr, 0, false, 0, {0x00001337u, 0x00001338u, 0x00001339u, 0x0000133Au}},
};
inline constexpr int kLoadCaseCount = 82;
inline constexpr int kStoreCaseCount = 52;
} // namespace ps2auto_eelsu
@@ -0,0 +1,313 @@
// SPDX-FileCopyrightText: 2026 yaps2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// EE loads and stores against real PS2 hardware.
//
// autocases_eelsu.h is generated from unknownbrackets/ps2autotests
// tests/cpu/ee/lsu.expected: every EE load and store — 21 opcodes — against a
// fixed byte pattern at three offsets, plus a deliberately misaligned +11 for
// the quad ops. 134 cases.
//
// ee_rec_loadstore_tests.cpp already covers this ground as a JIT-vs-interp
// differential, and that is its limitation: the two engines share an idea of
// what LWL at offset 1 means, and a differential cannot see a shared mistake.
// The eight unaligned ops (LDL/LDR/LWL/LWR, SDL/SDR/SWL/SWR) are the classic
// place for one — their byte/shift tables are fiddly enough that the same
// off-by-one gets written twice.
//
// Two things this capture pins that a 64-bit test cannot:
//
// * It prints the FULL 128-bit destination on every load, so "loads leave
// bits 127:64 alone" is asserted on all twelve of them — and LQ is the
// one that must not.
// * The `-> $0` cases load into $0 and read $0 back through `ori rt, $0, 0`,
// so a load that wrongly wrote the zero register shows up immediately.
//
// The register presets are executed rather than injected: where the capture
// builds rt with `lui/ori`, so does this, which re-pins that neither touches
// bits 127:64.
#include <gtest/gtest.h>
#include "harness/EeRecTestHarness.h"
#include "harness/MipsEncode.h"
#include <cstring>
#include <string>
#include <vector>
#include "autocases_eelsu.h"
using namespace ps2auto_eelsu;
namespace recompiler_tests
{
namespace
{
using namespace mips;
using namespace mips::ee; // LD/LDL/LDR/SD/SDL/SDR/LWU/LQ/SQ live here
constexpr u32 kScratch = RecompilerTestEnvironment::kScratchAddr;
constexpr u32 kBase = 8; // $t0 — points at C_PATTERN[1], i.e. pattern + 16
constexpr u32 kRt = 9; // $t1 — the loaded/stored register
// C_GARBAGE1 as a 128-bit register value. Both presets start from this; the
// [imm] cases then overwrite the low 64 with lui/ori, which is why every
// expected `hi` in the table is this same word.
constexpr u64 kGarbageLo =
(static_cast<u64>(kGarbage1[1]) << 32) | kGarbage1[0];
constexpr u64 kGarbageHi =
(static_cast<u64>(kGarbage1[3]) << 32) | kGarbage1[2];
u32 EncodeLoad(const std::string& op, u32 rt, s16 off, u32 base)
{
if (op == "lb") return LB(rt, off, base);
if (op == "lbu") return LBU(rt, off, base);
if (op == "lh") return LH(rt, off, base);
if (op == "lhu") return LHU(rt, off, base);
if (op == "lw") return LW(rt, off, base);
if (op == "lwu") return LWU(rt, off, base);
if (op == "lwl") return LWL(rt, off, base);
if (op == "lwr") return LWR(rt, off, base);
if (op == "ld") return LD(rt, off, base);
if (op == "ldl") return LDL(rt, off, base);
if (op == "ldr") return LDR(rt, off, base);
if (op == "lq") return LQ(rt, off, base);
return 0;
}
u32 EncodeStore(const std::string& op, u32 rt, s16 off, u32 base)
{
if (op == "sb") return SB(rt, off, base);
if (op == "sh") return SH(rt, off, base);
if (op == "sw") return SW(rt, off, base);
if (op == "swl") return SWL(rt, off, base);
if (op == "swr") return SWR(rt, off, base);
if (op == "sd") return SD(rt, off, base);
if (op == "sdl") return SDL(rt, off, base);
if (op == "sdr") return SDR(rt, off, base);
if (op == "sq") return SQ(rt, off, base);
return 0;
}
// The capture's `SET_U32<0xABCD4321>`: lui then ori, executed, so the low 64
// bits get there by sign extension and 127:64 keep C_GARBAGE1's half.
void AppendImmPreset(std::vector<u32>& prog)
{
prog.push_back(LUI(kRt, static_cast<u16>(kImmPreset >> 16)));
prog.push_back(ORI(kRt, kRt, static_cast<u16>(kImmPreset & 0xFFFF)));
}
void Prepare(EeRecTestHarness& h)
{
h.WriteBytes(kScratch, kPattern, sizeof(kPattern));
h.SetGpr64(kBase, kScratch + 16);
h.SetGpr128(kRt, kGarbageLo, kGarbageHi);
}
// Recorded from a real run, per engine — never derived from a rule.
struct Divergence { const char* label; bool bad_interp, bad_jit; };
constexpr Divergence kLoadDivergences[] = {
// The interpreter's LD (R5900OpcodeImpl.cpp) writes
// cpuRegs.GPR.r[_Rt_].UD[0] with no `!_Rt_` guard, so the following
// `ori rt, $0, 0` reads back the loaded quad instead of zero. All eleven
// of its neighbours guard: LB/LBU/LH/LHU/LW/LWU/LWL/LWR/LDL/LDR early-out
// on !_Rt_ and LQ routes through gpr_GetWritePtr, whose reason for
// existing is the dummy zero slot. The recompiler gets it right.
{"ld -> $0", true, false},
};
bool IsKnownBad(const char* label, bool jit)
{
for (const Divergence& d : kLoadDivergences)
{
if (std::string(label) == d.label)
return jit ? d.bad_jit : d.bad_interp;
}
return false;
}
// Builds and runs one load case on one engine. Returns false if an opcode has
// no encoder, so the caller can fail with a useful name.
bool RunLoadCase(const LoadCase& c, bool jit, u64& lo, u64& hi)
{
const u32 w1 = EncodeLoad(c.op1, c.into_zero ? 0u : kRt,
static_cast<s16>(c.off1), kBase);
if (w1 == 0u)
return false;
std::vector<u32> prog;
if (c.imm_preset)
AppendImmPreset(prog);
prog.push_back(w1);
if (c.op2)
{
const u32 w2 = EncodeLoad(c.op2, kRt, static_cast<s16>(c.off2), kBase);
if (w2 == 0u)
return false;
prog.push_back(w2);
}
if (c.into_zero)
prog.push_back(ORI(kRt, 0, 0)); // read $0 back, as the capture does
EeRecTestHarness h;
Prepare(h);
h.LoadProgram(prog);
if (jit)
h.RunJitNoDiff();
else
h.RunInterpOnly();
lo = jit ? h.GetGpr64Jit(kRt) : h.GetGpr64Interp(kRt);
hi = jit ? h.GetGprUpper64Jit(kRt) : h.GetGprUpper64Interp(kRt);
return true;
}
} // namespace
// The engines legitimately might disagree here, so each is scored against the
// console independently rather than against the other.
TEST(EeLsuConsoleConformance, LoadsMatchConsole)
{
int diverged = 0;
for (int i = 0; i < kLoadCaseCount; ++i)
{
const LoadCase& c = kLoadCases[i];
SCOPED_TRACE(::testing::Message() << c.label);
for (int jit = 0; jit < 2; ++jit)
{
SCOPED_TRACE(jit ? "[jit]" : "[interp]");
u64 lo = 0, hi = 0;
ASSERT_TRUE(RunLoadCase(c, jit != 0, lo, hi));
if (!IsKnownBad(c.label, jit != 0))
{
EXPECT_EQ(lo, c.lo) << "bits 63:0";
EXPECT_EQ(hi, c.hi) << "bits 127:64";
}
else
{
++diverged;
EXPECT_FALSE(lo == c.lo && hi == c.hi)
<< c.label << (jit ? " [jit]" : " [interp]")
<< " now MATCHES silicon. If LD was given the `!_Rt_` "
"guard its eleven neighbours have, drop it from "
"kLoadDivergences and delete the DISABLED tripwire.";
}
}
}
EXPECT_EQ(diverged, 1) << "exactly one recorded divergence";
}
// What passing looks like once LD stops writing the zero register.
TEST(EeLsuConsoleConformance, DISABLED_AllLoadsMatchConsole)
{
for (int i = 0; i < kLoadCaseCount; ++i)
{
const LoadCase& c = kLoadCases[i];
for (int jit = 0; jit < 2; ++jit)
{
SCOPED_TRACE(::testing::Message()
<< c.label << (jit ? " [jit]" : " [interp]"));
u64 lo = 0, hi = 0;
ASSERT_TRUE(RunLoadCase(c, jit != 0, lo, hi));
EXPECT_EQ(lo, c.lo) << "bits 63:0";
EXPECT_EQ(hi, c.hi) << "bits 127:64";
}
}
}
TEST(EeLsuConsoleConformance, StoresMatchConsole)
{
for (int i = 0; i < kStoreCaseCount; ++i)
{
const StoreCase& c = kStoreCases[i];
SCOPED_TRACE(::testing::Message() << c.label);
const u32 w1 = EncodeStore(c.op1, kRt, static_cast<s16>(c.off1),
kBase);
ASSERT_NE(w1, 0u) << "no encoder for " << c.op1;
std::vector<u32> prog;
if (c.imm_preset)
AppendImmPreset(prog);
prog.push_back(w1);
if (c.op2)
{
const u32 w2 = EncodeStore(c.op2, kRt, static_cast<s16>(c.off2),
kBase);
ASSERT_NE(w2, 0u) << "no encoder for " << c.op2;
prog.push_back(w2);
}
for (int jit = 0; jit < 2; ++jit)
{
SCOPED_TRACE(jit ? "[jit]" : "[interp]");
EeRecTestHarness h;
Prepare(h);
h.TrackMemWindow(kScratch, sizeof(kPattern));
h.LoadProgram(prog);
if (jit)
h.RunJitNoDiff();
else
h.RunInterpOnly();
const u32 blk = kScratch + 16 + static_cast<u32>(c.block);
for (int w = 0; w < 4; ++w)
{
EXPECT_EQ(h.ReadU32(blk + 4u * static_cast<u32>(w)), c.mem[w])
<< "word " << w;
}
}
}
}
// Two invariants the capture states outright, pulled out so a failure names
// the rule rather than a row of the table.
// LQ/SQ mask the address down to 16 bytes rather than faulting or reading
// unaligned: the console's `lq +11` row equals its own `lq +0` row.
TEST(EeLsuConsoleConformance, QuadOpsMaskTheAddressTo16Bytes)
{
int checked = 0;
for (int i = 0; i < kLoadCaseCount; ++i)
{
const LoadCase& c = kLoadCases[i];
if (std::string(c.op1) != "lq" || c.off1 != 11)
continue;
for (int j = 0; j < kLoadCaseCount; ++j)
{
const LoadCase& z = kLoadCases[j];
if (std::string(z.op1) != "lq" || z.off1 != 0 || z.into_zero ||
z.imm_preset != c.imm_preset)
continue;
EXPECT_EQ(c.lo, z.lo) << c.label << " vs " << z.label;
EXPECT_EQ(c.hi, z.hi) << c.label << " vs " << z.label;
++checked;
}
}
EXPECT_EQ(checked, 2) << "expected the +11 row of both presets";
}
TEST(EeLsuConsoleConformance, LoadsPreserveTheUpper64BitsExceptLq)
{
int preserved = 0, overwritten = 0;
for (int i = 0; i < kLoadCaseCount; ++i)
{
const LoadCase& c = kLoadCases[i];
// LQ writes all 128 bits, so only its `-> $0` row (which never lands)
// keeps the preset half.
if (std::string(c.op1) == "lq" && !c.into_zero)
{
EXPECT_NE(c.hi, kGarbageHi) << c.label;
++overwritten;
continue;
}
EXPECT_EQ(c.hi, kGarbageHi) << c.label;
++preserved;
}
EXPECT_EQ(overwritten, 8);
EXPECT_EQ(preserved, kLoadCaseCount - 8);
}
} // namespace recompiler_tests