mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
arm64: divert stale block-link sites to the dispatcher, not JITCompile
Remove() stamped a removed block's entry with B JITCompile, and Link() routed not-yet-compiled targets there too. JITCompile unconditionally recompiles from cpuRegs.pc — so when a block was removed and later recompiled at a new address, any orphaned link site still branching to the old entry re-entered recRecompile on an already-compiled block: fnptr assert on Devel, and on Release a spurious in-place recompile that superseded live code and stranded other orphaned callers in the stale pre-SMC compile (the NFS Carbon SLUS-21493 post-FMV deadlock, localized by twindiff against the pre-transplant build). Every link tail in both recs already stores the target guest pc before its branch, so the correct stale-site policy is re-DISPATCH, never re-COMPILE: divert to DispatcherReg, which routes through the recLUT (the single SMC-invalidation rewrite point) to the current code, or to compilation via the LUT's own JITCompile slot when genuinely uncompiled. Both AetherSX2's Remove() and the pre-transplant linker enforce this same fallback-to-dispatcher policy; the recRecompile fnptr assert becomes a true invariant again. Zero hot-path cost: the dispatcher hop is paid only by a cold first execution of an unresolved link (repatched direct by New()) or by a stale site after SMC churn. Remove() keeps its signal-safety contract (single atomic 4-byte B patch, no link-map access). recompiler_tests: 1415/1415. Carbon twincompare free-runs 200 frames where it previously aborted on the recRecompile assert.
This commit is contained in:
@@ -12,12 +12,24 @@
|
||||
// - Link()/New() touch the multimap, but they only run from the
|
||||
// compile path (single-threaded, never from a signal).
|
||||
// - Remove() does NOT walk the link map. Instead it overwrites the
|
||||
// first 4 bytes of each removed block with `B JITCompile`, so any
|
||||
// stale link still resolves correctly via the dispatcher (which
|
||||
// can re-patch the link to the freshly compiled target on its next
|
||||
// dispatch). Block memory isn't reclaimed until a full reset, so
|
||||
// this 4-byte rewrite always lands on memory the recompiler still
|
||||
// owns.
|
||||
// first 4 bytes of each removed block with `B DispatcherReg`, so any
|
||||
// stale link re-dispatches through the recLUT. Block memory isn't
|
||||
// reclaimed until a full reset, so this 4-byte rewrite always lands
|
||||
// on memory the recompiler still owns.
|
||||
//
|
||||
// Stale sites divert to the DISPATCHER, never to JITCompile. Every link
|
||||
// tail stores the target guest pc into cpuRegs.pc before its branch (both
|
||||
// recs, all forms — see SetBranchImm / psxSetBranchImm), so a diverted
|
||||
// site re-dispatches from a pc that is already correct; the recLUT (the
|
||||
// single SMC-invalidation rewrite point) then routes it to the current
|
||||
// code, or through JITCompile if the target is genuinely uncompiled.
|
||||
// Routing stale sites at JITCompile directly instead re-enters
|
||||
// recRecompile on an already-compiled block whenever the target has been
|
||||
// removed-and-recompiled — tripping the fnptr assert on Devel, and on
|
||||
// Release spuriously superseding live code in place, which strands
|
||||
// orphaned callers in the stale pre-SMC compile (the NFS Carbon
|
||||
// SLUS-21493 block-cache corruption). AetherSX2's Remove() and the
|
||||
// pre-transplant linker enforce the same fallback-to-dispatcher policy.
|
||||
//
|
||||
// The patch site for each link is the address of a single B instruction
|
||||
// emitted by SetBranchImm (see iR5900-arm64.cpp). Aligned 32-bit stores
|
||||
@@ -77,6 +89,16 @@ protected:
|
||||
BaseBlockArray blocks;
|
||||
linkmap_t links;
|
||||
uptr jitcompile = 0;
|
||||
uptr dispatcher = 0;
|
||||
|
||||
// Where a stale site (unresolved Link(), Remove() entry stamp) diverts.
|
||||
// The dispatcher when one is registered — re-dispatch from the pc the
|
||||
// site's own tail stored — with a JITCompile fallback for direct
|
||||
// instantiation (unit tests) that never registers one.
|
||||
__fi uptr StaleDispatchTarget() const
|
||||
{
|
||||
return dispatcher ? dispatcher : jitcompile;
|
||||
}
|
||||
|
||||
// The block currently being compiled, published by New(). Link() stamps
|
||||
// it onto every site it records.
|
||||
@@ -203,6 +225,11 @@ public:
|
||||
jitcompile = reinterpret_cast<uptr>(recompiler_);
|
||||
}
|
||||
|
||||
void SetDispatcher(const void* dispatcher_)
|
||||
{
|
||||
dispatcher = reinterpret_cast<uptr>(dispatcher_);
|
||||
}
|
||||
|
||||
// Register a link site that wants to branch directly to the block at
|
||||
// `pc`. Patches immediately if a block already exists; otherwise
|
||||
// records the site so New(pc, ...) can patch it later. `call` sites
|
||||
@@ -215,7 +242,7 @@ public:
|
||||
|
||||
BASEBLOCKEX* target = Get(pc);
|
||||
const uptr target_addr = (target && target->startpc == pc)
|
||||
? target->fnptr : jitcompile;
|
||||
? target->fnptr : StaleDispatchTarget();
|
||||
// No flush: the site is in the block being emitted right now, and
|
||||
// armEndBlock() range-flushes that buffer. See PatchWord.
|
||||
PatchWord(reinterpret_cast<uptr>(patch_site),
|
||||
@@ -337,12 +364,16 @@ public:
|
||||
}
|
||||
|
||||
// Signal-safe: writes a redirect stub at each removed block's entry
|
||||
// point so any stale link still resolves through JITCompile, then
|
||||
// erases from the flat sorted array. Does NOT touch the link map —
|
||||
// mutating an STL container here is not signal-safe. The entries this
|
||||
// strands are reaped by the next New() for their destination PC, which
|
||||
// sees the owner is gone; until then the redirect stub keeps any site
|
||||
// still pointing at this block correct.
|
||||
// point so any stale link re-dispatches through the recLUT (see the
|
||||
// stale-dispatch policy note in the file header — the diverting site's
|
||||
// tail already stored the correct pc, so DispatcherReg routes it to the
|
||||
// current code; JITCompile here would re-enter recRecompile on a
|
||||
// removed-and-recompiled block), then erases from the flat sorted
|
||||
// array. Does NOT touch the link map — mutating an STL container here
|
||||
// is not signal-safe. The entries this strands are reaped by the next
|
||||
// New() for their destination PC, which sees the owner is gone; until
|
||||
// then the redirect stub keeps any site still pointing at this block
|
||||
// correct.
|
||||
//
|
||||
// SL-1: a resident self-loop's back-edge is an internal B to the loop-top
|
||||
// (past the entry redirect), so it gets its own atomic repoint — to the
|
||||
@@ -358,7 +389,7 @@ public:
|
||||
for (int i = first; i <= last; ++i)
|
||||
{
|
||||
const uptr site = blocks[i].fnptr;
|
||||
PatchAtomic(site, EncodeB(site, jitcompile));
|
||||
PatchAtomic(site, EncodeB(site, StaleDispatchTarget()));
|
||||
|
||||
if (blocks[i].backedge_site)
|
||||
PatchAtomic(blocks[i].backedge_site,
|
||||
|
||||
@@ -211,10 +211,13 @@ static void _DynGen_Dispatchers()
|
||||
iopEnterRecompiledCode = _DynGen_EnterRecompiledCode();
|
||||
iopUnmappedRecLUTPage = _DynGen_UnmappedRecLUTPage();
|
||||
|
||||
// Block linker needs iopJITCompile so it can route stale / not-yet-
|
||||
// compiled link sites through the dispatcher path. Mirrors EE rec
|
||||
// at iR5900-arm64.cpp:674 and x86 IOP rec at iR3000A.cpp:257.
|
||||
// Block linker: stale / not-yet-compiled link sites divert to
|
||||
// iopDispatcherReg — re-dispatch from the pc the site's tail already
|
||||
// stored (psxSetBranchImm and the fallthrough tail both Str psxRegs.pc
|
||||
// before the link site), never a direct re-compile. See the
|
||||
// stale-dispatch policy in BaseblockEx-arm64.h; mirrors the EE rec.
|
||||
recBlocks.SetJITCompile(iopJITCompile);
|
||||
recBlocks.SetDispatcher(iopDispatcherReg);
|
||||
|
||||
Perf::any.Register(start, static_cast<u32>(armGetCurrentCodePointer() - start), "IOP Dispatcher");
|
||||
}
|
||||
|
||||
@@ -629,9 +629,13 @@ static void _DynGen_Dispatchers()
|
||||
DispatchPageReset = _DynGen_DispatchPageReset();
|
||||
UnmappedRecLUTPage = _DynGen_UnmappedRecLUTPage();
|
||||
|
||||
// Block linker needs JITCompile so it can route stale / not-yet-compiled
|
||||
// link sites through the dispatcher path.
|
||||
// Block linker: stale / not-yet-compiled link sites divert to
|
||||
// DispatcherReg — re-dispatch from the pc the site's tail already
|
||||
// stored, never a direct re-compile (see the stale-dispatch policy in
|
||||
// BaseblockEx-arm64.h). JITCompile is the direct-instantiation
|
||||
// fallback and stays registered for the unit tests' sake.
|
||||
recBlocks.SetJITCompile(JITCompile);
|
||||
recBlocks.SetDispatcher(DispatcherReg);
|
||||
|
||||
Perf::any.Register(start, static_cast<u32>(armGetCurrentCodePointer() - start), "EE Dispatcher");
|
||||
}
|
||||
|
||||
@@ -239,6 +239,108 @@ TEST(Arm64BaseBlocksLink, MapStaysBoundedUnderOwnerChurn)
|
||||
EXPECT_EQ(code[0], first_word);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stale-dispatch policy. Every link tail stores the target guest pc into
|
||||
// cpuRegs.pc before its branch, so a site that goes stale needs re-DISPATCH
|
||||
// (LUT lookup on the already-correct pc), never re-COMPILE. Routing stale
|
||||
// sites at JITCompile instead re-enters recRecompile on an already-compiled
|
||||
// block — the NFS Carbon (SLUS-21493) block-cache corruption: the spurious
|
||||
// recompile supersedes live code in place, leaving orphaned callers running
|
||||
// the stale pre-SMC compile.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(Arm64BaseBlocksLink, RemoveStampsRedirectToDispatcherNotRecompiler)
|
||||
{
|
||||
alignas(64) static u32 code[64];
|
||||
std::memset(code, 0, sizeof(code));
|
||||
|
||||
Arm64BaseBlocks bb;
|
||||
u32* const jitcompile = &code[0];
|
||||
u32* const dispatcher = &code[4];
|
||||
bb.SetJITCompile(jitcompile);
|
||||
bb.SetDispatcher(dispatcher);
|
||||
|
||||
constexpr u32 kPc = 0x3000;
|
||||
u32* const site = &code[8];
|
||||
u32* const entry1 = &code[16];
|
||||
|
||||
bb.Link(kPc, site);
|
||||
bb.New(kPc, reinterpret_cast<uptr>(entry1));
|
||||
|
||||
const int idx = bb.Index(kPc);
|
||||
ASSERT_GE(idx, 0);
|
||||
bb.Remove(idx, idx);
|
||||
|
||||
// The dead entry's redirect must target the dispatcher: a stale caller
|
||||
// landing here re-dispatches from the pc its own tail already stored.
|
||||
EXPECT_EQ(OpcodeBits(*entry1), kOpcB);
|
||||
EXPECT_EQ(DecodeImm26Bytes(*entry1),
|
||||
reinterpret_cast<intptr_t>(dispatcher) - reinterpret_cast<intptr_t>(entry1));
|
||||
|
||||
// Recompile at a new address: the old entry keeps diverting to the
|
||||
// dispatcher, which routes the stale caller into the new code via the
|
||||
// LUT — recRecompile is never re-entered on the compiled block.
|
||||
u32* const entry2 = &code[32];
|
||||
bb.New(kPc, reinterpret_cast<uptr>(entry2));
|
||||
EXPECT_EQ(DecodeImm26Bytes(*entry1),
|
||||
reinterpret_cast<intptr_t>(dispatcher) - reinterpret_cast<intptr_t>(entry1));
|
||||
EXPECT_EQ(bb.Get(kPc)->fnptr, reinterpret_cast<uptr>(entry2));
|
||||
}
|
||||
|
||||
TEST(Arm64BaseBlocksLink, UnresolvedLinkFallsBackToDispatcher)
|
||||
{
|
||||
alignas(64) static u32 code[64];
|
||||
std::memset(code, 0, sizeof(code));
|
||||
|
||||
Arm64BaseBlocks bb;
|
||||
u32* const jitcompile = &code[0];
|
||||
u32* const dispatcher = &code[4];
|
||||
bb.SetJITCompile(jitcompile);
|
||||
bb.SetDispatcher(dispatcher);
|
||||
|
||||
constexpr u32 kPc = 0x3000;
|
||||
u32* const site_b = &code[8];
|
||||
u32* const site_bl = &code[9];
|
||||
|
||||
// Not-yet-compiled target: both forms route through the dispatcher (one
|
||||
// extra hop on the cold first execution; the LUT slot still holds
|
||||
// JITCompile, so compilation is reached through it with a correct pc).
|
||||
bb.Link(kPc, site_b);
|
||||
bb.Link(kPc, site_bl, /*call=*/true);
|
||||
EXPECT_EQ(OpcodeBits(*site_b), kOpcB);
|
||||
EXPECT_EQ(OpcodeBits(*site_bl), kOpcBL);
|
||||
EXPECT_EQ(DecodeImm26Bytes(*site_b),
|
||||
reinterpret_cast<intptr_t>(dispatcher) - reinterpret_cast<intptr_t>(site_b));
|
||||
EXPECT_EQ(DecodeImm26Bytes(*site_bl),
|
||||
reinterpret_cast<intptr_t>(dispatcher) - reinterpret_cast<intptr_t>(site_bl));
|
||||
}
|
||||
|
||||
TEST(Arm64BaseBlocksLink, NoDispatcherRegisteredKeepsJITCompileFallback)
|
||||
{
|
||||
// Direct-instantiation compatibility: with no dispatcher registered the
|
||||
// class behaves exactly as before (fallback = JITCompile).
|
||||
alignas(64) static u32 code[64];
|
||||
std::memset(code, 0, sizeof(code));
|
||||
|
||||
Arm64BaseBlocks bb;
|
||||
bb.SetJITCompile(&code[0]);
|
||||
|
||||
constexpr u32 kPc = 0x3000;
|
||||
u32* const site = &code[8];
|
||||
u32* const entry = &code[16];
|
||||
|
||||
bb.Link(kPc, site);
|
||||
EXPECT_EQ(DecodeImm26Bytes(*site),
|
||||
reinterpret_cast<intptr_t>(&code[0]) - reinterpret_cast<intptr_t>(site));
|
||||
|
||||
bb.New(kPc, reinterpret_cast<uptr>(entry));
|
||||
const int idx = bb.Index(kPc);
|
||||
ASSERT_GE(idx, 0);
|
||||
bb.Remove(idx, idx);
|
||||
EXPECT_EQ(DecodeImm26Bytes(*entry),
|
||||
reinterpret_cast<intptr_t>(&code[0]) - reinterpret_cast<intptr_t>(entry));
|
||||
}
|
||||
|
||||
TEST(Arm64BaseBlocksLink, BlFormSurvivesPruning)
|
||||
{
|
||||
alignas(64) static u32 code[64];
|
||||
|
||||
Reference in New Issue
Block a user