From 3252cc23175db30ee53bc609bc02c2c4da2c5d4a Mon Sep 17 00:00:00 2001 From: Douglas Whittingham Date: Sat, 15 Aug 2026 03:40:32 -1000 Subject: [PATCH] Register gather-pipe exception checks against the compiling core CompileExceptionCheck records the faulting PC and invalidates the block so it gets recompiled with the check folded inline, after which the runtime hook is no longer reached from that address. It recorded both against m_jit. Under static recompilation m_jit is StaticRecompCore, which executes prebuilt code and keeps m_fallback_jit -- a real Jit64/JitArm64 -- for everything the module does not cover. When a hook fires from a fallback-compiled block, the registration lands in StaticRecompCore's js sets and its block cache. The fallback JIT reads neither: it consults its own js at compile time, and its own block cache owns the block. So the address was never seen by the core that could act on it, the block was never rebuilt, and the hook kept firing from the same address forever. Ask the core which JIT compiled the block and register there. JitBase answers "this", so nothing changes for Jit64 or JitArm64 driving execution directly; StaticRecompCore answers with its fallback. Found on Pokemon Colosseum (GC6E01), which goes black after its boot videos on a Raspberry Pi 4 and never recovers. Profiling the wedged process: 38.17% JitInterface::CompileExceptionCheck 19.16% JitInterface::CompileExceptionCheckFromJIT 4.84% StaticRecompCore::UsesCompiledExceptionChecks 62% of all CPU in a hook that is supposed to retire itself. With the registration routed to the fallback JIT the chain leaves the profile entirely, and the title boots through its menus into gameplay. Luigi's Mansion, which already worked, is unaffected. The same reasoning applies to the PairedQuantize and SpeculativeConstants paths, which register through the same function. --- Source/Core/Core/PowerPC/JitCommon/JitBase.h | 8 ++++++++ Source/Core/Core/PowerPC/JitInterface.cpp | 17 ++++++++++++----- .../PowerPC/StaticRecomp/StaticRecompCore.h | 11 +++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Source/Core/Core/PowerPC/JitCommon/JitBase.h b/Source/Core/Core/PowerPC/JitCommon/JitBase.h index 066fe40a0d..1e660452f4 100644 --- a/Source/Core/Core/PowerPC/JitCommon/JitBase.h +++ b/Source/Core/Core/PowerPC/JitCommon/JitBase.h @@ -237,6 +237,14 @@ class JitBase : public CPUCoreBase virtual const CommonAsmRoutinesBase* GetAsmRoutines() = 0; + // The core that compiled the block a runtime hook fired from, and therefore + // the one whose js sets and block cache a registration belongs in. That is + // normally this core. A core that mostly executes prebuilt code but keeps a + // JIT for whatever it does not cover must name that JIT instead: the JIT is + // what reads those sets at compile time, and the only thing that can retire + // the hook by recompiling the block with the check folded in. + virtual JitBase* GetExceptionCheckTarget() { return this; } + virtual bool WantsPageTableMappings() const; virtual bool HandleFault(uintptr_t access_address, SContext* ctx) = 0; diff --git a/Source/Core/Core/PowerPC/JitInterface.cpp b/Source/Core/Core/PowerPC/JitInterface.cpp index e7385fce17..d098c19a64 100644 --- a/Source/Core/Core/PowerPC/JitInterface.cpp +++ b/Source/Core/Core/PowerPC/JitInterface.cpp @@ -322,18 +322,24 @@ void JitInterface::CompileExceptionCheck(ExceptionType type) if (!m_jit) return; + // Register against the core that compiled the block this fired from. Under + // static recompilation that is the fallback JIT rather than the static core: + // the fallback is what reads these sets when it compiles, and what retires + // the call site by recompiling with the check folded inline. + JitBase* const target = m_jit->GetExceptionCheckTarget(); + std::unordered_set* exception_addresses = nullptr; switch (type) { case ExceptionType::FIFOWrite: - exception_addresses = &m_jit->js.fifoWriteAddresses; + exception_addresses = &target->js.fifoWriteAddresses; break; case ExceptionType::PairedQuantize: - exception_addresses = &m_jit->js.pairedQuantizeAddresses; + exception_addresses = &target->js.pairedQuantizeAddresses; break; case ExceptionType::SpeculativeConstants: - exception_addresses = &m_jit->js.noSpeculativeConstantsAddresses; + exception_addresses = &target->js.noSpeculativeConstantsAddresses; break; } @@ -355,8 +361,9 @@ void JitInterface::CompileExceptionCheck(ExceptionType type) exception_addresses->insert(ppc_state.pc); // Invalidate the JIT block so that it gets recompiled with the external exception check - // included. - m_jit->GetBlockCache()->InvalidateICache(ppc_state.pc, 4, true); + // included. Same core as the set above: invalidating a different core's + // cache leaves the block standing and the hook firing. + target->GetBlockCache()->InvalidateICache(ppc_state.pc, 4, true); } } diff --git a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore.h b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore.h index 44d027bef7..d084fea634 100644 --- a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore.h +++ b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore.h @@ -61,6 +61,17 @@ class StaticRecompCore : public JitBase bool HandleFault(uintptr_t access_address, SContext* ctx) override { return false; } JitBaseBlockCache* GetBlockCache() override { return &m_block_cache; } + + // Gather-pipe and quantize hooks do not only fire from module code: anything + // the module does not cover runs on m_fallback_jit, a real Jit64/JitArm64. + // That core reads fifoWriteAddresses at compile time and owns the block that + // has to be invalidated, so registering against this core instead left the + // check uncompiled and the hook firing on every gather-pipe store forever. + JitBase* GetExceptionCheckTarget() override + { + return m_fallback_jit ? m_fallback_jit.get() : this; + } + void EraseSingleBlock(const JitBlock& block) override {} std::vector GetMemoryStats() const override { return {}; } std::size_t DisassembleNearCode(const JitBlock& block, std::ostream& stream) const override