Register gather-pipe exception checks against the compiling core - #15
Open
dougchansan wants to merge 1 commit into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Pokémon Colosseum (GC6E01) boots its videos, then goes to a black screen and never
recovers. The emulator stays alive — the window keeps reporting ~38 FPS — but the
framebuffer never changes again and the process stops responding to SIGTERM.
Reproduced on a Raspberry Pi 4 (Cortex-A72, Debian) with three independently built
modules: a module built natively on the Pi, and C-backend and llvm-aot modules
cross-compiled on x86-64. All three behave identically, so this is not a codegen
problem. The same title on x86-64 boots through its menus into gameplay.
Root cause
JitInterface::CompileExceptionCheckrecords the faulting PC and invalidates theblock so it is recompiled with the check folded inline, after which the runtime hook
is no longer reached from that address. It records both against
m_jit.Under static recompilation
m_jitisStaticRecompCore, which executes prebuiltcode and keeps
m_fallback_jit— a realJit64/JitArm64— for everything themodule does not cover. When the hook fires from a fallback-compiled block, the
registration lands in
StaticRecompCore::jsand itsEmptyBlockCache. The fallbackJIT reads neither: it consults its own
jswhen compiling, and its own blockcache owns the block. The address is therefore never seen by the core that could act
on it, the block is never rebuilt, and the hook fires from the same address forever.
Because the gather-pipe check never gets compiled in, the FIFO write path never
completes, which is why the title hangs rather than merely running slowly.
Fix
Ask the core which JIT compiled the block, and register there.
JitBaseanswersthis, so nothing changes whenJit64/JitArm64drive execution directly;StaticRecompCoreanswers with its fallback.The same reasoning applies to the
PairedQuantizeandSpeculativeConstantspaths, which register through the same function.
Evidence
Profile of the wedged process (
perf record -F 999, 20s, all threads):JitInterface::CompileExceptionCheckJitInterface::CompileExceptionCheckFromJITStaticRecompCore::UsesCompiledExceptionChecks¹After the fix the profile is unremarkable: JIT code 60.8%, texture hashing, audio
mixing, DSP HLE.
Colosseum, same module and runner, single core: black screen from t=70s onward,
0 frame changes → boots through logos, menus, name entry and the intro cutscene into
gameplay. Dual core: 1.1–1.4 MB frames at 31–34 FPS, 7 of 7 sampled frames advancing.
Cost on a title that already worked. Luigi's Mansion (GLME01), mansion foyer,
dual core, four alternating pairs, 20 FPS samples per run, both runner binaries built
once and swapped between runs:
Four of four pairs favour the unfixed build, by more than the within-run standard
deviation (0.18–0.31). The regression is real and worth stating plainly: blocks now
carry the inline gather-pipe check that stock Dolphin always compiles in, where
before the check was silently skipped. That is the cost of the check being present at
all, paid by a title that happened to survive without it.
¹ The
UsesCompiledExceptionChecksline comes from a local modification on the testmachine, not from this repository — an early-return added to make the runaway hook
cheaper. It is not on any branch here, and this change does not depend on it. Upstream
reaches the same dead end by a different route, registering against the wrong core
rather than skipping registration.
Testing
fallback=0advancing,
fallback=0; costs 2.3% as measured abovesys/main.dol