From 54bc47685a7f51ae1df14da017eb50b80e3ad657 Mon Sep 17 00:00:00 2001 From: Douglas Whittingham Date: Sun, 23 Aug 2026 09:11:34 -1000 Subject: [PATCH 1/2] Invalidate the FP-availability cache when MSR is written emitFPAvailable checks MSR[FP] once and caches the answer for the rest of the region, so later floating-point instructions skip the check. That is sound only while MSR cannot change underneath it, and noteStateWrite does not clear the cache on an MSR write. So a region shaped like fadd f1, f2, f3 ; checks MSR[FP], caches "available" mtmsr r0 ; clears MSR[FP] fadd f4, f5, f6 ; skips the check, executes anyway runs the second fadd with floating point disabled and raises no FP-unavailable exception. The guest's lazy FPU context switching is built on receiving that exception, so a thread that should have had its FPU context saved silently does not. noteStateWrite already invalidates the related caches for FPR, PS1, FPSCR and HID2 writes; MSR was the missing case. The C backend does not have this problem because it emits a check per instruction rather than caching one per region. Found while investigating an unrelated title. It is rare in practice: on Pokemon Colosseum it changes exactly one of 4890 emitted objects (chunk_3686_text1_801CFBE0.o), because an mtmsr and a floating-point instruction have to land in the same region for it to bite. I do not have a title whose visible behaviour changes because of it, so this is offered as a correctness fix rather than a fix for a reported symptom. --- src/backend/llvm/register_state.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/backend/llvm/register_state.cpp b/src/backend/llvm/register_state.cpp index 8de0335..4c692b9 100644 --- a/src/backend/llvm/register_state.cpp +++ b/src/backend/llvm/register_state.cpp @@ -58,6 +58,15 @@ void FunctionEmitter::noteStateWrite(DolIRStateSlot slot, Value *value) { } else if (slot == DOLIR_STATE_HID2) { psq_direct_proven_ = false; psq_indexed_proven_ = false; + } else if (slot == DOLIR_STATE_MSR) { + // MSR[FP] decides whether a floating-point instruction traps, and + // emitFPAvailable caches that answer for the rest of the region. A + // write to MSR can clear FP, so the cached answer has to go. Without + // this an mtmsr that disables FP mid-region leaves the FP + // instructions after it running unchecked, and the guest never takes + // the FP-unavailable exception its lazy FPU context switching is + // built on. + fp_available_checked_ = false; } } From dd8c8d8760e36deaa606746044c3fa26172193d9 Mon Sep 17 00:00:00 2001 From: Douglas Whittingham Date: Sun, 23 Aug 2026 09:21:39 -1000 Subject: [PATCH 2/2] Invalidate emitter caches after a native call, not just the values The call-resume path reloads each used state slot from CPUState by hand but leaves the emitter's compile-time conclusions in place. Those conclusions are about state the callee is free to change: fp_available_checked_ whether MSR[FP] was already checked known_state_ which slots hold a known constant psq_direct_proven_ whether paired-single addressing was proven psq_indexed_proven_ the FP representation tracking known_state_ is the sharpest: stateValue() returns the cached constant in preference to loading memory, so if the emitter proved a slot held a constant before the call and the callee overwrites it, the caller keeps using the stale constant even though the reload put the correct value in memory. reloadUsedState() does the same per-slot reload and drops the caches, and the fallback resume path in control_flow.cpp already uses it for exactly this reason. Rare in practice, like the MSR case: on Pokemon Colosseum the two fixes together change one of 4890 emitted objects. Offered as a correctness fix rather than a fix for an observed symptom. --- src/backend/llvm/branch_targets.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/backend/llvm/branch_targets.cpp b/src/backend/llvm/branch_targets.cpp index 43ed05c..7b45280 100644 --- a/src/backend/llvm/branch_targets.cpp +++ b/src/backend/llvm/branch_targets.cpp @@ -89,12 +89,15 @@ BasicBlock *FunctionEmitter::externalDestination(const DolIRTerminator &term, builder_.CreateRetVoid(); } else { reloadCallCounters(); - for (u32 state = 0; state < DOLIR_STATE_COUNT; state++) { - if (!used_[state]) - continue; - auto stateSlot = static_cast(state); - builder_.CreateStore(loadContext(stateSlot), state_[state]); - } + // The callee can invalidate anything the emitter had concluded about + // guest state: whether MSR[FP] was already checked, which slots hold + // known constants, whether paired-single addressing was proven. + // Reloading the values without dropping those conclusions leaves + // stateValue() returning a constant the callee has since overwritten. + // The fallback resume path already uses reloadUsedState() for exactly + // this reason; this path reloaded values by hand and skipped the + // invalidation. + reloadUsedState(); builder_.CreateBr(blocks_[continuationBlock]); } builder_.restoreIP(saved);