arm64 COP2 macro: VSUB with Fs==Ft is exact +0 (True Crime black world)

PS2 VU floats have no inf/NaN — exp-FF bit patterns are valid huge
numbers and x - x cancels to +0 exactly. The hand-rolled macro VSUB
emitted a raw Fsub whose NaN - NaN result the unconditional result clamp
turned into +FLT_MAX, corrupting True Crime NYC's VU0-macro camera/bbox
kernel (qmtc2 leaves stale EE-GPR bits in the upper lanes; the game
zeroes W with vsub.w vf1,vf1,vf1): world geometry degenerates and the
scene renders black with a live HUD within 2 frames, bistable with the
EE interp toggle. Interp (vuDouble operand clamping), x86 mVU
(microVU_Upper's (_Ft_ == _Fs_) opCase1 short-circuit), and our arm64
micro-mode port all produce +0 — only the hand-rolled macro op dropped
the corner. Short-circuit Fs==Ft to a zero move like micro mode,
non-broadcast only, matching x86 ("Don't do this with BC's!").

Found via the twindiff cross-build comparator (armsx2-master gold JIT vs
yaps2; three-engine-consensus corruption at 0x58aa54/0x540aa4/0x58ab24)
plus a gdb watchpoint on the fastmem alias. Pinned by
EeVu0Cop2Macro.VsubSameRegNanPattern{MaskedW,FullMask} — red before,
green after; full recompiler_tests suite 1391/1391.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Brian Degenhardt
2026-07-20 12:30:33 -07:00
co-authored by Claude
parent 9d0f84b12e
commit ac51b16ea7
2 changed files with 57 additions and 4 deletions
+17 -4
View File
@@ -1543,11 +1543,24 @@ void recCOP2_VSUB()
if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return;
setupMacroOp_arm64(0x110);
const a64::VRegister fs = cop2GetVF(_Fs_cop2);
const a64::VRegister ft = cop2GetVF(_Ft_cop2);
const a64::VRegister rd = cop2ResultReg(_Fd_cop2, _XYZW_cop2);
armAsm->Fsub(rd.V4S(), fs.V4S(), ft.V4S());
cop2ClampResultReg(rd);
if (_Fs_cop2 == _Ft_cop2)
{
// PS2 x - x is exactly +0 in every lane: VU floats have no inf/NaN,
// so exp-FF bit patterns are valid huge numbers that cancel. A host
// Fsub would give NaN - NaN = NaN and the result clamp would turn
// that into +FLT_MAX (True Crime NYC black-world, 2026-07-20).
// Mirrors microVU_Upper's (_Ft_ == _Fs_) opCase1 short-circuit —
// non-broadcast only, matching x86 ("Don't do this with BC's!").
armAsm->Movi(rd.V4S(), 0);
}
else
{
const a64::VRegister fs = cop2GetVF(_Fs_cop2);
const a64::VRegister ft = cop2GetVF(_Ft_cop2);
armAsm->Fsub(rd.V4S(), fs.V4S(), ft.V4S());
cop2ClampResultReg(rd);
}
cop2EmitFlagUpdate(_XYZW_cop2, rd);
cop2ApplyDestMaskExplicit(_Fd_cop2, _XYZW_cop2, rd);
@@ -102,6 +102,46 @@ TEST(EeVu0Cop2Macro, VmulXyzwProductsLanes)
EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l));
}
// True Crime NYC black-world regression (2026-07-20): the game zeroes W with
// `vsub.w vf1,vf1,vf1` after a QMTC2 left exp-FF bit patterns in the upper
// lanes. PS2 VU floats have no inf/NaN — exp-FF is a valid huge number and
// x - x is exactly +0 in every lane. A raw host Fsub gives NaN - NaN = NaN,
// and the result clamp then manufactures +FLT_MAX (0x7f7fffff) where the
// architecture demands 0 — blowing up the camera/bbox kernel. Mirrors
// microVU_Upper's (_Ft_ == _Fs_) opCase1 zero short-circuit (non-broadcast
// only); the micro-mode arm64 port has it, the hand-rolled macro op dropped
// it.
TEST(EeVu0Cop2Macro, VsubSameRegNanPatternIsExactZeroMaskedW)
{
EeRecTestHarness h;
h.EnableVu0Capture();
h.EnableCop1();
h.SeedVu0VfBits(1, 0x3f800000u, 0x40000000u, 0xff800000u, 0xffffffffu);
h.LoadProgram({VSUB_C2(/*mask w*/ 0x1, /*fd*/1, /*fs*/1, /*ft*/1)});
h.Run();
EXPECT_EQ(h.GetVu0VfBitsJit(1, 'w'), 0u);
// Unmasked lanes keep their original bit patterns.
EXPECT_EQ(h.GetVu0VfBitsJit(1, 'x'), 0x3f800000u);
EXPECT_EQ(h.GetVu0VfBitsJit(1, 'z'), 0xff800000u);
for (char l : {'x', 'y', 'z', 'w'})
EXPECT_EQ(h.GetVu0VfBitsJit(1, l), h.GetVu0VfBitsInterp(1, l));
}
TEST(EeVu0Cop2Macro, VsubSameRegNanPatternIsExactZeroFullMask)
{
EeRecTestHarness h;
h.EnableVu0Capture();
h.EnableCop1();
h.SeedVu0VfBits(2, 0xffffffffu, 0x7fffffffu, 0xff800000u, 0x7f800000u);
h.LoadProgram({VSUB_C2(mask_xyzw, /*fd*/3, /*fs*/2, /*ft*/2)});
h.Run();
for (char l : {'x', 'y', 'z', 'w'})
{
EXPECT_EQ(h.GetVu0VfBitsJit(3, l), 0u);
EXPECT_EQ(h.GetVu0VfBitsJit(3, l), h.GetVu0VfBitsInterp(3, l));
}
}
TEST(EeVu0Cop2Macro, VaddMaskedYZOnlyTouchesYZ)
{
EeRecTestHarness h;