mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
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:
co-authored by
Claude Fable 5
parent
7eab2c4eff
commit
3bc64ac11a
@@ -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_]));
|
||||
}
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user