Tests: pin the VU FMAC's range against the console

The VU's largest value is 0x7FFFFFFF, one binade above FLT_MAX, the same
range the EE FPU has. So an exponent-255 word is an ordinary number on
the way in and on the way out, and "overflow" starts above it rather
than above FLT_MAX.

Both engines put the boundary a binade lower. vuDouble() rewrites an
exponent-255 operand as 0x7F7FFFFF and VU_MAC_UPDATE() calls every
exponent-255 result an overflow; the arm64 COP2 macro emitters clamp the
result to +/-FLT_MAX and raise neither O nor U. microVU's per-op operand
clamps approximate the same thing from a list of games rather than a
rule.

68 rows off an SCPH-90000 through VU0 macro mode, scored per engine and
per column, with what each engine cannot yet reproduce recorded per case
so a fix trips the test as loudly as a regression. Nothing is fixed
here.

Two of the rows are structural rather than about range. An overflowed
product does not become 0x7FFFFFFF before the accumulate: an addend of
-0x7FFFFFFF cannot cancel it. An underflowed product does become zero
before it. And the multiplier is the EE's, with the same one-ULP deficit
decided by ft's mantissa alone.

The harness grows two things the rows need: a VADDA encoder, and an
opt-out from Run()'s VU0 JIT-vs-interp auto-diff for tests that score
each engine against a hardware capture instead of against the other
engine.
This commit is contained in:
pstef
2026-08-18 08:00:56 -07:00
committed by Brian Degenhardt
parent 2a98726692
commit 83f2510134
6 changed files with 355 additions and 1 deletions
@@ -142,6 +142,7 @@ add_pcsx2_test(recompiler_tests
ee_fpu_rsqrt_sign_console_tests.cpp
ee_lsu_console_conformance_tests.cpp
vu0_macro_console_conformance_tests.cpp
vu0_macro_fmac_range_console_tests.cpp
vu_madd_contract_console_tests.cpp
vu1_efu_console_conformance_tests.cpp
vu_sticky_console_conformance_tests.cpp
@@ -0,0 +1,111 @@
// GENERATED by captures/vusat/gen_autocases.py from a capture taken on an
// SCPH-90000 (hw-run1.out, byte-identical to hw-run2.out). Do not edit by hand.
//
// VU0 macro-mode FMAC at the top and the bottom of its range. Each row seeds
// fs/ft/acc into all four lanes of vf1/vf2/vf3, runs
//
// vadda.xyzw $ACC, $vf3, $vf7 ; vf7 is zero, so ACC = acc
// <op>.xyzw $vf4, $vf1, $vf2
//
// and records vf4 (identical in all four lanes), the MAC flag and the status
// flag. The status flag is cleared before the pair, so it carries both ops.
//
// bad_interp / bad_jit are not from the console: they are what this tree
// currently fails to reproduce, per column, regenerated from the test binary's
// own dump. Clearing a bit is how a fix lands.
#pragma once
#include <common/Pcsx2Types.h>
namespace console_vusat {
enum VuSatOp { VS_ADD, VS_SUB, VS_MUL, VS_MADD, VS_MSUB, VS_MAX, VS_MINI };
// Columns a case can be wrong in, independently.
enum { VSB_VALUE = 1, VSB_MAC = 2, VSB_STAT = 4 };
struct VuSatCase {
u8 op;
u32 fs, ft, acc; // seeded into all four lanes of vf1 / vf2 / vf3
u32 out; // vf4 on the console, all four lanes
u16 mac, stat; // VI17 and VI16 on the console
u8 bad_interp, bad_jit;
const char* what;
};
inline constexpr VuSatCase kVuSatCases[] = {
{VS_ADD, 0x3F800000u, 0x3F800000u, 0x00000000u, 0x40000000u, 0x0000u, 0x0040u, 0, 0, "CONTROL 1.0+1.0, plumbing"},
{VS_ADD, 0x3F800000u, 0xBF800000u, 0x00000000u, 0x00000000u, 0x000Fu, 0x0041u, 0, 0, "CONTROL liveness 1.0-1.0: value and MAC flag must both differ from row 0"},
{VS_MUL, 0x3F000000u, 0x7F800000u, 0x00000000u, 0x7F000000u, 0x0000u, 0x0040u, 1, 1, "A 0.5*2^128 full 7F000000 | operand clamped 7EFFFFFF"},
{VS_MUL, 0x3F000000u, 0x7FFFFFFFu, 0x00000000u, 0x7F7FFFFEu, 0x0000u, 0x0040u, 1, 1, "A 0.5*max full 7F7FFFFF | operand clamped 7EFFFFFF"},
{VS_ADD, 0x7FFFFFFFu, 0xFF800000u, 0x00000000u, 0x7F7FFFFEu, 0x0000u, 0x0040u, 7, 1, "A max + -2^128 full 7F7FFFFE | operand clamped 00000000"},
{VS_SUB, 0x7FFFFFFFu, 0x7F800000u, 0x00000000u, 0x7F7FFFFEu, 0x0000u, 0x0040u, 7, 1, "A max - 2^128 full 7F7FFFFE | operand clamped 00000000"},
{VS_ADD, 0x7F800000u, 0xFF800000u, 0x00000000u, 0x00000000u, 0x000Fu, 0x0041u, 0, 7, "A 2^128 + -2^128 zero under either reading"},
{VS_MUL, 0x7F7FFFFFu, 0x40000000u, 0x00000000u, 0x7FFFFFFFu, 0x0000u, 0x0040u, 1, 1, "B FLT_MAX*2 exactly 7FFFFFFF if the range runs that far"},
{VS_MUL, 0x7F7FFFFFu, 0xC0000000u, 0x00000000u, 0xFFFFFFFFu, 0x00F0u, 0x00C2u, 1, 1, "B FLT_MAX*-2 sign mirror of the row above"},
{VS_ADD, 0x7FFFFFFFu, 0x74000000u, 0x00000000u, 0x7FFFFFFFu, 0xF000u, 0x0248u, 7, 7, "B max + 1ulp first value past the top: saturate or wrap"},
{VS_MUL, 0x7F800000u, 0x40000000u, 0x00000000u, 0x7FFFFFFFu, 0xF000u, 0x0248u, 7, 7, "B 2^128*2 one binade past the top"},
{VS_MUL, 0x7F800000u, 0xC0000000u, 0x00000000u, 0xFFFFFFFFu, 0xF0F0u, 0x02CAu, 7, 7, "B 2^128*-2 sign mirror"},
{VS_ADD, 0x7FFFFFFFu, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0xF000u, 0x0248u, 7, 7, "B max+max"},
{VS_MUL, 0x7F800000u, 0x7F800000u, 0x00000000u, 0x7FFFFFFFu, 0xF000u, 0x0248u, 7, 7, "B 2^128*2^128 far past the top"},
{VS_SUB, 0xFFFFFFFFu, 0x7FFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0xF0F0u, 0x02CAu, 7, 7, "B -max-max"},
{VS_MUL, 0x7F000000u, 0x3F800001u, 0x00000000u, 0x7F000001u, 0x0000u, 0x0040u, 0, 0, "B 2^127*(1+2^-23) exp FE, in range under every reading"},
{VS_MUL, 0x7F800000u, 0x3F800001u, 0x00000000u, 0x7F800001u, 0x0000u, 0x0040u, 1, 1, "B 2^128*(1+2^-23) exp FF and representable: does exp FF alone raise O?"},
{VS_ADD, 0x7F800000u, 0x74000000u, 0x00000000u, 0x7F800001u, 0x0000u, 0x0040u, 1, 1, "B 2^128 + 1ulp exp FF and representable, via the adder"},
{VS_MUL, 0x00800000u, 0x3F000000u, 0x00000000u, 0x00000000u, 0x0F0Fu, 0x0145u, 6, 6, "C 2^-126*0.5 first value below the bottom"},
{VS_MUL, 0x80800000u, 0x3F000000u, 0x00000000u, 0x80000000u, 0x0FFFu, 0x01C7u, 6, 6, "C -2^-126*0.5 sign of a flushed result"},
{VS_MUL, 0x00800000u, 0x00800000u, 0x00000000u, 0x00000000u, 0x0F0Fu, 0x0145u, 6, 6, "C 2^-126*2^-126 far below the bottom"},
{VS_ADD, 0x00800000u, 0x80800001u, 0x00000000u, 0x80000000u, 0x0FFFu, 0x01C7u, 6, 6, "C 2^-126 + -(2^-126+1ulp) tiny negative from the adder"},
{VS_MUL, 0x00400000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x000Fu, 0x0041u, 0, 0, "C denormal operand *2 flushed 00000000 | kept 00800000"},
{VS_ADD, 0x00400000u, 0x00400000u, 0x00000000u, 0x00000000u, 0x000Fu, 0x0041u, 0, 0, "C denormal operand + itself flushed 00000000 | kept 00800000"},
{VS_MUL, 0x00000000u, 0x7F800000u, 0x00000000u, 0x00000000u, 0x000Fu, 0x0041u, 0, 7, "D 0*2^128"},
{VS_MUL, 0x80000000u, 0x7F800000u, 0x00000000u, 0x80000000u, 0x00FFu, 0x00C3u, 0, 7, "D -0*2^128"},
{VS_MUL, 0x00000000u, 0x7FFFFFFFu, 0x00000000u, 0x00000000u, 0x000Fu, 0x0041u, 0, 7, "D 0*max"},
{VS_MADD, 0x00000000u, 0x7F800000u, 0x3F800000u, 0x3F800000u, 0x0000u, 0x0040u, 4, 5, "D 1.0 + 0*2^128"},
{VS_MADD, 0x7F800000u, 0x40800000u, 0xFFFFFFFFu, 0x7FFFFFFFu, 0xF000u, 0x0288u, 7, 7, "E -max + 2^130 saturated product 00000000 | wide product 7FFFFFFF"},
{VS_MSUB, 0x7F800000u, 0x40800000u, 0x7FFFFFFFu, 0xFFFFFFFFu, 0xF0F0u, 0x028Au, 7, 7, "E max - 2^130 saturated product 00000000 | wide product FFFFFFFF"},
{VS_MADD, 0x7F800000u, 0xC0800000u, 0x7FFFFFFFu, 0xFFFFFFFFu, 0xF0F0u, 0x028Au, 7, 7, "E max + -2^130 saturated product 00000000 | wide product FFFFFFFF"},
{VS_MADD, 0x7F800000u, 0x40800000u, 0x00000000u, 0x7FFFFFFFu, 0xF000u, 0x0248u, 7, 7, "E 0 + 2^130 both readings overflow"},
{VS_MADD, 0x00800000u, 0x3F000000u, 0x00800000u, 0x00800000u, 0x0000u, 0x0140u, 4, 4, "E 2^-126 + 2^-127 flushed product 00800000 | wide product 00C00000"},
{VS_MSUB, 0x00800000u, 0x3F000000u, 0x00800000u, 0x00800000u, 0x0000u, 0x0140u, 4, 4, "E 2^-126 - 2^-127 flushed product 00800000 | wide product 00400000->0"},
{VS_MAX, 0x7F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x000Fu, 0x0041u, 0, 0, "F max(2^128, max)"},
{VS_MAX, 0xFF800000u, 0x7F800000u, 0x00000000u, 0x7F800000u, 0x000Fu, 0x0041u, 0, 0, "F max(-2^128, 2^128)"},
{VS_MINI, 0xFF800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x000Fu, 0x0041u, 0, 0, "F mini(-2^128, -max)"},
{VS_MAX, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x000Fu, 0x0041u, 0, 0, "F max(+0, -0) integer compare 00000000"},
{VS_MINI, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x000Fu, 0x0041u, 0, 0, "F mini(+0, -0) integer compare 80000000"},
{VS_MAX, 0x00400000u, 0x00000000u, 0x00000000u, 0x00400000u, 0x000Fu, 0x0041u, 0, 0, "F max(denormal, 0) operand flushed 00000000 | kept 00400000"},
{VS_MINI, 0x00400000u, 0x00800000u, 0x00000000u, 0x00400000u, 0x000Fu, 0x0041u, 0, 0, "F mini(denormal, 2^-126)"},
{VS_ADD, 0x7FFFFFFFu, 0x3F800000u, 0x00000000u, 0x7FFFFFFFu, 0x0000u, 0x0040u, 1, 1, "G max + 1.0 far below one ULP: chop"},
{VS_SUB, 0x7F800000u, 0x74000000u, 0x00000000u, 0x7F7FFFFEu, 0x0000u, 0x0040u, 1, 1, "G 2^128 - 1ulp"},
{VS_SUB, 0x3F800000u, 0x3F800000u, 0x00000000u, 0x00000000u, 0x000Fu, 0x0041u, 0, 0, "G 1.0-1.0 sign of an exact zero"},
{VS_ADD, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000Fu, 0x0041u, 0, 0, "G -0 + +0"},
{VS_ADD, 0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00FFu, 0x00C3u, 0, 0, "G -0 + -0"},
{VS_MUL, 0x80000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00FFu, 0x00C3u, 0, 0, "G -0 * +0"},
{VS_MAX, 0x00000000u, 0x80000000u, 0x3F800000u, 0x00000000u, 0x0000u, 0x0000u, 0, 0, "H max -> 0, seed 1.0 -> no flag: 000F if MAX writes MAC, 0000 if not"},
{VS_MINI, 0x00000000u, 0x80000000u, 0x3F800000u, 0x80000000u, 0x0000u, 0x0000u, 0, 0, "H mini -> -0, seed 1.0: 00FF if MINI writes MAC, 0000 if not"},
{VS_MAX, 0x3F800000u, 0xBF800000u, 0x00000000u, 0x3F800000u, 0x000Fu, 0x0041u, 0, 0, "H max -> 1.0, seed 0 -> Z: 0000 if MAX writes MAC, 000F if not"},
{VS_MINI, 0x3F800000u, 0xBF800000u, 0x00000000u, 0xBF800000u, 0x000Fu, 0x0041u, 0, 0, "H mini -> -1.0, seed 0: 00F0 if MINI writes MAC, 000F if not"},
{VS_MADD, 0x7F800000u, 0x40000000u, 0xFFFFFFFFu, 0x7FFFFFFFu, 0xF000u, 0x0288u, 7, 7, "I -max + 2^129 saturated product 00000000 | exact wide product 74000000"},
{VS_MADD, 0x7F7FFFFFu, 0x40800000u, 0xFFFFFFFFu, 0x7FFFFFFFu, 0xF000u, 0x0288u, 7, 7, "I -max + 2*max saturated product 00000000 | exact wide product 7FFFFFFF no O"},
{VS_MADD, 0x7F800000u, 0x3F800000u, 0x7F800000u, 0x7FFFFFFFu, 0xF000u, 0x0208u, 7, 7, "I 2^128 + 2^128 in-range product, sum past the top"},
{VS_MUL, 0x7FFFFFFFu, 0x3F000000u, 0x00000000u, 0x7F7FFFFFu, 0x0000u, 0x0040u, 1, 0, "J max*0.5 case 3 swapped"},
{VS_MUL, 0x7FFFFFFFu, 0x3F800000u, 0x00000000u, 0x7FFFFFFFu, 0x0000u, 0x0040u, 1, 1, "J max*1.0"},
{VS_MUL, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFEu, 0x0000u, 0x0040u, 1, 1, "J 1.0*max swapped"},
{VS_MUL, 0x3EAAAAABu, 0x40400000u, 0x00000000u, 0x3F800000u, 0x0000u, 0x0040u, 0, 0, "J (1/3)*3 exact 1.00000002980232239"},
{VS_MUL, 0x40400000u, 0x3EAAAAABu, 0x00000000u, 0x3F800000u, 0x0000u, 0x0040u, 0, 0, "J 3*(1/3) swapped"},
{VS_MUL, 0x3F800001u, 0x3F800001u, 0x00000000u, 0x3F800002u, 0x0000u, 0x0040u, 0, 0, "J (1+2^-23)^2 exact 1+2^-22+2^-46"},
{VS_MUL, 0x3FB504F3u, 0x3FB504F3u, 0x00000000u, 0x3FFFFFFFu, 0x0000u, 0x0040u, 0, 0, "J sqrt2^2 exact 1.9999999830"},
{VS_ADD, 0x3F800000u, 0x33C00000u, 0x00000000u, 0x3F800000u, 0x0000u, 0x0040u, 0, 0, "K 1.0 + 1.5*2^-24 chop 3F800000 | nearest 3F800001"},
{VS_ADD, 0xBF800000u, 0xB3C00000u, 0x00000000u, 0xBF800000u, 0x00F0u, 0x00C2u, 0, 0, "K -1.0 - 1.5*2^-24 chop BF800000 | nearest BF800001"},
{VS_ADD, 0x7FFFFFFFu, 0x73FFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x0000u, 0x0040u, 1, 1, "K max + just under 1ulp chop 7FFFFFFF, no O"},
{VS_ADD, 0xFF800000u, 0xFF800000u, 0x00000000u, 0xFFFFFFFFu, 0xF0F0u, 0x02CAu, 7, 7, "L -2^128 + -2^128"},
{VS_SUB, 0x7FFFFFFFu, 0xFF800000u, 0x00000000u, 0x7FFFFFFFu, 0xF000u, 0x0248u, 7, 7, "L max - -2^128"},
{VS_MSUB, 0x00800000u, 0x3F800000u, 0x00800000u, 0x00000000u, 0x000Fu, 0x0041u, 0, 0, "L 2^-126 - 2^-126 exact zero, Z without U"},
{VS_MUL, 0x00800000u, 0xBF000000u, 0x00000000u, 0x80000000u, 0x0FFFu, 0x01C7u, 6, 6, "L 2^-126 * -0.5 sign of a flushed product"},
};
inline constexpr int kVuSatCaseCount = 68;
// Column-misses, not case-misses: a case wrong in two columns counts twice.
inline constexpr int kVuSatBadInterp = 76;
inline constexpr int kVuSatBadJit = 84;
} // namespace console_vusat
@@ -468,7 +468,7 @@ void EeRecTestHarness::Run(RunMode mode)
ADD_FAILURE() << ss.str();
}
if (capture_vu0_)
if (capture_vu0_ && !vu0_expect_divergence_)
{
const auto vudiffs = DiffVu(vu0_jit_snapshot_, vu0_interp_snapshot_,
VuDiffMode::PipelinePermissive, vu0_ignored_vi_);
@@ -236,6 +236,14 @@ public:
// DiffVu's ignored_vi parameter.
void IgnoreVu0Vi(u32 reg_idx) { vu0_ignored_vi_.push_back(static_cast<int>(reg_idx)); }
// Drop Run()'s VU0 JIT-vs-interp auto-diff entirely. Only for tests that
// score each engine against an EXTERNAL reference -- a hardware capture --
// where the two engines being wrong in different ways is the subject
// rather than the failure. A test that calls this and then compares the
// two engines to each other has disabled its own assertion; compare each
// one to the reference instead.
void ExpectVu0Divergence() { vu0_expect_divergence_ = true; }
void SeedVu0Vf(u32 reg_idx, float x, float y, float z, float w);
void SeedVu0VfBits(u32 reg_idx, u32 x, u32 y, u32 z, u32 w);
void SeedVu0Acc(float x, float y, float z, float w);
@@ -342,6 +350,7 @@ private:
EeSnapshot interp_snapshot_;
bool capture_vu0_ = false;
bool vu0_expect_divergence_ = false;
std::vector<int> vu0_ignored_vi_;
VuSnapshot vu0_pre_snapshot_;
VuSnapshot vu0_jit_snapshot_;
@@ -571,6 +571,11 @@ constexpr u32 VMADDAx_C2(u32 mask_xyzw, u32 fs, u32 ft) { return COP2_FMAC(mask_
constexpr u32 VMADDAy_C2(u32 mask_xyzw, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, 0x02, fs, ft, 0x3D); }
constexpr u32 VMULAz_C2 (u32 mask_xyzw, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, 0x06, fs, ft, 0x3E); }
// VADDA — non-broadcast accumulate (ACC = fs + ft). SPEC2 sub-op 0x0A,
// funct 0x3C. Do not reach for the assembler to check this one: ps2dev's
// binutils transposes FS and FT for the vadda mnemonic.
constexpr u32 VADDA_C2 (u32 mask_xyzw, u32 fs, u32 ft) { return COP2_FMAC(mask_xyzw, 0x0A, fs, ft, 0x3C); }
// COP2-CO SPECIAL2 (LowerOP2 trampolines via SPEC1 funct 0x3C..0x3F).
// The SPEC2 dispatch index inside recCOP2SPECIAL2t is
// (code & 0x3) | ((code >> 4) & 0x7c)
@@ -0,0 +1,228 @@
// SPDX-FileCopyrightText: 2026 ARMSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// The VU FMAC at the top and the bottom of its range, against real hardware.
//
// autocases_vusat.h is generated from a capture taken on an SCPH-90000
// (captures/vusat). The rows are the answer to a question neither engine had
// ever been asked: microVU's per-op operand-clamp table names the games that
// broke without each clamp rather than a rule, and the interpreter's two
// range functions -- vuDouble() on the way in, VU_MAC_UPDATE() on the way out
// -- both treat exponent 255 as out of range and substitute 0x7F7FFFFF.
//
// What the console says instead, in one line: the VU's largest value is
// 0x7FFFFFFF, one binade above FLT_MAX, exactly like the EE FPU's. So
//
// * an exponent-255 operand is read at its full value (cases 2-5);
// * an exponent-255 RESULT is an ordinary number, not an overflow -- it
// raises no MAC O bit and keeps its mantissa (cases 16, 17);
// * a result past 0x7FFFFFFF saturates to it and raises O (cases 9-14);
// * below 2^-126 the result flushes to a signed zero and raises U and Z,
// while a denormal OPERAND is flushed on the way in, so the op sees a
// plain zero and raises Z without U (cases 18-23);
// * MAX and MINI do not flush a denormal operand, order +0 above -0, and
// write neither flag register (cases 37-40, 47-50).
//
// Two structural results come out of the MADD rows. An overflowed product does
// not become 0x7FFFFFFF before the accumulate: an addend of -0x7FFFFFFF cannot
// cancel it and the result still saturates with the product's sign (cases 28,
// 51, 52). A product that underflows does become zero before the accumulate
// (cases 32, 33). And the status flag's sticky bits carry the multiply stage's
// flags as well as the sum's, which is visible where MAC is clear but a sticky
// bit is set (cases 27, 32).
//
// Scoring is per engine and per column, not JIT-versus-interp: at the time of
// writing both engines are wrong here in overlapping ways, so a differential
// between them would report agreement on rows where they are agreeing about
// the wrong number.
#include "harness/EeRecTestHarness.h"
#include "harness/MipsEncode.h"
#include "VU.h"
#include <gtest/gtest.h>
#include <cstdio>
#include "autocases_vusat.h"
using namespace console_vusat;
namespace recompiler_tests {
using namespace mips;
using namespace mips::ee;
namespace {
// The console probe's register assignment, kept so the emitted pair is the
// same one that was measured.
constexpr u32 kFs = 1, kFt = 2, kAcc = 3, kFd = 4, kZero = 7;
constexpr u32 kMaskXyzw = 0xF;
u32 Encode(const VuSatCase& c)
{
switch (c.op)
{
case VS_ADD: return VADD_C2 (kMaskXyzw, kFd, kFs, kFt);
case VS_SUB: return VSUB_C2 (kMaskXyzw, kFd, kFs, kFt);
case VS_MUL: return VMUL_C2 (kMaskXyzw, kFd, kFs, kFt);
case VS_MADD: return VMADD_C2(kMaskXyzw, kFd, kFs, kFt);
case VS_MSUB: return VMSUB_C2(kMaskXyzw, kFd, kFs, kFt);
case VS_MAX: return VMAX_C2 (kMaskXyzw, kFd, kFs, kFt);
case VS_MINI: return VMINI_C2(kMaskXyzw, kFd, kFs, kFt);
default: return 0;
}
}
struct Observed
{
u32 out;
u32 mac;
u32 stat;
};
// Runs one row's VADDA/op pair and reads back one engine's answer. The ACC is
// seeded through VADDA rather than written directly because that is what the
// capture measured -- the seed's own flags are part of the status column.
Observed RunCase(const VuSatCase& c, u32 word, bool jit)
{
EeRecTestHarness h;
h.EnableVu0Capture();
// Both engines are read here and each is scored against the console on its
// own, so Run()'s JIT-vs-interp auto-diff would fail on the very rows this
// file exists to record.
h.ExpectVu0Divergence();
h.EnableCop1();
h.SeedVu0VfBits(kFs, c.fs, c.fs, c.fs, c.fs);
h.SeedVu0VfBits(kFt, c.ft, c.ft, c.ft, c.ft);
h.SeedVu0VfBits(kAcc, c.acc, c.acc, c.acc, c.acc);
h.SeedVu0VfBits(kZero, 0, 0, 0, 0);
h.SeedVu0VfBits(kFd, 0xDEADBEEFu, 0xDEADBEEFu, 0xDEADBEEFu, 0xDEADBEEFu);
h.LoadProgram({VADDA_C2(kMaskXyzw, kAcc, kZero), word});
h.Run();
Observed o{};
o.out = jit ? h.GetVu0VfBitsJit(kFd, 'x') : h.GetVu0VfBitsInterp(kFd, 'x');
o.mac = (jit ? h.GetVu0ViJit(REG_MAC_FLAG) : h.GetVu0ViInterp(REG_MAC_FLAG)) & 0xFFFFu;
o.stat = (jit ? h.GetVu0ViJit(REG_STATUS_FLAG) : h.GetVu0ViInterp(REG_STATUS_FLAG)) & 0xFFFFu;
return o;
}
// One bit per column, set where the engine disagrees with the console.
u8 Misses(const VuSatCase& c, const Observed& o)
{
u8 m = 0;
if (o.out != c.out) m |= VSB_VALUE;
if (o.mac != c.mac) m |= VSB_MAC;
if (o.stat != c.stat) m |= VSB_STAT;
return m;
}
const char* kColName[] = {"value", "mac", "stat"};
constexpr u8 kColBit[] = {VSB_VALUE, VSB_MAC, VSB_STAT};
int PopCount(u8 v) { return (v & 1) + ((v >> 1) & 1) + ((v >> 2) & 1); }
} // namespace
// Asserts the columns this emulator DOES reproduce, and asserts that the ones
// it does not still fail -- so a fix trips the test rather than quietly
// widening the allowance. Regenerate the masks from
// DISABLED_DumpConsoleComparison below; do not hand-edit them.
TEST(Vu0MacroFmacRangeConsole, FmacRangeMatchesConsole)
{
int checked = 0, bad_interp = 0, bad_jit = 0;
for (int i = 0; i < kVuSatCaseCount; ++i)
{
const VuSatCase& c = kVuSatCases[i];
const u32 word = Encode(c);
ASSERT_NE(word, 0u) << "case " << i << ": no encoder for op " << int(c.op);
for (int jit = 0; jit < 2; ++jit)
{
const u8 known = jit ? c.bad_jit : c.bad_interp;
const u8 got = Misses(c, RunCase(c, word, jit != 0));
(jit ? bad_jit : bad_interp) += PopCount(known);
for (int col = 0; col < 3; ++col)
{
SCOPED_TRACE(::testing::Message()
<< "case " << i << " [" << (jit ? "jit" : "interp") << "] "
<< kColName[col] << ": " << c.what);
if (known & kColBit[col])
{
EXPECT_TRUE(got & kColBit[col])
<< "now MATCHES the console. Regenerate autocases_vusat.h "
"from DISABLED_DumpConsoleComparison.";
}
else
{
EXPECT_FALSE(got & kColBit[col]) << "new divergence from silicon";
}
}
}
++checked;
}
EXPECT_EQ(checked, kVuSatCaseCount);
EXPECT_EQ(bad_interp, kVuSatBadInterp);
EXPECT_EQ(bad_jit, kVuSatBadJit);
}
// The controls the capture was taken with, re-asserted against the values in
// the header. Neither engine gets these wrong, so the point of them is the
// one above: if the plumbing here ever returns a fixed word, or the flag
// read-back stops moving, the sweep's other 66 rows say nothing.
TEST(Vu0MacroFmacRangeConsole, ControlsSeparate)
{
ASSERT_GE(kVuSatCaseCount, 2);
const VuSatCase& a = kVuSatCases[0];
const VuSatCase& b = kVuSatCases[1];
EXPECT_NE(a.out, b.out);
EXPECT_NE(a.mac, b.mac);
for (int jit = 0; jit < 2; ++jit)
{
const Observed oa = RunCase(a, Encode(a), jit != 0);
const Observed ob = RunCase(b, Encode(b), jit != 0);
SCOPED_TRACE(jit ? "jit" : "interp");
EXPECT_EQ(oa.out, a.out);
EXPECT_EQ(ob.out, b.out);
EXPECT_EQ(oa.mac, a.mac);
EXPECT_EQ(ob.mac, b.mac);
EXPECT_NE(oa.out, ob.out);
EXPECT_NE(oa.mac, ob.mac);
}
}
// What passing looks like once the FMAC range model is right, and the source
// of the known-bad masks. Run it with --gtest_also_run_disabled_tests and feed
// its VUSAT-MISS lines to captures/vusat/gen_autocases.py.
TEST(Vu0MacroFmacRangeConsole, DISABLED_DumpConsoleComparison)
{
for (int i = 0; i < kVuSatCaseCount; ++i)
{
const VuSatCase& c = kVuSatCases[i];
const u32 word = Encode(c);
ASSERT_NE(word, 0u);
for (int jit = 0; jit < 2; ++jit)
{
const char* engine = jit ? "jit" : "interp";
const Observed o = RunCase(c, word, jit != 0);
const u8 got = Misses(c, o);
for (int col = 0; col < 3; ++col)
{
if (got & kColBit[col])
std::printf("VUSAT-MISS %d %s %s\n", i, engine, kColName[col]);
}
std::printf("VUSAT-ROW %2d %-6s out %08X/%08X mac %04X/%04X stat %04X/%04X %s\n",
i, engine, c.out, o.out, c.mac, o.mac, c.stat, o.stat, c.what);
SCOPED_TRACE(::testing::Message() << "case " << i << " [" << engine << "] " << c.what);
EXPECT_EQ(o.out, c.out);
EXPECT_EQ(o.mac, c.mac);
EXPECT_EQ(o.stat, c.stat);
}
}
}
} // namespace recompiler_tests