The ARM64 PowerPC JIT computed floating-point arithmetic results correctly
but omitted the set_fprf() side-effect that the interpreter runs after every
FP arithmetic op (updates FPSCR[FPRF] with the result class/sign). Games that
read FPRF back via mffs/mcrfs to classify or clamp their results diverged from
the interpreter: in Daytona USA 2 the opponent AI misbehaved and the rival
cars crashed at the start of the race. With the JIT disabled (interpreter) the
AI matched the standalone.
Add a jit_set_fprf(rD) helper (calls the interpreter's set_fprf on FPR[rD])
and emit a call to it after every op the interpreter runs set_fprf for:
op59 fadds/fmuls/fdivs/fsqrts (via the STORE_SP macro) and op63
fadd/fsub/fmul/fdiv/fsqrt/fmadd/fmsub/fnmadd/fnmsub/frsqrte. Moves (fmr/fneg/
fabs/fnabs), fcmp and fsel are left untouched, matching the interpreter which
does not update FPRF for them. The arithmetic stays native, so the cost is one
short call per FP op rather than falling back to the interpreter.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Thanks to beudbeud for discovering and sharing these improvements:
- emit_ram_load32 applied to lfsx/lfsux, lfs/lfsu, lwbrx, lmw
- ppc_set_ram_ptr() now stores to s_jit_ram_base so pointer
survives ppc_base_init() memset calls (correctness fix)
Also:
- Fix duplicate aarch64 GLES source block in Makefile
- R3DShaderTriangles: explicit highp on projMat in fragment shader
(prevents precision mismatch link error on GLES)
- GLES GPU timer query guards wrapped in #if defined(CORE_GLES)
so desktop GL builds don't reference EXT function pointers
Add emit_ram_load32() which bypasses jit_read32() C call for word-aligned
RAM reads (addr%4==0, addr<0x800000) via a direct LDR from ram_ptr.
Guards: two TBNZ alignment checks prevent the mismatch that occurs at
addr%4==2, where the interpreter uses XOR-addressed Read16 pairs that
read different host bytes than a plain LDR.
Stores stay on the slow path to preserve smc_write() cache-flush detection.
ram_ptr field added to PPC_REGS and set via ppc_set_ram_ptr() at init.
LDRH/STRH added to Arm64Emitter for future 16-bit fast-path work.
Three fixes that together unblock RPi5 JIT execution past boot:
1. stwcx. (subop 150): after storing CR0[EQ]=1 via STRB, emit CMP W0,2
to set ARM Z=1. Without this, the Rc=1 peephole optimizer fuses the
following 'bf cr0[eq]' with stale ARM flags from andi. two instructions
earlier (Z=0), causing the CAS retry loop to never exit. The MIDI queue
producer's kick to midiCtrlPort=0x27 was unreachable, leaving the queue
permanently full and the game frozen at the disclaimer screen.
2. ppc_set_irq_line(0): clear interrupt_pending bit 0x8 alongside 0x1.
Bit 0x8 is the JIT chained-epilogue fast-exit signal; leaving it set
after IRQ deassert caused every JIT block to exit via CBNZ instead of
chaining, making JIT effectively interpreter-speed.
3. mtmsr (subop 146): fall back to interpreter so ppc_set_msr() runs,
which calls ppc603_check_interrupts() to deliver any interrupt newly
enabled by the MSR write (e.g. rfi re-enabling EE).
__aarch64__ fires for the arm64 slice of the macOS universal binary, causing
the linker to reference JitArm64 symbols that are not compiled for platform=osx.
Introduce HAVE_PPC_JIT (set by Makefile for android/rpi64/aarch64) and replace
all __aarch64__ guards in ppc.cpp and ppc603.c with it.
Add m_frameBufferIDTrans2: pre-wired with att2 color + the copy depth
renderbuffer. After StoreDepth fills the copy buffer with opaque depth,
SetFBO(Layer::trans2) binds m_frameBufferIDTrans2 directly — no second
glBlitFramebuffer(GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT) call.
Reduces depth blits per transparent priority layer from 2 to 1.
- FCMP_D_ZERO encoding: 0x1E402008 → 0x1E602008 (was single-precision opcode);
fixed distorted tree geometry in VF4
- frsp (op63 case 12): inline FCVT_S_D+FCVT_D_S causes wrong camera direction;
fall back to interpreter which also updates FPSCR[FPRF] via set_fprf()
- Add jit_frsp helper at FN table index 13 for future op59 STORE_SP use
- Remove all bisect scaffolding (JIT_DP_BISECT_* defines and guards)
- SMC detection: route interpreter WRITE8/16/32 and jit_write8/16/32 through
smc_write() page-bitmap check to invalidate stale JIT blocks on code writes
- Add JitArm64::invalidate_containing() and smc_write(); flush() now clears
m_code_pages bitmap
- Remove debug probes: CAMSTW/CAMST64 in jit_write32/64, CAMW in
Real3D::WriteHighCullingRAM
- Fold jit_write32_stfs into jit_write32; remove cur_pc plumbing from
translate_stfs and translate_op31
- 53C810.cpp: revert entirely — all changes were cosmetic (whitespace,
removed already-commented lines, one DebugLog that is a release no-op)
- Real3D.cpp: revert entirely — only change was commenting out a DebugLog
that is already a release no-op via the LibretroWrapper fix
- Model3.cpp: restore two DebugLog("write32") calls removed in debug-probe
cleanup pass — release no-ops, no reason to diverge from dev
- ppc.cpp: remove spurious blank line added inside ppc_init
- ppc_set_irq_line: set interrupt_pending |= 0x1 | 0x8 so the chained
epilogue's CBNZ exits the block chain immediately on external IRQ
- Guard ppc603_check_interrupts() with s_jit_executing: calling it while
a JIT block is executing corrupts MSR[EE]/npc before the block returns,
leaving EE=0 so jit_done skips IRQ delivery permanently
- DEC sentinel: guard dec_trigger_cycle check with != 0x7fffffff to
prevent a DEC storm that fires on every single block dispatch
- Add ppc_set_jit_enabled(bool) to toggle JIT vs interpreter at runtime
Three coordinated changes:
1. Mirror fatalError into interrupt_pending bit 3 (0x8) at every fatal-set
site, so the JIT chained epilogue needs only one load+branch for both
fatal and interrupt instead of a separate LDRB+CBNZ for fatalError.
2. Use SUBS_W_IMM (flags-setting) for the icount decrement so the
subsequent B.LE needs no separate CMP_W_IMM(W4, 0).
3. Defer the pc/npc write to the slow-exit path only (icount exhausted /
interrupt / fatal). On the tail-call fast path pc/npc are never written,
saving two MOV_W32 + STP per chained block transition. The slow path
always writes them before returning to the dispatch loop, which is the
only consumer of ppc.npc. Interrupt handlers see correct pc/npc because
they are only reachable after a slow-exit which wrote both fields.
Add UINT8 xer_ca to PPC_REGS mirroring XER.CA for direct LDRB/STRB
access instead of LDR+UBFM / CSET+LDR+BFI+STR round-trips through the
full XER word.
emit_load_xer_ca: 2 insns → 1 (LDRB only)
emit_update_xer_ca: 4 insns → 2 (CSET + STRB)
sraw/srawi CA write: 3 insns → 1 (STRB only)
mfspr XER / mtspr XER / mcrxr reconstruct or sync the cache on the
rare paths where the full XER word must be consistent.
ppc.cpp and ppc_ops.c updated so all interpreter CA writes go through
SET_XER_CA_SET/CLR macros that keep xer_ca in sync; ppc_set_spr(XER)
and LoadState also sync on write.
Introduces a block-compiling JIT in Src/CPU/PowerPC/Jit/ that replaces the
interpreter hot loop on aarch64 targets. Goal: reduce PPC thread CPU usage
from ~90-100% on Android phones and RPi5.
Key components:
- Arm64Emitter.h: hand-rolled ARM64 instruction encoder
- JitArm64.h/cpp: block cache, compiler, and opcode translators
- ppc_regs.h: PPC_REGS struct extracted so JIT can include it without
pulling in all of ppc.cpp
- ppc.cpp: extern "C" bridge functions (memory, SPR, FP estimate helpers)
- ppc603.c: JIT dispatch loop + flush on reset
Opcode coverage: all integer D/X-form arithmetic, all load/store forms
(including byte-reverse and FP indexed), all shift/rotate ops, full FP
op59/op63 (including fres/frsqrte/mffs/mcrfs), all CR logical ops,
branch ops (b/bl/bc fully inline; bclr/bcctr fully inline—no interpreter
call per blr), cache/TLB NOPs, and all common SPR accesses.