mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
EE-SRA 2 WS-B1: lazy-dirty pin seam machinery (inert; EE_PIN_LAZY_DIRTY=0)
Groundwork for pins-canonical-between-seams mode, all behind the compile-time EE_PIN_LAZY_DIRTY flag (default 0 = write-through, byte-identical emission): - armFlushEEGPRPins / armFlushEEClobberedPins: flush-ALL / flush-caller-saved pin mirrors to canonical memory. No dirty tracking — the lrps2 lesson says compile-time dirty subsets are unsound; seams flush everything. - iFlushCall: FLUSH_ALL_X86-carrying modes (interpreter fallbacks, branch tails) flush all pins; lighter modes don't (their inline-fastmem hit paths make no call) — the caller-saved flush is instead emitted at every actual call site that pairs with armReloadEEClobberedPins (the reload reads canonical memory; unflushed it would resurrect stale values over dirty caller-saved pins). 20 sites paired across the rec + backpatch thunks. - Dispatcher stubs: JITCompile / DispatcherEvent flush all pins before recRecompile / recEventTest (compile-time hooks and savestate/VM-exit read guest GPR memory); block-discard / page-reset flush the caller-saved set. - mVU macro-mode COP2 bridge gains the matching flush-before (armEmitEEClobberedPinFlushForCOP2) around waitMTVU. - iCOP2 vu0Sync sequences: flush hoisted before the FIRST call of each sequence, and the previously brace-less if/else bodies are braced — the flush must not run after the pins are already clobbered. recompiler_tests 1112/1112 with the flag off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
fce6d7985f
commit
39ce31186d
@@ -145,10 +145,12 @@ void armBeginStackFrame(bool save_fpr);
|
||||
void armEndStackFrame(bool save_fpr);
|
||||
bool armIsCalleeSavedRegister(int reg);
|
||||
|
||||
// Emits the EE JIT's caller-saved pin reload (see kEEPinTable in
|
||||
// iR5900-arm64.h). Out-of-line bridge for emission contexts that can't
|
||||
// include the EE rec header — currently only mVU macro-mode emit bodies
|
||||
// that emit C calls inline into EE blocks (mVUaddrFix's waitMTVU).
|
||||
// Emits the EE JIT's caller-saved pin flush-before / reload-after (see
|
||||
// kEEPinTable in iR5900-arm64.h). Out-of-line bridges for emission contexts
|
||||
// that can't include the EE rec header — currently only mVU macro-mode emit
|
||||
// bodies that emit C calls inline into EE blocks (mVUaddrFix's waitMTVU).
|
||||
// The flush is a lazy-dirty-mode no-op (EE_PIN_LAZY_DIRTY).
|
||||
void armEmitEEClobberedPinFlushForCOP2();
|
||||
void armEmitEEClobberedPinReloadForCOP2();
|
||||
|
||||
vixl::aarch64::MemOperand armOffsetMemOperand(const vixl::aarch64::MemOperand& op, s64 offset);
|
||||
|
||||
@@ -140,10 +140,15 @@ void vtlb_DynBackpatchLoadStore(uptr code_address, u32 code_size, u32 guest_pc,
|
||||
// events and cause cascading mid-block timing bugs. Matches the
|
||||
// pattern at recVTLB-arm64.cpp:112+120.
|
||||
armFlushCycleDelta();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
if (is_load)
|
||||
{
|
||||
armEmitCall((void*)vtlb_memRead128);
|
||||
}
|
||||
else
|
||||
{
|
||||
armEmitCall((void*)vtlb_memWrite128);
|
||||
}
|
||||
armReloadCycleDelta();
|
||||
// preserve_most spares x9-x15 but never x0-x8 — restore the rung-3
|
||||
// x4-x7 pins the call clobbered (pins are not allocator state, so
|
||||
@@ -200,6 +205,7 @@ void vtlb_DynBackpatchLoadStore(uptr code_address, u32 code_size, u32 guest_pc,
|
||||
armAsm->Mov(a64::w0, a64::w9);
|
||||
// Spill/reload RECCYCLE — see 128-bit slow_path above for rationale.
|
||||
armFlushCycleDelta();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
switch (size_in_bits)
|
||||
{
|
||||
case 8: armEmitCall((void*)vtlb_memRead<mem8_t>); break;
|
||||
@@ -293,6 +299,7 @@ void vtlb_DynBackpatchLoadStore(uptr code_address, u32 code_size, u32 guest_pc,
|
||||
|
||||
// Spill/reload RECCYCLE — see 128-bit slow_path above for rationale.
|
||||
armFlushCycleDelta();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
switch (size_in_bits)
|
||||
{
|
||||
case 8: armEmitCall((void*)vtlb_memWrite<mem8_t>); break;
|
||||
|
||||
@@ -325,6 +325,7 @@ void recMTC0()
|
||||
case 25: // Performance counters
|
||||
iFlushCall(FLUSH_INTERPRETER);
|
||||
emitFlushBlockCycles();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)Interp::MTC0);
|
||||
emitReloadCycle();
|
||||
// MTC0 writes no guest GPRs; restore the caller-saved pins the
|
||||
|
||||
@@ -649,9 +649,16 @@ void cop2EmitConditionalSync(bool interlock, void (*finishFunc)())
|
||||
// reschedule nextEventCycle.
|
||||
armFlushCycleDelta();
|
||||
|
||||
// Lazy-dirty seam: flush BEFORE the first call of the sequence
|
||||
// (the pins are clobbered from then on; pairs with the reload
|
||||
// below). The iFlushCall above lacks FLUSH_ALL_X86, so the
|
||||
// central hook didn't cover this.
|
||||
armFlushEEClobberedPins();
|
||||
armEmitCall((void*)vu0Sync);
|
||||
if (finishFunc)
|
||||
{
|
||||
armEmitCall((void*)finishFunc);
|
||||
}
|
||||
|
||||
armReloadCycleDelta();
|
||||
// The sync callees write VU state, not EE GPRs; restore the
|
||||
@@ -687,13 +694,20 @@ void cop2EmitConditionalSync(bool interlock, void (*finishFunc)())
|
||||
// Flush + re-derive the cycle delta around the C call (see comment above).
|
||||
armFlushCycleDelta();
|
||||
|
||||
// Lazy-dirty seam: flush before whichever call is emitted (pairs with the
|
||||
// reload below; the lighter iFlushCall above didn't cover this).
|
||||
armFlushEEClobberedPins();
|
||||
if (needsSync)
|
||||
{
|
||||
// Non-interlocked catch-up: run a 16-cycle minimum to amortize the mVU
|
||||
// dispatch envelope over small blocks (6dc5087cb). If the block also
|
||||
// contains an interlocked op, fall back to the exact sync.
|
||||
armEmitCall((void*)(s_nBlockInterlocked ? vu0Sync : vu0SyncRunAhead));
|
||||
}
|
||||
else
|
||||
{
|
||||
armEmitCall((void*)_vu0FinishMicro);
|
||||
}
|
||||
|
||||
armReloadCycleDelta();
|
||||
// See the interlock branch above — same caller-saved pin restore, same
|
||||
|
||||
@@ -293,6 +293,7 @@ static void recFPUCall(void (*func)())
|
||||
}
|
||||
}
|
||||
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)func);
|
||||
// FPU interpreter fallbacks touch fpuRegs only, never cpuRegs.GPR; restore
|
||||
// the caller-saved pins the C call clobbered — the block continues.
|
||||
|
||||
@@ -423,6 +423,7 @@ static const void* _DynGen_JITCompile()
|
||||
// convention is "every C-call boundary syncs RECCYCLE both ways".
|
||||
armFlushCycleDelta();
|
||||
|
||||
armFlushEEGPRPins(); // lazy-dirty seam: compile-time hooks read guest GPRs
|
||||
armAsm->Ldr(RWARG1, armCpuRegMem(&cpuRegs.pc));
|
||||
armEmitCall((void*)recRecompile);
|
||||
|
||||
@@ -444,6 +445,7 @@ static const void* _DynGen_DispatcherEvent()
|
||||
// modifies cpuRegs.cycle (e.g. fast-forwarding to nextEventCycle) and
|
||||
// reschedules nextEventCycle itself.
|
||||
armFlushCycleDelta();
|
||||
armFlushEEGPRPins(); // lazy-dirty seam: savestate save / VM exit read GPR memory
|
||||
armEmitCall((void*)recEventTest);
|
||||
armReloadCycleDelta();
|
||||
// Event processing can rewrite every guest GPR (savestate load on the EE
|
||||
@@ -511,6 +513,7 @@ static const void* _DynGen_EnterRecompiledCode()
|
||||
static const void* _DynGen_DispatchBlockDiscard()
|
||||
{
|
||||
u8* retval = armGetCurrentCodePointer();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)dyna_block_discard);
|
||||
// DispatcherReg's cache-hit path reloads nothing — restore the
|
||||
// caller-saved pins the C call clobbered before re-entering blocks.
|
||||
@@ -522,6 +525,7 @@ static const void* _DynGen_DispatchBlockDiscard()
|
||||
static const void* _DynGen_DispatchPageReset()
|
||||
{
|
||||
u8* retval = armGetCurrentCodePointer();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)dyna_page_reset);
|
||||
// See _DynGen_DispatchBlockDiscard.
|
||||
armReloadEEClobberedPins();
|
||||
@@ -631,6 +635,17 @@ void iFlushCall(int flushtype)
|
||||
g_cpuFlushedCode = true;
|
||||
}
|
||||
|
||||
// Lazy-dirty pin seam (EE-SRA 2 WS-B; no-op in write-through mode).
|
||||
// GPR-reading callees (interpreter fallbacks, branch tails, event paths —
|
||||
// the modes carrying FLUSH_ALL_X86) need every pin canonical in memory.
|
||||
// Lighter modes (FLUSH_CONSTANT_REGS before inline fastmem) do NOT flush
|
||||
// here — their hit paths make no call; the caller-saved flush is emitted
|
||||
// at the actual call sites instead (paired with armReloadEEClobberedPins).
|
||||
// Ordered after _flushConstRegs: a const-tracked pinned reg materializes
|
||||
// through the pin first.
|
||||
if (flushtype & FLUSH_ALL_X86)
|
||||
armFlushEEGPRPins();
|
||||
|
||||
#if 0
|
||||
// Disabled in x86 too (iR5900.cpp:1235-1242). Left here #if 0'd so the
|
||||
// FLUSH_CAUSE / g_maySignalException mechanism stays a faithful mirror of
|
||||
@@ -691,8 +706,14 @@ static u32 interpCallFlushPcBias()
|
||||
return g_recompilingDelaySlot ? 4 : 0;
|
||||
}
|
||||
|
||||
// Bridge for mVU macro-mode emit bodies (can't see kEEPinTable): emit the
|
||||
// caller-saved pin reload after a C call emitted inline into an EE block.
|
||||
// Bridges for mVU macro-mode emit bodies (can't see kEEPinTable): emit the
|
||||
// caller-saved pin flush-before / reload-after around a C call emitted inline
|
||||
// into an EE block. The flush is a lazy-dirty-mode no-op in write-through.
|
||||
void armEmitEEClobberedPinFlushForCOP2()
|
||||
{
|
||||
armFlushEEClobberedPins();
|
||||
}
|
||||
|
||||
void armEmitEEClobberedPinReloadForCOP2()
|
||||
{
|
||||
armReloadEEClobberedPins();
|
||||
@@ -991,6 +1012,7 @@ void SetBranchReg()
|
||||
// target and receives the translated paddr.
|
||||
if (EmuConfig.Gamefixes.GoemonTlbHack)
|
||||
{
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)vtlb_V2P);
|
||||
// vtlb_V2P writes no guest GPRs but clobbers the caller-saved pins;
|
||||
// the DispatcherReg jump below can cache-hit straight into a block.
|
||||
@@ -1293,6 +1315,7 @@ void recompileNextInstruction(bool delayslot, bool swapped_delay_slot)
|
||||
// Step 2: Emit call to snapshot pre-instruction state
|
||||
armAsm->Mov(a64::w0, cpuRegs.code);
|
||||
armAsm->Mov(a64::w1, pc - 4); // current instruction PC
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)verifySnapshotPre);
|
||||
// The hook is GPR-read-only but clobbers the caller-saved pins,
|
||||
// and the native codegen emitted next reads guest state through
|
||||
@@ -1308,6 +1331,7 @@ void recompileNextInstruction(bool delayslot, bool swapped_delay_slot)
|
||||
// Step 5: Emit call to verify against interpreter
|
||||
armAsm->Mov(a64::w0, cpuRegs.code);
|
||||
armAsm->Mov(a64::w1, pc - 4);
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)verifyCheckPost);
|
||||
armReloadEEClobberedPins(); // see verifySnapshotPre above
|
||||
}
|
||||
@@ -2262,6 +2286,7 @@ static void recRecompile(const u32 startpc)
|
||||
{
|
||||
armFlushCycleDelta();
|
||||
armAsm->Mov(RWARG1, startpc);
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)ee_divtrace_jit_block_hook);
|
||||
// Read-only hook, but the C call clobbers the caller-saved pins and
|
||||
// the block body it precedes reads guest state through them.
|
||||
@@ -2333,6 +2358,7 @@ static void recRecompile(const u32 startpc)
|
||||
{
|
||||
// 0x33ad48 / 0x35060c are the return address of the function (0x356250)
|
||||
// that populates the TLB cache.
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)GoemonPreloadTlb);
|
||||
// TLB-cache population writes no guest GPRs; restore the
|
||||
// caller-saved pins the C call clobbered before the block body.
|
||||
@@ -2346,6 +2372,7 @@ static void recRecompile(const u32 startpc)
|
||||
// 0x3563b8 is the start of the function that invalidates a TLB-cache entry;
|
||||
// a0 holds the key. Guest state is memory-resident at the prologue.
|
||||
armAsm->Ldr(RWARG1, armCpuRegMem(&cpuRegs.GPR.n.a0.UL[0]));
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)GoemonUnloadTlb);
|
||||
armReloadEEClobberedPins(); // see GoemonPreloadTlb above
|
||||
}
|
||||
|
||||
@@ -247,6 +247,45 @@ static __fi void armReloadEEClobberedPins()
|
||||
}
|
||||
}
|
||||
|
||||
// EE-SRA 2 WS-B: lazy-dirty pin mode. 0 = write-through (memory canonical
|
||||
// after every guest write — the shipping default). 1 = pins canonical between
|
||||
// seams: guest writes to pinned GPRs skip the canonical store, and every seam
|
||||
// where C code (or a JIT-internal 128-bit/raw reader) could observe stale GPR
|
||||
// memory restores canonicity first via the flush helpers below. A/B by
|
||||
// building a second binary with -DEE_PIN_LAZY_DIRTY=1 (codegen_ab two-binary
|
||||
// flow); no runtime toggle — mixing modes across blocks is unsound.
|
||||
#ifndef EE_PIN_LAZY_DIRTY
|
||||
#define EE_PIN_LAZY_DIRTY 0
|
||||
#endif
|
||||
|
||||
// Write every pin mirror back to canonical memory. No dirty tracking — the
|
||||
// lrps2 lesson says compile-time dirty subsets are unsound for runtime-path-
|
||||
// dependent dirtiness, so seams flush ALL pins (9 Str to one hot line at
|
||||
// already-expensive boundaries). No-op in write-through mode.
|
||||
static __fi void armFlushEEGPRPins()
|
||||
{
|
||||
if (!EE_PIN_LAZY_DIRTY)
|
||||
return;
|
||||
for (const EEPinnedGPR& pin : kEEPinTable)
|
||||
armAsm->Str(pin.host, armCpuRegMem(&cpuRegs.GPR.r[pin.gpr].UD[0]));
|
||||
}
|
||||
|
||||
// Flush only the CALLER-saved pins. Required before any C call that is
|
||||
// followed by armReloadEEClobberedPins: the reload reads canonical memory,
|
||||
// which under lazy-dirty is stale until flushed — the pair would otherwise
|
||||
// silently LOSE the in-register writes. (Callee-saved pins ride through the
|
||||
// call in their registers, so they need neither.) No-op in write-through.
|
||||
static __fi void armFlushEEClobberedPins()
|
||||
{
|
||||
if (!EE_PIN_LAZY_DIRTY)
|
||||
return;
|
||||
for (const EEPinnedGPR& pin : kEEPinTable)
|
||||
{
|
||||
if (!armIsCalleeSavedRegister(static_cast<int>(pin.host.GetCode())))
|
||||
armAsm->Str(pin.host, armCpuRegMem(&cpuRegs.GPR.r[pin.gpr].UD[0]));
|
||||
}
|
||||
}
|
||||
|
||||
static __fi void armLoadEERegPtr(const vixl::aarch64::CPURegister& reg, const void* field)
|
||||
{
|
||||
// Pinned guest GPR: serve the read from the mirror register. The mirror
|
||||
|
||||
@@ -311,6 +311,7 @@ static void recVCallmsImpl(void (*func)())
|
||||
armAsm->Add(RECCYCLE, RECCYCLE, cycles);
|
||||
|
||||
armFlushCycleDelta();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)func);
|
||||
armReloadCycleDelta();
|
||||
// VCALLMS/VCALLMSR run VU0 micro (writes VU state, not EE GPRs); restore
|
||||
|
||||
@@ -331,11 +331,14 @@ __fi void mVUaddrFix(mV, const a64::Register& gprReg)
|
||||
// Accessing VU1 regs from VU0
|
||||
if (THREAD_VU1)
|
||||
{
|
||||
// Need to wait for VU1 thread
|
||||
armEmitCall((void*)mVU.waitMTVU);
|
||||
// COP2 macro mode emits this inline into an EE block where the
|
||||
// caller-saved EE pins (x12/x13) are live across the call; micro
|
||||
// mode runs behind a C boundary whose call site reloads instead.
|
||||
// Flush-before is a lazy-dirty no-op in write-through mode.
|
||||
if (mVU.cop2)
|
||||
armEmitEEClobberedPinFlushForCOP2();
|
||||
// Need to wait for VU1 thread
|
||||
armEmitCall((void*)mVU.waitMTVU);
|
||||
if (mVU.cop2)
|
||||
armEmitEEClobberedPinReloadForCOP2();
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ static void vtlbSoftmemRead(int addr_wreg, u32 bits, bool sign)
|
||||
// this the JIT's pinned x25 stays stale and block-end cycle compare
|
||||
// never trips on tight INTC polls.
|
||||
armFlushCycleDelta();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
switch (bits)
|
||||
{
|
||||
case 8: armEmitCall((void*)vtlb_memRead<mem8_t>); break;
|
||||
@@ -178,6 +179,7 @@ static void vtlbSoftmemWrite(int addr_wreg, int value_reg, u32 bits)
|
||||
// any cycle-mutating handler reachable from MMIO must keep the JIT's
|
||||
// pinned x25 coherent. See vtlbSoftmemRead for full rationale.
|
||||
armFlushCycleDelta();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
switch (bits)
|
||||
{
|
||||
case 8: armEmitCall((void*)vtlb_memWrite<mem8_t>); break;
|
||||
@@ -419,6 +421,7 @@ static bool recLoadConstPaddrMMIOShortcut(u32 bits, bool sign)
|
||||
// path, including page-0F INTC_STAT → IntCHackCheck which mutates
|
||||
// cpuRegs.cycle. See vtlbSoftmemRead for full rationale.
|
||||
armFlushCycleDelta();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall(vmv.assumeHandlerGetRaw(szidx, false));
|
||||
armReloadCycleDelta();
|
||||
// Raw registered handlers are plain AAPCS (not preserve_most like the
|
||||
@@ -596,6 +599,7 @@ static bool recStoreConstPaddrMMIOShortcut(u32 bits)
|
||||
|
||||
// RECCYCLE coherence — same rationale as recLoadConstPaddrMMIOShortcut.
|
||||
armFlushCycleDelta();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall(vmv.assumeHandlerGetRaw(szidx, true));
|
||||
armReloadCycleDelta();
|
||||
// Caller-saved pin restore — same rationale as the read shortcut.
|
||||
@@ -705,6 +709,7 @@ static void vtlbSoftmemRead128(int addr_wreg)
|
||||
armAsm->Mov(a64::w0, a64::w9);
|
||||
// See vtlbSoftmemRead for the RECCYCLE coherence rationale.
|
||||
armFlushCycleDelta();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)vtlb_memRead128);
|
||||
armReloadCycleDelta();
|
||||
// x4-x7 pin restore — see vtlbSoftmemRead (q0 result untouched).
|
||||
@@ -739,6 +744,7 @@ static void vtlbSoftmemWrite128(int addr_wreg)
|
||||
armAsm->Mov(a64::w0, a64::w9);
|
||||
// See vtlbSoftmemRead for the RECCYCLE coherence rationale.
|
||||
armFlushCycleDelta();
|
||||
armFlushEEClobberedPins(); // lazy-dirty seam: pairs with the reload below
|
||||
armEmitCall((void*)vtlb_memWrite128);
|
||||
armReloadCycleDelta();
|
||||
// x4-x7 pin restore — see vtlbSoftmemRead.
|
||||
|
||||
Reference in New Issue
Block a user