From 62961b549b9d3f0137f8acfbe362a678b636ae8b Mon Sep 17 00:00:00 2001 From: Douglas Whittingham Date: Thu, 27 Aug 2026 04:48:41 -1000 Subject: [PATCH] Make the instruction after an MSR write a region leader emitStateWrite side-exits to guest_pc+4 when mtmsr takes MSR[EE] from 0 to 1, so the dispatcher can deliver a pending external interrupt there. That address was not an entry-switch case, so the dispatch missed, fell into emitColdEntry, and the runtime interpreted from there until it reached the next leader. On Pokemon Colosseum OSRestoreInterrupts runs roughly every 139 guest cycles and its tail sits exactly in that gap: 140M interpreted instructions, 92% of all dispatches. Marking the following instruction a leader makes the exit land natively instead. Keyed on an MSR write rather than on MAY_EXIT generally. MAY_EXIT is carried by a large number of instructions -- the broad rule fires 160,525 times on Colosseum -- and every extra leader fragments a region and costs optimisation. Measured, the broad rule cost plain llvm 5-16% on the three titles that never needed it. Cross-title A/B against the unmodified backend, mean of per-scene ratios, 3 pairs per scene with forward and reversed blocks: Pokemon Colosseum 1.9506 (1 scene) Luigi's Mansion 1.0056 (2 scenes) Mario Kart Double Dash 0.9986 (6 scenes) Skyward Sword 1.0000 (2 scenes) This depends on the runtime delivering pending external interrupts at the post-mtmsr boundary. Without that half, the interpreter detour this removes was the only thing delivering the interrupt, and Colosseum advances 19 frames in 20s instead of 1069. --- src/backend/llvm/register_state.cpp | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/backend/llvm/register_state.cpp b/src/backend/llvm/register_state.cpp index 1a9bd5d..f76697c 100644 --- a/src/backend/llvm/register_state.cpp +++ b/src/backend/llvm/register_state.cpp @@ -1,4 +1,5 @@ #include "backend/llvm/emitter.h" + #include "cpu/cpu.h" #include @@ -138,6 +139,43 @@ void FunctionEmitter::scanRegionLeaders() { region_leaders_[i] = true; if (i + 1u < source_.block_count && term.kind != DOLIR_TERM_FALLTHROUGH) region_leaders_[i + 1u] = true; + // A block that writes MSR can return to the dispatcher and resume at the + // NEXT instruction, so that instruction has to be enterable. Terminators + // are already covered above; this catches the fallthrough case, which is + // mtmsr. + // + // Keyed on an MSR write rather than on MAY_EXIT generally. MAY_EXIT is + // carried by a large number of instructions -- the broader rule fired + // 160,525 times on Colosseum -- and every extra leader fragments a region, + // which costs optimisation. Measured: the broad rule cost plain llvm + // 5-16% on the three titles that never needed it, while only a handful of + // addresses are ever actually re-entered. + // + // Without this, re-entry lands on a non-leader address, the entry dispatch + // has no case for it, and the runtime interprets from there until it + // reaches the next leader. Colosseum calls OSRestoreInterrupts roughly + // every 139 guest cycles, and its last two instructions sit exactly in that + // gap: 166M interpreted instructions, 92% of all dispatches in that arm. + // The C backend never pays this because it labels every address. + // + // REQUIRES the runtime to deliver pending external interrupts at the + // post-mtmsr boundary. emitStateWrite side-exits to guest_pc+4 when MSR[EE] + // goes 0->1; before this rule the address was not enterable, so the runtime + // interpreted from there and delivered the interrupt as a side effect of + // that detour. Removing the detour without the runtime half leaves the + // guest spinning with EE set on an interrupt that never arrives -- Colosseum + // advances 19 frames in 20s instead of 1069. native_exc is the tell: 312 + // while hung, 25,009 once delivery is restored. + if (i + 1u < source_.block_count) { + const DolIRBlock &body = source_.blocks[i]; + for (u32 n = 0; n < body.instruction_count; n++) { + if (dolir_state_mask_test(body.instructions[n].state_defs, + DOLIR_STATE_MSR)) { + region_leaders_[i + 1u] = true; + break; + } + } + } u32 count = term.kind == DOLIR_TERM_COND_BRANCH ? 2u : term.kind == DOLIR_TERM_BRANCH ? 1u : term.kind == DOLIR_TERM_INDIRECT ? 2u