From 16b4d33cf63dae641377565be27c753bfadef234 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 3 May 2026 15:11:47 -0700 Subject: [PATCH] recompilation: emit MIPS link side-effect ($ra := PC+8) for jal/jalr/bltzal/bgezal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the engine relied on callees to save/restore $ra on their own stack frame, leaving ctx->r31 stale across linking instructions. This worked for normal code that only uses $ra for `jr $ra` returns (which the engine maps to a C `return`, never reading ctx->r31), but broke any handwritten code that computes addresses from $ra. Concrete failure in Stadium: func_80048904 (audio synth) uses the bltzal $zero, X / bgezal $zero, X PC-arithmetic primitive: the branch is always-not-taken (since $zero is never < 0), but the link write is still committed by real MIPS, allowing the following `addiu $t3, $ra, OFFSET` to compute a helper-function address PC-relatively. With the link write missing, $t3 became a stale return-address-into-the-caller plus OFFSET, then `jalr $t3` inside func_80048A58 dispatched to garbage (LOOKUP_FUNC miss at 0x800AA638) and SEH-crashed mid-audio-frame. Fix: emit `ctx->r31 = vram + 8;` immediately before each linking instruction's call/branch emit. For conditional-link branches the write is hoisted outside the if-block so it fires regardless of branch direction. Verified: regen + 60s smoke test reaches frame 3597 with func_80048904 → func_80048A58 in the trace, no SEH crash, no 0x800AA638 lookup miss, audio probe 6/6 OK. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/recompilation.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/recompilation.cpp b/src/recompilation.cpp index a6e8fc9..948ba7f 100644 --- a/src/recompilation.cpp +++ b/src/recompilation.cpp @@ -538,6 +538,12 @@ bool process_instruction(GeneratorType& generator, const N64Recomp::Context& con break; // Branches case InstrId::cpu_jal: + // MIPS link side-effect: $ra := PC+8, written before the delay slot + // executes. Required for handwritten code that computes addresses + // from $ra (e.g. Stadium audio synth's `addiu $t3, $ra, OFFSET` + // PC-arithmetic trick). + print_indent(); + fmt::print(output_file, "ctx->r31 = 0x{:08X}u;\n", instr_vram + 8); if (!print_func_call_by_address(instr.getBranchVramGeneric())) { return false; } @@ -548,6 +554,9 @@ bool process_instruction(GeneratorType& generator, const N64Recomp::Context& con fmt::print(stderr, "Invalid return address reg for jalr: f{}\n", rd); return false; } + // MIPS link side-effect: $ra := PC+8 (see cpu_jal comment). + print_indent(); + fmt::print(output_file, "ctx->r31 = 0x{:08X}u;\n", instr_vram + 8); needs_link_branch = true; print_func_call_by_register(rs); break; @@ -773,6 +782,18 @@ bool process_instruction(GeneratorType& generator, const N64Recomp::Context& con auto find_conditional_branch_it = conditional_branch_ops.find(instr_id); if (find_conditional_branch_it != conditional_branch_ops.end()) { + // MIPS link side-effect for bltzal/bgezal/bltzall/bgezall: $ra := PC+8 + // is committed unconditionally, regardless of whether the branch is + // taken. Emit the write BEFORE the if-block so it runs in both + // branch-taken and branch-not-taken paths. Required for handwritten + // code (e.g. Stadium audio synth) that uses bltzal $zero,X as a + // PC-arithmetic primitive: the branch is always-not-taken (since + // $zero is never < 0), but $ra still gets loaded so that the + // following `addiu $t3, $ra, OFFSET` can compute a function pointer. + if (find_conditional_branch_it->second.link) { + print_indent(); + fmt::print(output_file, "ctx->r31 = 0x{:08X}u;\n", instr_vram + 8); + } print_indent(); // TODO combining the branch condition and branch target into one generator call would allow better optimization in the runtime's JIT generator. // This would require splitting into a conditional jump method and conditional function call method.