diff --git a/pcsx2/arm64/iCOP2-arm64.cpp b/pcsx2/arm64/iCOP2-arm64.cpp index 5881aee99f..15065b129a 100644 --- a/pcsx2/arm64/iCOP2-arm64.cpp +++ b/pcsx2/arm64/iCOP2-arm64.cpp @@ -792,7 +792,7 @@ void recCOP2_QMTC2() else { armAsm->Ldr(RQSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rt_])); - armMergeEEPinIntoQuad(RQSCRATCH, _Rt_); // lazy-dirty: stale lower half + armMergeEEResidentIntoQuad(RQSCRATCH, _Rt_); // lazy-dirty / residency merge } armAsm->Str(RQSCRATCH, armVU0Mem(&VU0.VF[_Rd_])); } diff --git a/pcsx2/arm64/iCore-arm64.cpp b/pcsx2/arm64/iCore-arm64.cpp index 120b72c726..5ad2cb8932 100644 --- a/pcsx2/arm64/iCore-arm64.cpp +++ b/pcsx2/arm64/iCore-arm64.cpp @@ -954,6 +954,36 @@ int _allocGPRtoNEONreg(int gprreg, int mode) return neonreg; } +// GE-M2 residency merge for a RAW quad load from cpuRegs memory (the SQ / +// QMFC2 / MMI memory-path loads that build a 128-bit value in a scratch NEON +// register rather than allocating one via _allocGPRtoNEONreg). The canonical +// lower 64 bits may be stale relative to a dirty pin mirror OR a dirty scalar +// ARM64TYPE_GPR slot (a resident lower-64 write the flip has not flushed yet); +// Ins the newest lower 64 into lane 0. Pin and scalar slot are mutually +// exclusive (invariant I1), so at most one branch fires. The upper 64 bits are +// never mirrored, so memory is always current for them. Side-effect-free: it +// does not bump the allocator LRU or touch `needed`, so a following consumer of +// the scalar slot is unaffected. Superset of armMergeEEPinIntoQuad — replaces it +// at the raw quad-load sites; the pin-only variant stays where a scalar slot is +// structurally impossible (inside _allocGPRtoNEONreg's no-scalar branch). +void armMergeEEResidentIntoQuad(const vixl::aarch64::VRegister& q, int gpr) +{ + if (const vixl::aarch64::Register* pin = armEEPinForGPR(gpr)) + { + armAsm->Ins(q.V2D(), 0, *pin); + return; + } + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + if (arm64gprs[i].inuse && arm64gprs[i].type == ARM64TYPE_GPR && + arm64gprs[i].reg == gpr && (arm64gprs[i].mode & MODE_WRITE)) + { + armAsm->Ins(q.V2D(), 0, armXRegister(i)); + return; + } + } +} + int _allocFPACCtoNEONreg(int mode) { for (int i = 0; i < NUM_ARM_NEON_REGS; i++) diff --git a/pcsx2/arm64/iMMI-arm64.cpp b/pcsx2/arm64/iMMI-arm64.cpp index 1f404db8df..95254c0ca7 100644 --- a/pcsx2/arm64/iMMI-arm64.cpp +++ b/pcsx2/arm64/iMMI-arm64.cpp @@ -57,8 +57,9 @@ static void mmiLoadReg(const a64::VRegister& qreg, int gpr) else { armAsm->Ldr(qreg, armCpuRegMem(&cpuRegs.GPR.r[gpr].UQ)); - // Lazy-dirty: merge the pin over the possibly-stale lower half. - armMergeEEPinIntoQuad(qreg, gpr); + // Lazy-dirty / residency: merge a dirty pin OR scalar slot over the + // possibly-stale lower half. + armMergeEEResidentIntoQuad(qreg, gpr); } } diff --git a/pcsx2/arm64/iR5900-arm64.h b/pcsx2/arm64/iR5900-arm64.h index 0b09a8c57e..1ad088c519 100644 --- a/pcsx2/arm64/iR5900-arm64.h +++ b/pcsx2/arm64/iR5900-arm64.h @@ -381,6 +381,12 @@ static __fi void armMergeEEPinIntoQuad(const vixl::aarch64::VRegister& q, int gp armAsm->Ins(q.V2D(), 0, *pin); } +// GE-M2 superset of armMergeEEPinIntoQuad: also merges a dirty scalar +// ARM64TYPE_GPR slot (a resident lower-64 write not yet flushed) into lane 0. +// Used by the raw SQ/QMFC2/MMI quad-load sites so they stay coherent once +// scalar residency is flipped on. Defined in iCore-arm64.cpp. +void armMergeEEResidentIntoQuad(const vixl::aarch64::VRegister& q, int gpr); + static __fi void armLoadEERegPtrRaw(const vixl::aarch64::CPURegister& reg, const void* field) { // Pinned guest GPR: serve the read from the mirror register. Write-through diff --git a/pcsx2/arm64/recVTLB-arm64.cpp b/pcsx2/arm64/recVTLB-arm64.cpp index d8779f79c1..2ce4d02bbd 100644 --- a/pcsx2/arm64/recVTLB-arm64.cpp +++ b/pcsx2/arm64/recVTLB-arm64.cpp @@ -965,8 +965,10 @@ void recSQ() // not written back by FLUSH_CONSTANT_REGS — relies on allocator // preferring NEON for full-128-bit guest GPRs. iFlushCall(FLUSH_CONSTANT_REGS); - armLoadEERegPtr(a64::q0, &cpuRegs.GPR.r[_Rt_].UQ); - armMergeEEPinIntoQuad(a64::q0, _Rt_); // lazy-dirty: stale lower half + // Raw quad load (bypasses the scalar tripwire): the lower half is + // intentionally stale-then-merged below for a dirty pin OR scalar slot. + armLoadEERegPtrRaw(a64::q0, &cpuRegs.GPR.r[_Rt_].UQ); + armMergeEEResidentIntoQuad(a64::q0, _Rt_); // lazy-dirty / residency merge armLoadEERegPtr(a64::w9, &cpuRegs.GPR.r[_Rs_].UL[0]); if (_Imm_ != 0) diff --git a/tests/ctest/core/recompilers/ee_rec_gem2_coherence_tests.cpp b/tests/ctest/core/recompilers/ee_rec_gem2_coherence_tests.cpp index 58d502fee0..9755e9db3e 100644 --- a/tests/ctest/core/recompilers/ee_rec_gem2_coherence_tests.cpp +++ b/tests/ctest/core/recompilers/ee_rec_gem2_coherence_tests.cpp @@ -145,3 +145,27 @@ TEST(EeRecGeM2Coherence, PoolPoisonWideBandSurvivesMixedOps) EXPECT_EQ(h.GetGpr64Interp(reg::t0), 0x0101010101010101ull); EXPECT_EQ(h.GetGpr64Interp(reg::t4), static_cast(kSentinelV0)); } + +// SQ reads a full 128-bit guest GPR through the raw quad-load + residency-merge +// path (recVTLB SQ, which only iFlushCall(FLUSH_CONSTANT_REGS) — it does NOT +// write back scalar allocator slots). Under the flip, the lower 64 bits live +// dirty in a scalar slot produced by the preceding ADDU; armMergeEEResidentIntoQuad +// must Ins that over the stale memory lower half. The upper 64 come from memory. +TEST(EeRecGeM2Coherence, SqAfterScalarWriteMergesDirtyLowerHalf) +{ + EeRecTestHarness h; + constexpr u32 kAddr = RecompilerTestEnvironment::kScratchAddr; + h.SetGpr128(reg::s2, 0x3333333344444444ull, 0x1111111122222222ull); // lo, hi + h.SetGpr64(reg::t0, 0x00000000AAAA0000ull); + h.SetGpr64(reg::t1, 0x000000000000BBBBull); + h.SetGpr64(reg::t3, kAddr); + h.TrackMemWindow(kAddr, 16); + h.LoadProgram({ + ADDU(reg::s2, reg::t0, reg::t1), // s2.lo = sext32(0xAAAABBBB); s2.hi preserved + SQ(reg::s2, 0, reg::t3), // store the full 128 bits of s2 + }); + h.Run(); + EXPECT_EQ(h.GetGpr64Interp(reg::s2), 0xFFFFFFFFAAAABBBBull); + EXPECT_EQ(h.ReadU64(kAddr + 0), 0xFFFFFFFFAAAABBBBull); // lower half (dirty-merged) + EXPECT_EQ(h.ReadU64(kAddr + 8), 0x1111111122222222ull); // upper half (from memory) +}