Skip to content

Register gather-pipe exception checks against the compiling core - #15

Open
dougchansan wants to merge 1 commit into
ExpansionPak:moderngekko-vendorfrom
dougchansan:fix/exception-check-target-vendor
Open

Register gather-pipe exception checks against the compiling core#15
dougchansan wants to merge 1 commit into
ExpansionPak:moderngekko-vendorfrom
dougchansan:fix/exception-check-target-vendor

Conversation

@dougchansan

Copy link
Copy Markdown

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::CompileExceptionCheck records the faulting PC and invalidates the
block 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_jit is StaticRecompCore, which executes prebuilt
code and keeps m_fallback_jit — a real Jit64/JitArm64 — for everything the
module does not cover. When the hook fires from a fallback-compiled block, the
registration lands in StaticRecompCore::js and its EmptyBlockCache. The fallback
JIT reads neither: it consults its own js when compiling, and its own block
cache 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. JitBase answers
this, so nothing changes when Jit64/JitArm64 drive execution directly;
StaticRecompCore answers with its fallback.

The same reasoning applies to the PairedQuantize and SpeculativeConstants
paths, which register through the same function.

Evidence

Profile of the wedged process (perf record -F 999, 20s, all threads):

before after
JitInterface::CompileExceptionCheck 38.17%
JitInterface::CompileExceptionCheckFromJIT 19.16%
StaticRecompCore::UsesCompiledExceptionChecks¹ 4.84%
total in a hook that is supposed to retire itself 62% absent

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:

pair with fix without Δ
1 18.91 19.74 −0.83
2 19.43 19.78 −0.35
3 19.45 19.79 −0.34
4 19.48 19.82 −0.34
mean 19.32 19.78 −2.3%

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 UsesCompiledExceptionChecks line comes from a local modification on the test
machine, 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

  • Colosseum: boots to gameplay, single core and dual core, fallback=0
  • Luigi's Mansion: unaffected functionally — reaches the foyer in 72s, frames
    advancing, fallback=0; costs 2.3% as measured above
  • Both titles run from the DOL the runtime loads, hash-verified against
    sys/main.dol

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant