mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
arm64 mVU: mVU_XGKICK_SYNC must fully flush the regalloc unconditionally
Root cause of the Crash Twinsanity VIF1<->VU1 VEW-deadlock hang. mVU_XGKICK_SYNC opened with flushCallerSavedRegisters() (x9-x15 only) and relied on the mVUbackupRegs (= regAlloc->flushAll()) emitted between the runtime `xgkickenable` / `cyclecount >= 2` branches and the _vuXGKICKTransfermVU call to spill everything else. But the allocator marks registers clean at COMPILE time, unconditionally — so a dirty CALLEE-SAVED VI allocation (w26) got its only writeback store emitted on a conditionally-executed path. At runtime, whenever no kick was pending (xgkickenable==0 — e.g. always, in programs with no XGKICK op), the spill was skipped: the value died in the callee-saved register at dispatcher exit, and the E-bit end flush wrote nothing because the reg was already "clean". x86 shares the emit structure but is immune: its mVU GPR pool is all caller-saved, so the top-of-function flush covers every dirty register and the conditional backup never has regalloc state left to spill. Our callee-saved-bearing pool turned the latent structure into a real bug. Twinsanity (GameDB forces XGKickHack=true) lost exactly two writes per frame in the pc0x0 display-list trampoline: the JALR link (vi15=0x0a) and the terminal handler's vi12=0 (double-buffer parity) — both landing in w26 with sync sites before their next flush. The walk then wedged, VIF1's VEW never cleared, and the EE bc0f-spun at 0x1820a4. Invisible to every offline oracle because the replay harness pins XgKickHack=false (different compiled shape); reproduced offline via PCSX2_VU_XGKICKHACK=1 on the fresh-boot capture (vi12/vi15 signature exact, from frame 0). Fix: flushAll() before any conditional emission, so every store the allocator accounts for executes on every runtime path. TDD: Vu1Xgkick.XgKickHackSyncMustNotLoseCalleeSavedViWrites red on unfixed code (vi3 JIT=0 vs INTERP=9), green after. recompiler_tests 1123/1123. twin-fresh2 corpus: hack-ON replay now bit-identical to hack-OFF reference on 768/769 caps (outlier = the budget-truncated final capture of the already-wedged state). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3a423cdb98
commit
9ee69fc238
@@ -1060,7 +1060,17 @@ static __fi void mVU_XGKICK_DELAY(mV)
|
||||
// JIT emitter: emit code for cycle-counted XGKICK sync
|
||||
static __fi void mVU_XGKICK_SYNC(mV, bool flush)
|
||||
{
|
||||
mVU.regAlloc->flushCallerSavedRegisters();
|
||||
// Full flush, NOT flushCallerSavedRegisters(): every store emitted here
|
||||
// must sit BEFORE the runtime xgkickenable / cyclecount branches below,
|
||||
// because the allocator marks the regs clean at compile time
|
||||
// unconditionally. Our GPR pool contains CALLEE-SAVED registers (unlike
|
||||
// x86's all-caller-saved pool), and a dirty callee-saved VI would
|
||||
// otherwise only be spilled by the mVUbackupRegs inside the conditional
|
||||
// path — skipped at runtime whenever no kick is pending, silently losing
|
||||
// the write (Crash Twinsanity: pc0x0's JALR link vi15 and the terminal
|
||||
// handler's vi12=0 died in w26 => VIF1 VEW deadlock; pinned by
|
||||
// Vu1Xgkick.XgKickHackSyncMustNotLoseCalleeSavedViWrites).
|
||||
mVU.regAlloc->flushAll();
|
||||
|
||||
// Test if xgkickenable is set
|
||||
a64::Label skipxgkick;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include "harness/VuTestHarness.h"
|
||||
|
||||
#include "Config.h"
|
||||
#include "VU.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
@@ -46,6 +47,50 @@ void WriteEopOnlyTagToVu1(VuTestHarness& h, u32 addr)
|
||||
|
||||
} // namespace
|
||||
|
||||
// Crash Twinsanity wedge root cause (2026-07-09): with the XgKickHack gamefix
|
||||
// ON (GameDB forces it for Twinsanity), every mVU_XGKICK_SYNC site must not
|
||||
// lose dirty VI register state. The sync's top-of-function
|
||||
// flushCallerSavedRegisters() only covers x9-x15; a VI allocated into a
|
||||
// CALLEE-SAVED pool register (w26) was only spilled by the mVUbackupRegs
|
||||
// emitted BETWEEN the runtime `xgkickenable` / `cyclecount >= 2` branches and
|
||||
// the C call — so at runtime the spill is skipped whenever no kick is pending
|
||||
// (xgkickenable==0), while the allocator's compile-time state says the reg is
|
||||
// clean. The value dies in the callee-saved reg at dispatcher exit; the E-bit
|
||||
// end flush writes nothing. Live signature: pc0x0's JALR link (vi15=0x0a) and
|
||||
// the terminal handler's vi12=0 lost => VIF1 VEW deadlock. x86 shares the
|
||||
// emit structure but is immune (its mVU GPR pool is all caller-saved).
|
||||
//
|
||||
// Program shape: three distinct VI writes so the third allocation lands in
|
||||
// the callee-saved pool reg, then an ISW (isMemWrite => kickcycles flush =>
|
||||
// SYNC site emitted before it) while that VI is dirty, then E-bit. No XGKICK
|
||||
// op at all — xgkickenable stays 0, so the buggy conditional spill is always
|
||||
// skipped at runtime.
|
||||
TEST(Vu1Xgkick, XgKickHackSyncMustNotLoseCalleeSavedViWrites)
|
||||
{
|
||||
VuTestHarness h(1);
|
||||
|
||||
const bool saved_hack = EmuConfig.Gamefixes.XgKickHack;
|
||||
EmuConfig.Gamefixes.XgKickHack = true;
|
||||
|
||||
h.LoadProgram({
|
||||
LowerOnly(VIADDIU_L(vi::vi1, vi::vi0, 5)), // vi1 = 5 (caller-saved alloc)
|
||||
LowerOnly(VIADDIU_L(vi::vi2, vi::vi0, 7)), // vi2 = 7 (caller-saved alloc)
|
||||
LowerOnly(VIADDIU_L(vi::vi3, vi::vi0, 9)), // vi3 = 9 (callee-saved alloc)
|
||||
LowerOnly(VISW_L(mask::x, vi::vi1, vi::vi0, 9)), // ISW => SYNC site while vi3 dirty
|
||||
EBitNopPair(),
|
||||
});
|
||||
h.Run();
|
||||
|
||||
EmuConfig.Gamefixes.XgKickHack = saved_hack;
|
||||
|
||||
EXPECT_EQ(h.GetViJit(vi::vi1), 5u);
|
||||
EXPECT_EQ(h.GetViJit(vi::vi2), 7u);
|
||||
EXPECT_EQ(h.GetViJit(vi::vi3), 9u)
|
||||
<< "VI write lost across an XgKickHack mVU_XGKICK_SYNC site: the "
|
||||
"callee-saved spill was emitted inside the sync's runtime "
|
||||
"conditionals (Crash Twinsanity vi12/vi15 wedge)";
|
||||
}
|
||||
|
||||
TEST(Vu1Xgkick, EopOnlyTagEmitsMatchingPath1Stream)
|
||||
{
|
||||
VuTestHarness h(1);
|
||||
|
||||
Reference in New Issue
Block a user