mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
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:
co-authored by
Claude
parent
9d0f84b12e
commit
ac51b16ea7
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user