GE-M2e: merge dirty scalar slots into raw quad loads

The raw quad-load sites (SQ / QMFC2-QMTC2 / MMI memory-path) load a 128-bit
guest GPR from cpuRegs memory and fix up a stale lower half from a dirty pin
via armMergeEEPinIntoQuad. Once scalar residency is flipped on, that lower half
can also live dirty in an ARM64TYPE_GPR slot the site never flushed (recVTLB SQ
only iFlushCall(FLUSH_CONSTANT_REGS), which leaves scalar slots resident).

Add armMergeEEResidentIntoQuad — a side-effect-free superset of the pin merge
that also Ins-es a dirty scalar slot's lower 64 into lane 0 (pin and scalar
slot are mutually exclusive by I1). Route the three emitter quad-load sites
through it. recVTLB SQ's quad load switches to armLoadEERegPtrRaw so the
intentionally-stale-then-merged load doesn't trip the I3 tripwire.

(_allocGPRtoNEONreg already displaces a dirty scalar slot on its fill path;
this closes the raw-load gap.) New SqAfterScalarWriteMergesDirtyLowerHalf test
pins the behavior; 1279/1279 recompiler_tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Brian Degenhardt
2026-07-14 22:47:45 -07:00
co-authored by Claude Fable 5
parent 7eab2c4eff
commit 3bc64ac11a
6 changed files with 68 additions and 5 deletions
+1 -1
View File
@@ -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_]));
}
+30
View File
@@ -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++)
+3 -2
View File
@@ -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);
}
}
+6
View File
@@ -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
+4 -2
View File
@@ -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)
@@ -145,3 +145,27 @@ TEST(EeRecGeM2Coherence, PoolPoisonWideBandSurvivesMixedOps)
EXPECT_EQ(h.GetGpr64Interp(reg::t0), 0x0101010101010101ull);
EXPECT_EQ(h.GetGpr64Interp(reg::t4), static_cast<u64>(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)
}