diff --git a/tests/ctest/core/recompilers/CMakeLists.txt b/tests/ctest/core/recompilers/CMakeLists.txt index 30a50232ee..fed5798f3f 100644 --- a/tests/ctest/core/recompilers/CMakeLists.txt +++ b/tests/ctest/core/recompilers/CMakeLists.txt @@ -118,6 +118,7 @@ add_pcsx2_test(recompiler_tests ee_lsu_console_conformance_tests.cpp vu0_macro_console_conformance_tests.cpp vu1_efu_console_conformance_tests.cpp + vu_sticky_console_conformance_tests.cpp ) target_include_directories(recompiler_tests PRIVATE diff --git a/tests/ctest/core/recompilers/autocases_vusticky.h b/tests/ctest/core/recompilers/autocases_vusticky.h new file mode 100644 index 0000000000..50b69d6556 --- /dev/null +++ b/tests/ctest/core/recompilers/autocases_vusticky.h @@ -0,0 +1,337 @@ +// SPDX-FileCopyrightText: 2026 yaps2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ +// +// GENERATED from a first-party capture taken on a real PS2. Do not edit. +// +// VU sticky-flag ground truth. Earlier VU captures cleared STATUS between +// ops, so none of them constrained how the six sticky bits accumulate; these +// cases never clear. Group A is VU0 macro mode (EE COP2), group B the same +// questions asked of VU0 micro mode. +#pragma once + +#include "common/Pcsx2Types.h" + +namespace console_vusticky +{ +// Op kinds, mirroring the case bodies the probe ran. +enum VuStickyOpKind +{ + VS_NOP = 0, + VS_MUL, // vmul.x vf6, vf4, vf5 + VS_ADD, // vadd.x vf6, vf4, vf5 + VS_MUL_MASK0, // vmul vf6, vf4, vf5 with an EMPTY destination mask + VS_DIV, // vdiv Q, vf4x, vf5x + VS_SQRT, // vsqrt Q, vf5x + VS_RSQRT, // vrsqrt Q, vf4x, vf5x + VS_CLIP, // vclipw.xyz vf4, vf5 + VS_IADD, // viadd vi1, vi2, vi3 + VS_CTC2_ZERO, // ctc2 $0, $vi16 + VS_CTC2_FFF, // ctc2 (0xFFF), $vi16 +}; + +// `mask` is the COP2 destination mask the probe issued (x=8, y=4, z=2, w=1). +// Most cases are scalar `vmul.x`; the four-lane case is 0xF and the empty-mask +// case is 0. The four per-lane operand words are the quadword the console +// loaded with LQC2, so a case that needs a different event per lane can say so. +struct VuStickyOp +{ + VuStickyOpKind kind; + u32 mask; // COP2 destination mask, x=8 .. w=1; 0 for the empty-mask case + u32 fs[4]; + u32 ft[4]; +}; + +// STATUS, MAC and Q as the console read them back through CFC2. +struct VuStickyRead +{ + u32 status; + u32 mac; + u32 q; +}; + +struct VuStickyCase +{ + const char* tag; + const char* rule; + VuStickyOp op[3]; + // read[0] is the post-prologue control; read[1..3] follow each op. + VuStickyRead read[4]; + u32 clip; + u32 vf6[4]; +}; + +inline constexpr VuStickyCase kVuStickyCases[] = { + {"VUSTICKY_FMAC_ZSUO_ACCUMULATE", + "An FMAC ORs its ZSUO events into the sticky field: after an underflow (Z+U) an overflow (O) leaves sticky ZUO, not sticky O alone", + {{static_cast(1), 0x8u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}}, {static_cast(1), 0x8u, {0x7F000000u, 0x7F000000u, 0x7F000000u, 0x7F000000u}, {0x7F000000u, 0x7F000000u, 0x7F000000u, 0x7F000000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000145u, 0x00000808u, 0x3F800000u}, {0x00000348u, 0x00008000u, 0x3F800000u}, {0x00000348u, 0x00008000u, 0x3F800000u}}, + 0x00000000u, {0x7FFFFFFFu, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_SURVIVES_SILENT_FMAC", + "A sticky bit survives an FMAC that raises nothing: the clean add clears the cause nibble and leaves sticky ZU standing", + {{static_cast(1), 0x8u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}}, {static_cast(2), 0x8u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000145u, 0x00000808u, 0x3F800000u}, {0x00000140u, 0x00000000u, 0x3F800000u}, {0x00000140u, 0x00000000u, 0x3F800000u}}, + 0x00000000u, {0x40000000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_ONE_OP_ALL_FOUR", + "A single four-lane FMAC sets all four ZSUO stickies at once, one per lane: x=+0 (Z), y=-1 (S), z underflows (Z+U), w overflows (O)", + {{static_cast(1), 0xFu, {0x00000000u, 0xBF800000u, 0x00800000u, 0x7F000000u}, {0x00000000u, 0x3F800000u, 0x3F000000u, 0x7F000000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x000003CFu, 0x0000124Au, 0x3F800000u}, {0x000003CFu, 0x0000124Au, 0x3F800000u}, {0x000003CFu, 0x0000124Au, 0x3F800000u}}, + 0x00000000u, {0x00000000u, 0xBF800000u, 0x00000000u, 0x7FFFFFFFu}}, + {"VUSTICKY_DIV_DI_ACCUMULATE", + "The div unit ORs into sticky D/I as well: x/0 then 0/0 leaves BOTH sticky D and sticky I, though the cause nibble holds only the newer I", + {{static_cast(4), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, {static_cast(4), 0x0u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000820u, 0x00000000u, 0x7FFFFFFFu}, {0x00000C10u, 0x00000000u, 0x7FFFFFFFu}, {0x00000C10u, 0x00000000u, 0x7FFFFFFFu}}, + 0x00000000u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_DIV_KEEPS_FMAC_FLAGS", + "A div-unit op disturbs neither the FMAC cause bits nor the FMAC stickies: the underflow's ZU cause and ZU sticky both survive the divide", + {{static_cast(1), 0x8u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}}, {static_cast(4), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000145u, 0x00000808u, 0x3F800000u}, {0x00000965u, 0x00000808u, 0x7FFFFFFFu}, {0x00000965u, 0x00000808u, 0x7FFFFFFFu}}, + 0x00000000u, {0x00000000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_FMAC_KEEPS_DI", + "KNOWN-ANSWER CONTROL: an FMAC leaves the div unit's D cause and sticky D alone, reproducing the earlier D-persists-across-FMAC result from a new angle", + {{static_cast(4), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, {static_cast(2), 0x8u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000820u, 0x00000000u, 0x7FFFFFFFu}, {0x00000820u, 0x00000000u, 0x7FFFFFFFu}, {0x00000820u, 0x00000000u, 0x7FFFFFFFu}}, + 0x00000000u, {0x40000000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_DI_ACCUMULATE_SQRT_DIV", + "Second witness for the D/I accumulation, with a different pair of ops: sqrt of a negative raises I, then x/0 raises D, and both stickies stand", + {{static_cast(5), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0xBF800000u, 0xBF800000u, 0xBF800000u, 0xBF800000u}}, {static_cast(4), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000410u, 0x00000000u, 0x3F800000u}, {0x00000C20u, 0x00000000u, 0x7FFFFFFFu}, {0x00000C20u, 0x00000000u, 0x7FFFFFFFu}}, + 0x00000000u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_CTC2_CLEARS_STICKY_NOT_CAUSE", + "CTC2 of zero to STATUS clears the sticky field and leaves the cause nibble standing: the cause is not a bit of the register, it tracks MAC", + {{static_cast(1), 0x8u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}}, {static_cast(9), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000145u, 0x00000808u, 0x3F800000u}, {0x00000005u, 0x00000808u, 0x3F800000u}, {0x00000005u, 0x00000808u, 0x3F800000u}}, + 0x00000000u, {0x00000000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_CTC2_WRITTEN_AND_OP_SET_ALIKE", + "A CTC2-written sticky is indistinguishable from an op-set one: all six survive a clean add, and a later underflow ORs into them", + {{static_cast(10), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, {static_cast(2), 0x8u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, {static_cast(1), 0x8u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000FC0u, 0x00000000u, 0x3F800000u}, {0x00000FC0u, 0x00000000u, 0x3F800000u}, {0x00000FC5u, 0x00000808u, 0x3F800000u}}, + 0x00000000u, {0x00000000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_EMPTY_DEST_MASK_SILENT", + "An FMAC with an empty destination mask clears the whole MAC and raises nothing -- not even the events its lanes would have produced", + {{static_cast(1), 0x8u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}}, {static_cast(3), 0x0u, {0x7F000000u, 0x7F000000u, 0x7F000000u, 0x7F000000u}, {0x7F000000u, 0x7F000000u, 0x7F000000u, 0x7F000000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000145u, 0x00000808u, 0x3F800000u}, {0x00000140u, 0x00000000u, 0x3F800000u}, {0x00000140u, 0x00000000u, 0x3F800000u}}, + 0x00000000u, {0x00000000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_VCLIP_TOUCHES_ONLY_CLIP", + "VCLIP writes CLIP and leaves STATUS and MAC alone; the operands are chosen so CLIP actually changes, which is what separates this from 'did not run'", + {{static_cast(1), 0x8u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}}, {static_cast(7), 0x0u, {0x7F000000u, 0x7F000000u, 0x7F000000u, 0x7F000000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000145u, 0x00000808u, 0x3F800000u}, {0x00000145u, 0x00000808u, 0x3F800000u}, {0x00000145u, 0x00000808u, 0x3F800000u}}, + 0x00000015u, {0x00000000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_INTEGER_OP_SILENT", + "An integer op leaves the entire flag file untouched", + {{static_cast(1), 0x8u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}}, {static_cast(8), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000145u, 0x00000808u, 0x3F800000u}, {0x00000145u, 0x00000808u, 0x3F800000u}, {0x00000145u, 0x00000808u, 0x3F800000u}}, + 0x00000000u, {0x00000000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_Z_AND_S_ARE_STICKY", + "Sticky Z and sticky S are real and independent: +0 then -1 leaves both, though the cause nibble holds only S", + {{static_cast(2), 0x8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, {static_cast(2), 0x8u, {0xBF800000u, 0xBF800000u, 0xBF800000u, 0xBF800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000041u, 0x00000008u, 0x3F800000u}, {0x000000C2u, 0x00000080u, 0x3F800000u}, {0x000000C2u, 0x00000080u, 0x3F800000u}}, + 0x00000000u, {0xBF800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_THREE_EVENTS_ONE_ACCUMULATION", + "Three ops, three different events, one accumulation: underflow, overflow and a negative result leave sticky ZSUO with only S in the cause", + {{static_cast(1), 0x8u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}}, {static_cast(1), 0x8u, {0x7F000000u, 0x7F000000u, 0x7F000000u, 0x7F000000u}, {0x7F000000u, 0x7F000000u, 0x7F000000u, 0x7F000000u}}, {static_cast(2), 0x8u, {0xBF800000u, 0xBF800000u, 0xBF800000u, 0xBF800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000145u, 0x00000808u, 0x3F800000u}, {0x00000348u, 0x00008000u, 0x3F800000u}, {0x000003C2u, 0x00000080u, 0x3F800000u}}, + 0x00000000u, {0xBF800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_DI_CAUSE_REPLACED_STICKY_KEPT", + "Each div-unit op REPLACES the D/I cause pair while the stickies only grow: 0/0 then x/0 then sqrt(-1) ends with cause I and sticky D+I", + {{static_cast(4), 0x0u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, {static_cast(4), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, {static_cast(5), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0xBF800000u, 0xBF800000u, 0xBF800000u, 0xBF800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000410u, 0x00000000u, 0x7FFFFFFFu}, {0x00000C20u, 0x00000000u, 0x7FFFFFFFu}, {0x00000C10u, 0x00000000u, 0x3F800000u}}, + 0x00000000u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, + {"VUSTICKY_CLEAN_DIV_KEEPS_STICKY_DI", + "The sharpest form: rsqrt of -0 raises both D and I, then a perfectly clean 1/1 clears the cause pair and leaves sticky D and sticky I standing", + {{static_cast(6), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}}, {static_cast(4), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, {static_cast(0), 0x0u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}}, + {{0x00000000u, 0x00000000u, 0x3F800000u}, {0x00000C30u, 0x00000000u, 0x7FFFFFFFu}, {0x00000C00u, 0x00000000u, 0x3F800000u}, {0x00000C00u, 0x00000000u, 0x3F800000u}}, + 0x00000000u, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}}, +}; + +// ---- group B: VU0 micro mode ---- +// +// Each program is a run of {lower, upper} pairs ending on the E bit; the test +// harness appends the architectural delay-slot NOP itself. vi02 and vi03 are +// FSAND snapshots the program took of STATUS after its first and second op. +// +// `inherited_mac` records that the console ran the eight programs back to +// back: a program with no FMAC of its own leaves MAC -- and therefore the +// STATUS ZSUO cause, which tracks MAC -- holding the previous program's value. +// A harness that starts from a clean VU cannot reproduce those four bits, so +// tests score the sticky field and the D/I cause and leave the ZSUO cause to +// the MAC-derived law that group A pins. +struct VuStickyProgram +{ + const char* tag; + const char* rule; + const u32* lower; + const u32* upper; + u32 n_pairs; + u32 seed_fs1[4]; + u32 seed_ft1[4]; + u32 seed_fs2[4]; + u32 seed_ft2[4]; + u32 status_after_op1; // vi02 + u32 status_after_op2; // vi03 + u32 final_status; + u32 final_mac; + u32 final_clip; + u32 final_q; + u32 final_vi01; + bool has_own_fmac; +}; + +inline constexpr u32 kVuStickyProg0Lower[] = { + 0x10010123u, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, +}; +inline constexpr u32 kVuStickyProg0Upper[] = { + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x400002FFu, +}; +inline constexpr u32 kVuStickyProg1Lower[] = { + 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x2C2207FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x2C2307FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, +}; +inline constexpr u32 kVuStickyProg1Upper[] = { + 0x010521AAu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x01083A6Au, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x400002FFu, +}; +inline constexpr u32 kVuStickyProg2Lower[] = { + 0x800523BCu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x800003BFu, 0x2C2207FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x80083BBCu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x800003BFu, 0x2C2307FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, +}; +inline constexpr u32 kVuStickyProg2Upper[] = { + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x400002FFu, +}; +inline constexpr u32 kVuStickyProg3Lower[] = { + 0x800523BEu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x800003BFu, 0x2C2207FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x80083BBCu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x800003BFu, 0x2C2307FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, +}; +inline constexpr u32 kVuStickyProg3Upper[] = { + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x400002FFu, +}; +inline constexpr u32 kVuStickyProg4Lower[] = { + 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x2C2207FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x2A000000u, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x2C2307FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, +}; +inline constexpr u32 kVuStickyProg4Upper[] = { + 0x010521AAu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x400002FFu, +}; +inline constexpr u32 kVuStickyProg5Lower[] = { + 0x2A2007C0u, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x2C2207FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x2C2307FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, +}; +inline constexpr u32 kVuStickyProg5Upper[] = { + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x400002FFu, +}; +inline constexpr u32 kVuStickyProg6Lower[] = { + 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x2C2207FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x2A200000u, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x2C2307FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, +}; +inline constexpr u32 kVuStickyProg6Upper[] = { + 0x010521AAu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x400002FFu, +}; +inline constexpr u32 kVuStickyProg7Lower[] = { + 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x2C2207FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, 0x8000033Cu, 0x2C2307FFu, 0x8000033Cu, 0x8000033Cu, 0x8000033Cu, + 0x8000033Cu, +}; +inline constexpr u32 kVuStickyProg7Upper[] = { + 0x010521AAu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x01083A68u, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, 0x000002FFu, + 0x400002FFu, +}; + +inline constexpr VuStickyProgram kVuStickyPrograms[] = { + {"VUSTICKY_MICRO_PATH_CONTROL", + "Path validation: a microprogram with no flag traffic at all. VI01 == 0x123 is what proves upload + kick + E-bit + read-back before anything else here", + kVuStickyProg0Lower, kVuStickyProg0Upper, 9u, + {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, + 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x3F800000u, 0x00000123u, false}, + {"VUSTICKY_MICRO_FMAC_ZSUO_ACCUMULATE", + "Micro mode accumulates the ZSUO stickies exactly as macro mode does", + kVuStickyProg1Lower, kVuStickyProg1Upper, 25u, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}, {0x7F000000u, 0x7F000000u, 0x7F000000u, 0x7F000000u}, {0x7F000000u, 0x7F000000u, 0x7F000000u, 0x7F000000u}, + 0x00000145u, 0x00000348u, + 0x00000348u, 0x00008000u, 0x00000000u, 0x3F800000u, 0x00000000u, true}, + {"VUSTICKY_MICRO_DIV_DI_ACCUMULATE", + "Micro mode accumulates sticky D/I too: x/0 then 0/0 leaves both. PCSX2's micro path sets NO sticky D or I at all, so this is a divergence in the opposite direction from the macro path's", + kVuStickyProg2Lower, kVuStickyProg2Upper, 25u, + {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + 0x00000828u, 0x00000C18u, + 0x00000C18u, 0x00008000u, 0x00000000u, 0x7FFFFFFFu, 0x00000000u, false}, + {"VUSTICKY_MICRO_CLEAN_DIV_KEEPS_STICKY_DI", + "Micro-mode counterpart of the clean-divide case: the cause pair clears, sticky D and I stand", + kVuStickyProg3Lower, kVuStickyProg3Upper, 25u, + {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, + 0x00000C38u, 0x00000C08u, + 0x00000C08u, 0x00008000u, 0x00000000u, 0x3F800000u, 0x00000000u, false}, + {"VUSTICKY_MICRO_FSSET_CLEARS", + "FSSET 0 clears the sticky field an FMAC had set", + kVuStickyProg4Lower, kVuStickyProg4Upper, 25u, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, + 0x00000145u, 0x00000005u, + 0x00000005u, 0x00000808u, 0x00000000u, 0x3F800000u, 0x00000000u, true}, + {"VUSTICKY_MICRO_FSSET_WRITE_MASK", + "FSSET writes all six sticky bits: 0xFC0 from a clean STATUS reads back whole", + kVuStickyProg5Lower, kVuStickyProg5Upper, 25u, + {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, + 0x00000FC5u, 0x00000FC5u, + 0x00000FC5u, 0x00000808u, 0x00000000u, 0x3F800000u, 0x00000000u, false}, + {"VUSTICKY_MICRO_FSSET_ASSIGNS_NOT_ORS", + "FSSET ASSIGNS the sticky field rather than ORing into it: after an underflow leaves sticky ZU, FSSET 0x800 reads back sticky D alone, not sticky ZUD", + kVuStickyProg6Lower, kVuStickyProg6Upper, 25u, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, + 0x00000145u, 0x00000805u, + 0x00000805u, 0x00000808u, 0x00000000u, 0x3F800000u, 0x00000000u, true}, + {"VUSTICKY_MICRO_SURVIVES_SILENT_FMAC", + "Micro-mode counterpart of the silent-FMAC case", + kVuStickyProg7Lower, kVuStickyProg7Upper, 25u, + {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, {0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u}, + 0x00000145u, 0x00000140u, + 0x00000140u, 0x00000000u, 0x00000000u, 0x3F800000u, 0x00000000u, true}, +}; + +} // namespace console_vusticky diff --git a/tests/ctest/core/recompilers/vu_sticky_console_conformance_tests.cpp b/tests/ctest/core/recompilers/vu_sticky_console_conformance_tests.cpp new file mode 100644 index 0000000000..4c0527519a --- /dev/null +++ b/tests/ctest/core/recompilers/vu_sticky_console_conformance_tests.cpp @@ -0,0 +1,685 @@ +// SPDX-FileCopyrightText: 2026 yaps2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// VU sticky flags against a first-party console capture. +// +// Earlier VU captures issued `ctc2 $0, $vi16` between every pair of ops, which +// clears the sticky field. That isolates each op's flags -- but it means +// nothing in them says how the six sticky bits (STATUS 0xFC0) accumulate. +// Every case here deliberately never clears. +// +// PCSX2 contradicts itself on the central question. For the div unit: +// +// micro _vuFDIVflush: STATUS = (STATUS & 0xFCF) | (statusflag & 0xC30) +// macro SYNCFDIV: STATUS = (STATUS & 0x3CF) | (statusflag & 0x30) +// | ((statusflag & 0x30) << 6) +// +// The macro line clears sticky D and I (bits 10-11) on every div-unit op and +// rewrites them from the new event. The micro line keeps them -- but +// `statusflag` never carries sticky bits outside an FSSET, so in practice the +// micro path never sets sticky D or I at all. Both cannot be right, and the +// console says neither is: the stickies accumulate, in both modes. +// +// What the capture establishes, in the order the cases prove it: +// +// 1. All six sticky bits are monotone. Only an explicit write clears them +// (CTC2 in macro mode, FSSET in micro mode). Proven for Z/S/U/O from the +// FMAC pipe, for D/I from the div unit, and across the two -- a divide +// leaves the FMAC's stickies alone and an FMAC leaves the divide's alone. +// 2. A clean divide clears the D/I *cause* pair and keeps the D/I stickies. +// 3. The cause nibble is not a stored bit of the register. `ctc2 $0` clears +// the stickies and leaves the cause standing, and the ZSUO cause always +// equals the OR of the MAC register's four lane nibbles. +// 4. FSSET assigns the sticky field; it does not OR into it. +// +// Reads go through CFC2, the way the console observed them, rather than +// through the VU0 snapshot -- CFC2 is the path under test. + +#include + +#include "harness/EeRecTestHarness.h" +#include "harness/MipsEncode.h" +#include "harness/RecompilerTestEnvironment.h" +#include "harness/VuEncode.h" +#include "harness/VuTestHarness.h" + +#include "VU.h" + +#include +#include + +#include "autocases_vusticky.h" + +using namespace console_vusticky; + +namespace recompiler_tests +{ +namespace +{ +using namespace mips; +using namespace mips::ee; + +// One VF pair per op, so the block never has to reload operands mid-stream the +// way the console probe did with LQC2. +constexpr u32 kFs[3] = {4, 7, 11}; +constexpr u32 kFt[3] = {5, 8, 12}; +constexpr u32 kFd = 6; +constexpr u32 kVfOne = 10; // 1.0 in every lane, for the prologue + +// GPRs holding the four (STATUS, MAC, Q) triples plus the final CLIP. +constexpr u32 kRStatus[4] = {8, 11, 14, 17}; +constexpr u32 kRMac[4] = {9, 12, 15, 18}; +constexpr u32 kRQ[4] = {10, 13, 16, 19}; +constexpr u32 kRClip = 20; +constexpr u32 kRTmp = 21; + +constexpr u32 kStickyMask = 0xFC0u; +constexpr u32 kCauseZsuo = 0x00Fu; +constexpr u32 kCauseDi = 0x030u; + +void AppendOp(std::vector& prog, const VuStickyOp& op, int slot) +{ + const u32 fs = kFs[slot], ft = kFt[slot]; + switch (op.kind) + { + case VS_NOP: prog.push_back(NOP); break; + case VS_MUL: prog.push_back(VMUL_C2(op.mask, kFd, fs, ft)); break; + case VS_ADD: prog.push_back(VADD_C2(op.mask, kFd, fs, ft)); break; + case VS_MUL_MASK0: prog.push_back(VMUL_C2(op.mask, kFd, fs, ft)); break; + case VS_DIV: prog.push_back(VDIV_C2(0, 0, fs, ft)); break; + case VS_SQRT: prog.push_back(VSQRT_C2(0, ft)); break; + case VS_RSQRT: prog.push_back(VRSQRT_C2(0, 0, fs, ft)); break; + case VS_CLIP: prog.push_back(VCLIP_C2(ft, fs)); break; + case VS_IADD: prog.push_back(VIADD_C2(1, 2, 3)); break; + case VS_CTC2_ZERO: prog.push_back(CTC2(0, REG_STATUS_FLAG)); break; + case VS_CTC2_FFF: + prog.push_back(ORI(kRTmp, 0, 0xFFF)); + prog.push_back(CTC2(kRTmp, REG_STATUS_FLAG)); + break; + } +} + +void AppendRead(std::vector& prog, int k) +{ + prog.push_back(CFC2(kRStatus[k], REG_STATUS_FLAG)); + prog.push_back(CFC2(kRMac[k], REG_MAC_FLAG)); + prog.push_back(CFC2(kRQ[k], REG_Q)); +} + +// Builds and runs one case, leaving the harness available for read-back. +// The prologue mirrors the probe's: a clean FMAC and a clean divide settle the +// cause nibble, then CTC2 clears the stickies and CLIP. Whether an FMAC +// clears the divide's D/I is one of the things under test, so the setup must +// not assume it -- hence both. +void BuildProgram(EeRecTestHarness& h, const VuStickyCase& c) +{ + h.EnableVu0Capture(); + for (int s = 0; s < 3; ++s) + { + h.SeedVu0VfBits(kFs[s], c.op[s].fs[0], c.op[s].fs[1], c.op[s].fs[2], c.op[s].fs[3]); + h.SeedVu0VfBits(kFt[s], c.op[s].ft[0], c.op[s].ft[1], c.op[s].ft[2], c.op[s].ft[3]); + } + h.SeedVu0VfBits(kVfOne, 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u); + h.SeedVu0VfBits(kFd, 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u); + + std::vector prog; + prog.push_back(VADD_C2(0x8, kFd, kVfOne, kVfOne)); + prog.push_back(VDIV_C2(0, 0, kVfOne, kVfOne)); + prog.push_back(CTC2(0, REG_STATUS_FLAG)); + prog.push_back(CTC2(0, REG_CLIP_FLAG)); + AppendRead(prog, 0); + for (int s = 0; s < 3; ++s) + { + AppendOp(prog, c.op[s], s); + AppendRead(prog, s + 1); + } + prog.push_back(CFC2(kRClip, REG_CLIP_FLAG)); + h.LoadProgram(prog); +} + +// A recorded per-engine divergence: this case's read `slot` does not match the +// console on the named engine, and that is the state as of the last capture. +struct Divergence +{ + const char* tag; + int slot; // 0 = post-prologue, 1..3 after each op + bool interp; + bool jit; + const char* cause; +}; + +// Recorded from an actual run of both engines, never derived from a rule. Six +// distinct causes are represented; each row names its own. +// +// The largest group is one defect, seen thirty times: `cop2EmitFlagUpdate` +// (pcsx2/arm64/iCOP2-arm64.cpp) extracts a sign bit (CMLT) and a zero bit +// (FCMEQ) per lane and nothing else, so the arm64 COP2 macro path can never +// raise MAC U or MAC O -- and, because the underflowing product is left as a +// denormal rather than flushed, it does not even raise Z where hardware does. +// `Arm64Cop2MacroFlagExtractionIsSignAndZeroOnly` below states that once, with +// a minimal witness; these rows are its fallout across the sticky cases. +// +// The rest are one-line mask defects in the shared interpreter, and in every +// one of them the arm64 JIT is the engine that matches the console. +constexpr Divergence kMacroStatusDivergences[] = { + {"VUSTICKY_FMAC_ZSUO_ACCUMULATE", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_FMAC_ZSUO_ACCUMULATE", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_FMAC_ZSUO_ACCUMULATE", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_SURVIVES_SILENT_FMAC", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_SURVIVES_SILENT_FMAC", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_SURVIVES_SILENT_FMAC", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_ONE_OP_ALL_FOUR", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_ONE_OP_ALL_FOUR", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_ONE_OP_ALL_FOUR", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_DIV_DI_ACCUMULATE", 2, true, false, + "macro SYNCFDIV clears sticky D/I (VUops.cpp:3907)"}, + {"VUSTICKY_DIV_DI_ACCUMULATE", 3, true, false, + "macro SYNCFDIV clears sticky D/I (VUops.cpp:3907)"}, + {"VUSTICKY_DIV_KEEPS_FMAC_FLAGS", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_DIV_KEEPS_FMAC_FLAGS", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_DIV_KEEPS_FMAC_FLAGS", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_FMAC_KEEPS_DI", 2, true, false, + "macro SYNCMSFLAGS drops the D/I cause (VUops.cpp:3889)"}, + {"VUSTICKY_FMAC_KEEPS_DI", 3, true, false, + "macro SYNCMSFLAGS drops the D/I cause (VUops.cpp:3889)"}, + {"VUSTICKY_DI_ACCUMULATE_SQRT_DIV", 2, true, false, + "macro SYNCFDIV clears sticky D/I (VUops.cpp:3907)"}, + {"VUSTICKY_DI_ACCUMULATE_SQRT_DIV", 3, true, false, + "macro SYNCFDIV clears sticky D/I (VUops.cpp:3907)"}, + {"VUSTICKY_CTC2_CLEARS_STICKY_NOT_CAUSE", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_CTC2_CLEARS_STICKY_NOT_CAUSE", 2, true, true, + "CTC2 to STATUS overwrites the live cause nibble"}, + {"VUSTICKY_CTC2_CLEARS_STICKY_NOT_CAUSE", 3, true, true, + "CTC2 to STATUS overwrites the live cause nibble"}, + {"VUSTICKY_CTC2_WRITTEN_AND_OP_SET_ALIKE", 1, true, false, + "interpreter CTC2 stores all 32 bits; the JIT's 0xFC0 mask is the correct one"}, + {"VUSTICKY_CTC2_WRITTEN_AND_OP_SET_ALIKE", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_EMPTY_DEST_MASK_SILENT", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_EMPTY_DEST_MASK_SILENT", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_EMPTY_DEST_MASK_SILENT", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_VCLIP_TOUCHES_ONLY_CLIP", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_VCLIP_TOUCHES_ONLY_CLIP", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_VCLIP_TOUCHES_ONLY_CLIP", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_INTEGER_OP_SILENT", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_INTEGER_OP_SILENT", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_INTEGER_OP_SILENT", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_THREE_EVENTS_ONE_ACCUMULATION", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_THREE_EVENTS_ONE_ACCUMULATION", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_THREE_EVENTS_ONE_ACCUMULATION", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_DI_CAUSE_REPLACED_STICKY_KEPT", 2, true, false, + "macro SYNCFDIV clears sticky D/I (VUops.cpp:3907)"}, + {"VUSTICKY_DI_CAUSE_REPLACED_STICKY_KEPT", 3, true, false, + "macro SYNCFDIV clears sticky D/I (VUops.cpp:3907)"}, + {"VUSTICKY_CLEAN_DIV_KEEPS_STICKY_DI", 1, true, true, + "vrsqrt of -0 raises only D; hardware raises D and I"}, + {"VUSTICKY_CLEAN_DIV_KEEPS_STICKY_DI", 2, true, true, + "macro SYNCFDIV clears sticky D/I (VUops.cpp:3907)"}, + {"VUSTICKY_CLEAN_DIV_KEEPS_STICKY_DI", 3, true, true, + "macro SYNCFDIV clears sticky D/I (VUops.cpp:3907)"}, +}; + +// MAC is scored on its own table for the same reason STATUS is: a mask defect +// in the flag merge and a missing lane-flag extraction are different bugs, and +// collapsing them would hide which engine is wrong about what. +constexpr Divergence kMacroMacDivergences[] = { + {"VUSTICKY_FMAC_ZSUO_ACCUMULATE", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_FMAC_ZSUO_ACCUMULATE", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_FMAC_ZSUO_ACCUMULATE", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_SURVIVES_SILENT_FMAC", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_ONE_OP_ALL_FOUR", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_ONE_OP_ALL_FOUR", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_ONE_OP_ALL_FOUR", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_DIV_KEEPS_FMAC_FLAGS", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_DIV_KEEPS_FMAC_FLAGS", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_DIV_KEEPS_FMAC_FLAGS", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_CTC2_CLEARS_STICKY_NOT_CAUSE", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_CTC2_CLEARS_STICKY_NOT_CAUSE", 2, false, true, + "CTC2 to STATUS overwrites the live cause nibble"}, + {"VUSTICKY_CTC2_CLEARS_STICKY_NOT_CAUSE", 3, false, true, + "CTC2 to STATUS overwrites the live cause nibble"}, + {"VUSTICKY_CTC2_WRITTEN_AND_OP_SET_ALIKE", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_EMPTY_DEST_MASK_SILENT", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_VCLIP_TOUCHES_ONLY_CLIP", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_VCLIP_TOUCHES_ONLY_CLIP", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_VCLIP_TOUCHES_ONLY_CLIP", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_INTEGER_OP_SILENT", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_INTEGER_OP_SILENT", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_INTEGER_OP_SILENT", 3, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_THREE_EVENTS_ONE_ACCUMULATION", 1, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, + {"VUSTICKY_THREE_EVENTS_ONE_ACCUMULATION", 2, false, true, + "arm64 cop2EmitFlagUpdate extracts sign and zero only -- no U/O, no underflow flush"}, +}; + +// Micro mode. Note the reversal: on the div-unit stickies and on FSSET it is +// the shared INTERPRETER that is wrong and microVU that matches the console. +constexpr Divergence kMicroDivergences[] = { + {"VUSTICKY_MICRO_FMAC_ZSUO_ACCUMULATE", 3, true, true, + "micro FMAC loses U (VU_MAC_UPDATE's ~0x1100 clears U on a flush-to-zero) " + "and O (the configured clamp mode saturates below exp 255)"}, + {"VUSTICKY_MICRO_DIV_DI_ACCUMULATE", 3, true, false, + "_vuFDIVflush ORs `statusflag & 0xC30`, but statusflag never carries a " + "sticky bit, so the interpreter's micro path sets NO sticky D or I at all; " + "microVU accumulates them and is correct (VUops.cpp:104)"}, + {"VUSTICKY_MICRO_CLEAN_DIV_KEEPS_STICKY_DI", 3, true, true, + "interpreter: the same _vuFDIVflush gap; JIT: vrsqrt of -0 raises only D " + "where hardware raises D and I, so sticky I is never set"}, + {"VUSTICKY_MICRO_FSSET_CLEARS", 3, true, false, + "the interpreter's FSSET leaves the existing sticky bits standing; " + "hardware and microVU both assign the field"}, + {"VUSTICKY_MICRO_FSSET_ASSIGNS_NOT_ORS", 3, true, false, + "same FSSET gap, in the form that separates assign from OR"}, + {"VUSTICKY_MICRO_SURVIVES_SILENT_FMAC", 3, true, true, + "micro FMAC loses U on the flush-to-zero underflow, as above"}, +}; + +const Divergence* FindDivergence(const Divergence* table, size_t n, const char* tag, int slot) +{ + for (size_t i = 0; i < n; ++i) + if (table[i].slot == slot && std::string(tag) == table[i].tag) + return &table[i]; + return nullptr; +} + +#define MACRO_STATUS_DIVERGENCE(tag, slot) \ + FindDivergence(kMacroStatusDivergences, std::size(kMacroStatusDivergences), tag, slot) +#define MACRO_MAC_DIVERGENCE(tag, slot) \ + FindDivergence(kMacroMacDivergences, std::size(kMacroMacDivergences), tag, slot) +#define MICRO_DIVERGENCE(tag, slot) \ + FindDivergence(kMicroDivergences, std::size(kMicroDivergences), tag, slot) + +std::vector ProgramPairs(const VuStickyProgram& p) +{ + std::vector pairs; + for (u32 i = 0; i < p.n_pairs; ++i) + pairs.push_back(vu::VuOp{p.lower[i], p.upper[i]}); + return pairs; +} + +void SeedMicro(VuTestHarness& h, const VuStickyProgram& p) +{ + h.SetVfBits(4, p.seed_fs1[0], p.seed_fs1[1], p.seed_fs1[2], p.seed_fs1[3]); + h.SetVfBits(5, p.seed_ft1[0], p.seed_ft1[1], p.seed_ft1[2], p.seed_ft1[3]); + h.SetVfBits(7, p.seed_fs2[0], p.seed_fs2[1], p.seed_fs2[2], p.seed_fs2[3]); + h.SetVfBits(8, p.seed_ft2[0], p.seed_ft2[1], p.seed_ft2[2], p.seed_ft2[3]); +} + +// The ZSUO cause nibble is the OR of the MAC register's four lane nibbles +// (VUflags.cpp VU_MAC_UPDATE: Z = 0x0001<interp) + EXPECT_NE(got_i, want) << "[interp] recorded divergence has been fixed"; + else + EXPECT_EQ(got_i, want) << "[interp] STATUS"; + if (d && d->jit) + EXPECT_NE(got_j, want) << "[jit] recorded divergence has been fixed"; + else + EXPECT_EQ(got_j, want) << "[jit] STATUS"; + if (d) + ++diverged; + ++checked; + } + } + EXPECT_EQ(checked, static_cast(std::size(kVuStickyCases)) * 4); + EXPECT_EQ(diverged, static_cast(std::size(kMacroStatusDivergences))); +} + +// MAC and CLIP are scored separately from STATUS: a STATUS divergence is about +// the flag-merge masks, and pinning the register values alongside it is what +// says the case executed the arithmetic the console executed. +// +// Q is deliberately NOT scored. The div-unit saturation value is a function of +// the configured VU clamp mode -- PCSX2 returns "max allowed" 0x7F7FFFFF where +// the console gives 0x7FFFFFFF -- which is a clamp-mode question, not a flag +// one. The console's Q is carried in the header for whoever wants it. +TEST(VuStickyConsoleConformance, MacroMacClipMatchConsole) +{ + int checked = 0, diverged = 0; + for (const VuStickyCase& c : kVuStickyCases) + { + EeRecTestHarness h; + BuildProgram(h, c); + h.RunJitNoDiff(); + EeRecTestHarness hi; + BuildProgram(hi, c); + hi.RunInterpOnly(); + + for (int k = 0; k < 4; ++k) + { + SCOPED_TRACE(::testing::Message() << c.tag << " read " << k); + const Divergence* d = MACRO_MAC_DIVERGENCE(c.tag, k); + const u32 want = c.read[k].mac; + if (d && d->interp) + EXPECT_NE(hi.GetGprInterp(kRMac[k]), want) << "[interp] MAC divergence fixed"; + else + EXPECT_EQ(hi.GetGprInterp(kRMac[k]), want) << "[interp] MAC"; + if (d && d->jit) + EXPECT_NE(h.GetGprJit(kRMac[k]), want) << "[jit] MAC divergence fixed"; + else + EXPECT_EQ(h.GetGprJit(kRMac[k]), want) << "[jit] MAC"; + if (d) + ++diverged; + ++checked; + } + SCOPED_TRACE(::testing::Message() << c.tag << " clip"); + EXPECT_EQ(hi.GetGprInterp(kRClip), c.clip) << "[interp] CLIP"; + EXPECT_EQ(h.GetGprJit(kRClip), c.clip) << "[jit] CLIP"; + } + EXPECT_EQ(checked, static_cast(std::size(kVuStickyCases)) * 4); + EXPECT_EQ(diverged, static_cast(std::size(kMacroMacDivergences))); +} + +// The class behind thirty of the STATUS rows and every one of the MAC rows, +// stated once with the smallest witness that shows it. +// +// `cop2EmitFlagUpdate` (pcsx2/arm64/iCOP2-arm64.cpp) builds the MAC flag from +// exactly two per-lane predicates -- CMLT for the sign bit and FCMEQ for the +// zero bit -- and then clears the U/O positions outright. So on the arm64 COP2 +// macro path: +// +// * a product that underflows raises neither U nor Z, because the result is +// left as a denormal instead of being flushed to zero; +// * a product that overflows raises no O. +// +// The shared interpreter gets both right, and both match the console, so the +// gap is one emitter's, not the model's. Recorded rather than fixed: the fix +// belongs with the emitter and wants its own before/after. +TEST(VuStickyConsoleConformance, Arm64Cop2MacroFlagExtractionIsSignAndZeroOnly) +{ + struct Witness + { + const char* what; + u32 fs, ft; + u32 console_mac; + }; + // Straight off the console: read 1 of the two cases that use these operands. + constexpr Witness kWitnesses[] = { + {"underflow (2^-126 * 0.5)", 0x00800000u, 0x3F000000u, 0x0808u}, + {"overflow (2^127 * 2^127)", 0x7F000000u, 0x7F000000u, 0x8000u}, + }; + for (const Witness& w : kWitnesses) + { + SCOPED_TRACE(w.what); + const auto build = [&](EeRecTestHarness& h) { + h.EnableVu0Capture(); + h.SeedVu0VfBits(4, w.fs, w.fs, w.fs, w.fs); + h.SeedVu0VfBits(5, w.ft, w.ft, w.ft, w.ft); + h.LoadProgram({ + CTC2(0, REG_STATUS_FLAG), + VMUL_C2(0x8, 6, 4, 5), + CFC2(kRMac[0], REG_MAC_FLAG), + }); + }; + EeRecTestHarness hi; + build(hi); + hi.RunInterpOnly(); + EeRecTestHarness hj; + build(hj); + hj.RunJitNoDiff(); + EXPECT_EQ(hi.GetGprInterp(kRMac[0]), w.console_mac) + << "the interpreter is the control here and must match the console"; + EXPECT_EQ(hj.GetGprJit(kRMac[0]), 0u) + << "the arm64 COP2 macro path is recorded as raising no flag at all " + "for this operand pair; if it now raises one, retire this test"; + } +} + +TEST(VuStickyConsoleConformance, DISABLED_Arm64Cop2MacroExtractsUnderflowAndOverflow) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.SeedVu0VfBits(4, 0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u); + h.SeedVu0VfBits(5, 0x3F000000u, 0x3F000000u, 0x3F000000u, 0x3F000000u); + h.LoadProgram({ + CTC2(0, REG_STATUS_FLAG), + VMUL_C2(0x8, 6, 4, 5), + CFC2(kRMac[0], REG_MAC_FLAG), + }); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetGprJit(kRMac[0]), 0x0808u) << "MAC Z+U on an underflowing product"; +} + +// The structural law behind case 8: the ZSUO cause nibble is not stored in +// STATUS, it tracks MAC. Asserted on the CONSOLE data, so it stands whatever +// the emulator does, and separately on each engine, where it must also hold -- +// PCSX2 derives both from the same `macflag`, so an engine that broke it would +// have broken the MAC register too. +TEST(VuStickyConsoleConformance, CauseNibbleTracksMac) +{ + int console_checked = 0, engine_checked = 0; + for (const VuStickyCase& c : kVuStickyCases) + { + for (int k = 0; k < 4; ++k) + { + SCOPED_TRACE(::testing::Message() << c.tag << " read " << k); + EXPECT_EQ(c.read[k].status & kCauseZsuo, CauseFromMac(c.read[k].mac)) + << "console STATUS cause does not match its MAC"; + ++console_checked; + } + } + for (const VuStickyCase& c : kVuStickyCases) + { + EeRecTestHarness h; + BuildProgram(h, c); + h.RunJitNoDiff(); + for (int k = 0; k < 4; ++k) + { + SCOPED_TRACE(::testing::Message() << c.tag << " read " << k << " [jit]"); + EXPECT_EQ(h.GetGprJit(kRStatus[k]) & kCauseZsuo, + CauseFromMac(h.GetGprJit(kRMac[k]))); + ++engine_checked; + } + } + EXPECT_EQ(console_checked, static_cast(std::size(kVuStickyCases)) * 4); + EXPECT_EQ(engine_checked, console_checked); +} + +// Monotonicity, stated as a property of the capture rather than of any one +// case: outside the two cases that write STATUS explicitly, no op ever clears +// a sticky bit. This is the assertion that would catch a future capture, or a +// future edit of the case table, that quietly contradicts the finding. +TEST(VuStickyConsoleConformance, StickyFieldIsMonotoneWithoutAWrite) +{ + int transitions = 0, writes = 0; + for (const VuStickyCase& c : kVuStickyCases) + { + for (int k = 0; k < 3; ++k) + { + const bool writer = c.op[k].kind == VS_CTC2_ZERO || c.op[k].kind == VS_CTC2_FFF; + const u32 prev = c.read[k].status & kStickyMask; + const u32 cur = c.read[k + 1].status & kStickyMask; + SCOPED_TRACE(::testing::Message() << c.tag << " op" << (k + 1)); + if (writer) + { + EXPECT_NE(prev, cur) << "the explicit STATUS write changed nothing"; + ++writes; + } + else + { + EXPECT_EQ(prev & ~cur, 0u) << "a sticky bit was cleared with no write"; + ++transitions; + } + } + } + EXPECT_EQ(transitions + writes, static_cast(std::size(kVuStickyCases)) * 3); + EXPECT_EQ(writes, 2); +} + +// --------------------------------------------------------------------------- +// Group B -- VU0 micro mode +// --------------------------------------------------------------------------- +// +// Scored on the sticky field and the D/I cause only. The console ran the +// eight programs back to back, so a program with no FMAC of its own inherited +// the previous one's MAC -- and the ZSUO cause tracks MAC, so those four bits +// carry an artifact a harness starting from a clean VU cannot reproduce. The +// rule itself is pinned by CauseNibbleTracksMac above. + +TEST(VuStickyMicroConsoleConformance, MicroStatusMatchesConsole) +{ + int checked = 0, diverged = 0; + for (const VuStickyProgram& p : kVuStickyPrograms) + { + VuTestHarness h(0); + SeedMicro(h, p); + h.LoadProgram(ProgramPairs(p)); + h.RunNoDiff(); + ASSERT_TRUE(h.HasTerminated()) << p.tag << " did not reach its E bit"; + + const u32 mask = kStickyMask | kCauseDi; + const u32 want = p.final_status & mask; + SCOPED_TRACE(::testing::Message() << p.tag << " -- " << p.rule); + const Divergence* d = MICRO_DIVERGENCE(p.tag, 3); + const u32 got_i = h.GetViInterp(REG_STATUS_FLAG) & mask; + const u32 got_j = h.GetViJit(REG_STATUS_FLAG) & mask; + if (d && d->interp) + EXPECT_NE(got_i, want) << "[interp] recorded divergence has been fixed"; + else + EXPECT_EQ(got_i, want) << "[interp] STATUS sticky+DI"; + if (d && d->jit) + EXPECT_NE(got_j, want) << "[jit] recorded divergence has been fixed"; + else + EXPECT_EQ(got_j, want) << "[jit] STATUS sticky+DI"; + if (d) + ++diverged; + ++checked; + } + EXPECT_EQ(checked, static_cast(std::size(kVuStickyPrograms))); + EXPECT_EQ(diverged, static_cast(std::size(kMicroDivergences))); +} + +// The path control. Program 0 does no flag work at all -- it exists so that a +// zero anywhere else in group B means "the emulator got the flags wrong", not +// "the microprogram never ran". +TEST(VuStickyMicroConsoleConformance, MicroPathControlRuns) +{ + const VuStickyProgram& p = kVuStickyPrograms[0]; + ASSERT_STREQ(p.tag, "VUSTICKY_MICRO_PATH_CONTROL"); + VuTestHarness h(0); + SeedMicro(h, p); + h.LoadProgram(ProgramPairs(p)); + h.RunNoDiff(); + ASSERT_TRUE(h.HasTerminated()); + EXPECT_EQ(p.final_vi01, 0x123u) << "the console's own control did not run"; + EXPECT_EQ(h.GetViInterp(1), 0x123u) << "[interp]"; + EXPECT_EQ(h.GetViJit(1), 0x123u) << "[jit]"; +} + +// --------------------------------------------------------------------------- +// Tripwires +// --------------------------------------------------------------------------- + +TEST(VuStickyConsoleConformance, DISABLED_AllMacroStatusMatchesConsole) +{ + for (const VuStickyCase& c : kVuStickyCases) + { + EeRecTestHarness h; + BuildProgram(h, c); + h.RunJitNoDiff(); + EeRecTestHarness hi; + BuildProgram(hi, c); + hi.RunInterpOnly(); + for (int k = 0; k < 4; ++k) + { + SCOPED_TRACE(::testing::Message() << c.tag << " read " << k << " -- " << c.rule); + EXPECT_EQ(hi.GetGprInterp(kRStatus[k]), c.read[k].status) << "[interp]"; + EXPECT_EQ(h.GetGprJit(kRStatus[k]), c.read[k].status) << "[jit]"; + } + } +} + +TEST(VuStickyMicroConsoleConformance, DISABLED_AllMicroStatusMatchesConsole) +{ + for (const VuStickyProgram& p : kVuStickyPrograms) + { + VuTestHarness h(0); + SeedMicro(h, p); + h.LoadProgram(ProgramPairs(p)); + h.RunNoDiff(); + const u32 mask = kStickyMask | kCauseDi; + SCOPED_TRACE(::testing::Message() << p.tag << " -- " << p.rule); + EXPECT_EQ(h.GetViInterp(REG_STATUS_FLAG) & mask, p.final_status & mask) << "[interp]"; + EXPECT_EQ(h.GetViJit(REG_STATUS_FLAG) & mask, p.final_status & mask) << "[jit]"; + } +} + +} // namespace recompiler_tests