mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
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:
@@ -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
|
||||
Reference in New Issue
Block a user