Files
ARMSX2/pcsx2/Interpreter.cpp
Brandon 45368481be iOS Hybrid JIT Safety, No-JIT Fallback, and Interpreter Performance
# iOS Hybrid JIT Safety, No-JIT Fallback, and Interpreter Performance

## Summary

This pull request preserves the current master JIT path while making iOS game boot reliable when JIT is unavailable, revoked, or unable to allocate executable code memory. Instead of continuing into recompiler-only code with missing mappings, ARMSX2 selects the existing interpreter providers and disables only the facilities which require generated native code.

It also adds a conservative predecoded EE interpreter block cache and enables link-time optimization for the iOS PCSX2 core. Together, these changes improve the practical No-JIT path without changing the normal JIT execution architecture.

Base revision after fetching the latest master:

`1a5fc1c3731dd98f84eb62c7d9ac948241743ce7`

The working branch and `origin/master` were already identical at that revision, so no commit replay or conflict resolution was required.

## Problems addressed

### Booting without JIT could crash or remain on a black screen

The emulator previously continued through code paths which assumed executable code memory existed. If iOS had not granted JIT, had revoked it, or the executable mapping could not be allocated, null or unavailable code-cache memory could still reach:

- The EE and IOP recompilers.
- microVU0 and microVU1.
- Generated VIF unpackers.
- The software GS scanline JIT.
- Fastmem setup.
- MTVU paths which depend on generated VIF execution.

The new capability-driven fallback allows the VM to start using existing interpreter implementations instead of invoking those incompatible providers.

## Performance impact

### No-JIT EE execution

The user-observed EE time changed from **37.55 ms to 30.55 ms** after the conservative cache and ThinLTO baseline was introduced.

That is:

- **7.00 ms less EE time** in the observed workload.
- Approximately **18.6% lower EE processing time** for that workload.
- Approximately **1.23x the previous EE throughput**, if all other conditions are equal.

This is an observed device result supplied with the change, not a benchmark performed during this packaging step. Results remain game-, scene-, device-, thermal-, and settings-dependent.

The fixed block cache uses approximately 1 MiB of static memory on 64-bit builds. It trades that bounded allocation and per-block RAM validation for fewer repeated instruction decodes.

### Normal JIT gameplay

The EE block cache is not called from the JIT's `recExecute()` path. No cache lookup, validation, or interpreter callback is added per JIT-generated instruction.

The validation mutex is used only during idle JIT checking, startup transitions, backend switching, and teardown. The keep-alive timer is stopped before gameplay.

ThinLTO may provide a small native-core improvement, but no specific steady-state JIT gain is claimed.

### JIT validation could race code-memory teardown

Master's JIT keep-alive validates more than the `CS_DEBUGGED` process flag: it checks the active writable code mapping by writing a temporary canary byte, reading it back, and restoring the original byte.

That stronger validation is retained. The problem addressed here is synchronization: canceling a dispatch source prevents future callbacks but does not wait for a callback which is already executing. Without a shared lifetime boundary, a callback could retain the mapping address while another path dismantled the code cache.

### Persistent workers cannot safely change backend in place

A JIT worker owns executable mappings and initialized recompiler providers. An interpreter worker deliberately owns neither. Reusing one worker as the other backend can leave incompatible memory and provider state.

Backend changes now perform a complete worker teardown and recreation rather than attempting to mutate an initialized worker.

### The instruction-by-instruction EE interpreter repeated decode work

No-JIT EE execution previously fetched and decoded every instruction every time it executed. Repeated loops therefore paid the same opcode lookup cost continuously.

A bounded block cache now stores short, validated sequences of instruction words and their predecoded opcode descriptors.

## JIT behavior preserved from master

The normal JIT path remains the preferred path whenever executable code memory is available.

- Fresh-launch worker preparation remains intact.
- The persistent CPU worker and condition-variable wait model remain intact.
- The configured iOS JIT script protocol is still applied by the existing gate.
- `CS_DEBUGGED` validation remains intact.
- Master's writable code-memory canary remains intact.
- The 12-second idle validation interval is unchanged.
- The canary is skipped while the VM or CPU initialization is active.
- JIT-enabled sessions retain EE, IOP, VU0, VU1, VIF, software-GS, fastmem, and MTVU acceleration as configured.
- Temporary No-JIT fallback does not overwrite the user's saved recompiler preferences.
- No interpreter cache lookup is performed by `recExecute()`.

## Hybrid JIT validation synchronization

`DarwinMisc` now owns a private mutex covering the validation canary and executable mapping lifetime.

The synchronization sequence is:

1. Mapping address, size, and alias offset are published while holding the validation mutex.
2. `ValidateJITAlive()` first checks whether CPU work is active.
3. An idle validation takes the mutex before reading or touching the mapping.
4. The original byte and page protection are restored before releasing the mutex.
5. Mapping teardown takes the same mutex, clears the published mapping state, and only then unmaps the aliases.

`WaitForJITValidation()` gives the iOS worker an explicit drain point:

- Before gameplay, the worker marks itself active, stops future periodic validation, and waits for any callback which already passed the idle check.
- Before backend teardown, it stops validation and waits for the same lifetime boundary before `CPUThreadShutdown()` releases executable memory.

The keep-alive dispatch source has separate ownership synchronization so concurrent start/stop operations do not create multiple timers or race timer release.

Interpreter-only sessions never start the keep-alive timer because they have no executable mapping and no JIT grant to preserve.

## No-JIT boot flow

When JIT is unavailable, the boot-scoped runtime flow is:

1. The iOS JIT gate requests interpreter mode for the new CPU worker.
2. VM data memory is allocated normally.
3. Executable code memory is omitted.
4. Recompiler providers are not reserved or initialized.
5. Runtime configuration is clamped to the providers which actually exist.
6. EE and IOP select their interpreter implementations.
7. VU0 and VU1 select their interpreter implementations.
8. VIF uses precompiled unpack functions.
9. Software GS uses its C setup, scanline, and edge functions.
10. Fastmem and MTVU are disabled for that boot.
11. The JIT keep-alive timer is not created.

If the initial JIT gate succeeds but executable allocation subsequently fails, allocation now falls back to the same interpreter path instead of aborting the boot.

These are runtime capability overrides. Saved JIT, fastmem, VU, and MTVU preferences are not rewritten, so a later worker created with valid JIT access can use the configured accelerated path again.

## Safe provider and memory handling

`SysMemory` exposes two explicit capabilities:

- `IsAllocated()` distinguishes an initialized VM memory map from early settings loading.
- `HasCodeMemory()` identifies whether native code generators can be used.

This distinction prevents startup settings loading from being mistaken for a No-JIT VM while allowing every code-generation path to gate itself after allocation.

Provider initialization is tracked explicitly. Shutdown and cache-reset paths therefore avoid touching recompilers which were never constructed.

Releasing the memory map also clears the recorded JIT address range, preventing later diagnostics or validation from treating released memory as live code.

## VIF and MTVU fallback

Generated VIF unpackers share the executable VM allocation. `CanUseVifDynarec()` now describes the actual runtime capability: generated VIF support must be compiled in and executable code memory must exist.

The capability check covers:

- Standard VIF unpack dispatch.
- MTVU unpack dispatch.
- VIF reset.
- Mode-zero unpack tables.

When generated mode-zero entries do not exist, VIF uses the existing precompiled C function table instead of dereferencing an uninitialized generated-function pointer.

MTVU is disabled in interpreter-only mode because its VIF path depends on generated unpack execution.

## Software GS fallback

The software renderer no longer resets, queries, or emits into its native scanline cache when code memory is absent.

Interpreter-only execution selects:

- `CSetupPrim`
- `CDrawScanline`
- `CDrawEdge` when antialiasing requires it

The normal generated software renderer remains unchanged when JIT memory exists, and the Metal hardware renderer is not replaced by this fallback.

## Fastmem behavior

No-JIT execution does not emit fastmem accesses, so interpreter-only sessions skip the 4 GB virtual-address reservation and force fastmem off for that boot.

Settings reloads cannot silently re-enable fastmem against a missing reservation. The existing iOS behavior for a genuine fastmem allocation failure also remains: the VM continues without fastmem instead of terminating startup.

## Persistent-worker backend switching

The CPU worker records whether it was initialized with JIT capability.

When a later boot requests a different backend:

1. The current worker receives an exit request.
2. Idle validation is stopped and drained.
3. `CPUThreadShutdown()` releases the matching providers and mappings.
4. The exiting worker clears its creation/backend state and notifies waiters.
5. The caller waits for actual teardown rather than relying on a fixed sleep.
6. A new worker is created for the requested backend.

This prevents overlapping workers, duplicate memory reservations, and reuse of stale JIT mappings.

## Conservative EE interpreter block cache

The new `no-jit-improvements` component accelerates only the EE interpreter.

### Cache structure

- 4,096 direct-mapped cache slots.
- Up to 16 EE instructions per slot.
- Fixed process-lifetime allocation; no heap allocation occurs in the execution loop.
- Each entry stores the original instruction words and pointers to their decoded opcode descriptors.
- Cache entries are aligned to reduce false sharing and keep slot access predictable.

### Execution

On a cache hit, the interpreter reuses the decoded opcode descriptors and executes the short sequence through the existing interpreter functions. It stops immediately if:

- The program counter no longer matches the expected instruction.
- An exception or other control transfer changes the PC.
- A branch boundary is reached.

If the address cannot be cached safely, the original one-instruction `execI()` path is used.

### Correctness safeguards

- Instruction bytes are compared with current emulated RAM before every reuse.
- Blocks terminate after branches, stores, and COP0 instructions which can change memory or address-translation state.
- EE cache-clear notifications eagerly invalidate overlapping entries.
- A complete reset occurs at interpreter reset and shutdown.
- Large or wrapping invalidation ranges trigger a complete cache reset.
- Debug/development oracle configurations retain the original instruction path.
- The cache is disabled when EE cache emulation requires an instruction view which differs from RAM.

This is intentionally conservative. It does not add direct-threaded dispatch, superinstructions, or new MMI/VU NEON implementations.

## ThinLTO for the iOS core

The iOS Xcode generation script now enables `LTO_PCSX2_CORE`.

PCSX2's existing CMake support applies interprocedural optimization to the selected core source set. This can reduce native call and optimization boundaries in both JIT-enabled and interpreter builds.

ThinLTO does not alter the runtime-generated EE JIT blocks and is not expected to transform JIT performance. Its largest relevance here is reducing host-side overhead around the interpreter and core helpers.

## iOS build stability

The generated Xcode target uses per-file optimized Swift compilation with batch mode disabled. This limits peak compiler memory use for the large SwiftUI source set while preserving optimized Release emission.

This is a build-time setting and adds no runtime work.

### Idle menu

The existing 12-second validation cadence is unchanged. Each idle check validates one mapping byte, restores it, and exits. Interpreter-only workers create no validation timer.

### No-JIT resource use

Interpreter fallback avoids resources which cannot improve No-JIT execution:

- No EE/IOP/VU native code caches.
- No generated VIF cache.
- No software-GS JIT cache.
- No 4 GB fastmem virtual reservation.
- No MTVU worker dependent on generated VIF execution.
- No JIT keep-alive source.

No-JIT remains materially slower than a valid ARM64 recompiler. The purpose is safe boot plus a measurable reduction in interpreter overhead, not parity with JIT.

## Behavior matrix

| Runtime state | EE/IOP | VU0/VU1 | Code cache | Fastmem | MTVU | VIF | SW GS | Keep-alive |
|---|---|---|---:|---:|---:|---|---|---|
| Valid JIT grant | Recompiler as configured | microVU as configured | Allocated | Configured/available | Configured | Generated | Generated | Idle only |
| No `CS_DEBUGGED` grant | Interpreter | Interpreter | None | Off | Off | Precompiled | C functions | Off |
| Executable allocation failure | Interpreter | Interpreter | None | Off | Off | Precompiled | C functions | Off |
| Active JIT gameplay | Recompiler as configured | microVU as configured | Allocated | Configured/available | Configured | Generated | Generated | Stopped |

## Files changed

### JIT lifecycle and iOS worker

- `common/Darwin/DarwinMisc.cpp`
  - Synchronizes validation with executable mapping publication and release.
  - Preserves and safely drains the writable code-memory canary.
- `common/Darwin/DarwinMisc.h`
  - Documents the interpreter capability override and exposes the validation drain API.
- `platforms/ios/app/src/main/cpp/IOS/SceneDelegate.mm`
  - Adds backend-aware persistent-worker recreation and keep-alive ownership synchronization.
- `platforms/ios/app/src/main/cpp/ios_main.mm`
  - Prevents settings repair from re-enabling recompilers during an interpreter-only boot.

### Runtime capability and No-JIT fallbacks

- `pcsx2/Memory.cpp`
- `pcsx2/Memory.h`
- `pcsx2/VMManager.cpp`
- `pcsx2/Vif_Dynarec.h`
- `pcsx2/Vif_Unpack.cpp`
- `pcsx2/MTVU.cpp`
- `pcsx2/GS/Renderers/SW/GSDrawScanline.cpp`
- `pcsx2/vtlb.cpp`
- `pcsx2/vtlb.h`

These files make executable code memory an explicit capability and gate EE, IOP, VU, VIF, software-GS, fastmem, and MTVU behavior accordingly.

### EE interpreter performance

- `pcsx2/no-jit-improvements.cpp`
- `pcsx2/no-jit-improvements.h`
- `pcsx2/Interpreter.cpp`
- `pcsx2/CMakeLists.txt`

These files implement and register the validated predecoded EE block cache.

### iOS build configuration

- `platforms/ios/scripts/generate-ios-xcode.sh`
  - Enables the existing PCSX2 core LTO target.
- `platforms/ios/app/src/main/cpp/CMakeLists.txt`
  - Uses per-file optimized Swift compilation to reduce build-time frontend memory pressure.


## Explicitly not included

The subsequently evaluated POC's are absent from this PR:

- Direct-threaded EE dispatch.
- EE superinstructions.
- New ARM64 NEON MMI implementations.
- New ARM64 NEON VU implementations.

`pcsx2/MMI.cpp` and `pcsx2/VUops.cpp` remain identical to the current master revision.

## Validation

- Fetched and compared against the latest `origin/master`.
- Confirmed the local branch and remote master resolve to the same base revision.
- Full unsigned iOS IPA build completed with `platforms/ios/scripts/build-ios-ipa.sh` before packaging.
- `git diff --check` completed without whitespace errors.
2026-07-31 15:56:20 +02:00

745 lines
17 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Common.h"
#include "R5900OpcodeTables.h"
#include "VMManager.h"
#include "Elfheader.h"
#include "Cache.h"
#include "ee_divtrace.h"
#include "no-jit-improvements.h"
#include "DebugTools/Breakpoints.h"
#include "common/FastJmp.h"
#include <float.h>
using namespace R5900; // for OPCODE and OpcodeImpl
extern int vu0branch, vu1branch;
static int branch2 = 0;
static u32 cpuBlockCycles = 0; // 3 bit fixed point version of cycle count
static std::string disOut;
static bool intExitExecution = false;
static fastjmp_buf intJmpBuf;
static u32 intLastBranchTo;
void intEventTest();
// Charge raw block cycles for a syscall handler the interpreter
// SKIPPED (FlushCache/iFlushCache under g_skip_flushcache_syscall), mirroring the
// JIT's recSYSCALL `s_nBlockCycles += 5650`. cpuBlockCycles is the same 3-bit
// fixed-point accumulator as the JIT's s_nBlockCycles with identical scaling
// (intUpdateCPUCycles == scaleblockcycles_calculation), so adding the same raw
// constant keeps the cycle-derived hardware (EE timers) in lockstep across the
// skip. Gated entirely by the caller; no effect in production.
void intChargeSkippedHandlerCycles(u32 raw_block_cycles)
{
cpuBlockCycles += raw_block_cycles;
}
void intUpdateCPUCycles()
{
const bool lowcycles = (cpuBlockCycles <= 40);
const s8 cyclerate = EmuConfig.Speedhacks.EECycleRate;
u32 scale_cycles = 0;
if (cyclerate == 0 || lowcycles || cyclerate < -99 || cyclerate > 3)
scale_cycles = cpuBlockCycles >> 3;
else if (cyclerate > 1)
scale_cycles = cpuBlockCycles >> (2 + cyclerate);
else if (cyclerate == 1)
scale_cycles = (cpuBlockCycles >> 3) / 1.3f; // Adds a mild 30% increase in clockspeed for value 1.
else if (cyclerate == -1) // the mildest value.
// These values were manually tuned to yield mild speedup with high compatibility
scale_cycles = (cpuBlockCycles <= 80 || cpuBlockCycles > 168 ? 5 : 7) * cpuBlockCycles / 32;
else
scale_cycles = ((5 + (-2 * (cyclerate + 1))) * cpuBlockCycles) >> 5;
// Ensure block cycle count is never less than 1.
cpuRegs.cycle += (scale_cycles < 1) ? 1 : scale_cycles;
if (cyclerate > 1)
{
cpuBlockCycles &= (0x1 << (cyclerate + 2)) - 1;
}
else
{
cpuBlockCycles &= 0x7;
}
}
// These macros are used to assemble the repassembler functions
void intBreakpoint(bool memcheck)
{
const u32 pc = cpuRegs.pc;
if (CBreakPoints::CheckSkipFirst(BREAKPOINT_EE, pc) != 0)
{
CBreakPoints::ClearSkipFirst(BREAKPOINT_EE);
return;
}
if (!memcheck)
{
auto cond = CBreakPoints::GetBreakPointCondition(BREAKPOINT_EE, pc);
if (cond && !cond->Evaluate())
return;
}
CBreakPoints::SetBreakpointTriggered(true, BREAKPOINT_EE);
VMManager::SetPaused(true);
Cpu->ExitExecution();
}
void intMemcheck(u32 op, u32 bits, bool store)
{
// compute accessed address
u32 start = cpuRegs.GPR.r[(op >> 21) & 0x1F].UL[0];
if (static_cast<s16>(op) != 0)
start += static_cast<s16>(op);
if (bits == 128)
start &= ~0x0F;
start = standardizeBreakpointAddress(start);
const u32 end = start + bits/8;
auto checks = CBreakPoints::GetMemChecks(BREAKPOINT_EE);
for (size_t i = 0; i < checks.size(); i++)
{
auto& check = checks[i];
if (check.result == 0)
continue;
if ((check.memCond & MEMCHECK_WRITE) == 0 && store)
continue;
if ((check.memCond & MEMCHECK_READ) == 0 && !store)
continue;
if (check.hasCond)
{
if (!check.cond.Evaluate())
continue;
}
if (start < check.end && check.start < end)
intBreakpoint(true);
}
}
void intCheckMemcheck()
{
const u32 pc = cpuRegs.pc;
const int needed = isMemcheckNeeded(pc);
if (needed == 0)
return;
const u32 op = memRead32(needed == 2 ? pc + 4 : pc);
const OPCODE& opcode = GetInstruction(op);
const bool store = (opcode.flags & IS_STORE) != 0;
switch (opcode.flags & MEMTYPE_MASK)
{
case MEMTYPE_BYTE:
intMemcheck(op, 8, store);
break;
case MEMTYPE_HALF:
intMemcheck(op, 16, store);
break;
case MEMTYPE_WORD:
intMemcheck(op, 32, store);
break;
case MEMTYPE_DWORD:
intMemcheck(op, 64, store);
break;
case MEMTYPE_QWORD:
intMemcheck(op, 128, store);
break;
}
}
static void execI()
{
// execI is called for every instruction so it must remains as light as possible.
// If you enable the next define, Interpreter will be much slower (around
// ~4fps on 3.9GHz Haswell vs ~8fps (even 10fps on dev build))
// Extra note: due to some cycle count issue PCSX2's internal debugger is
// not yet usable with the interpreter
//#define EXTRA_DEBUG
#if defined(EXTRA_DEBUG) || defined(PCSX2_DEVBUILD)
// check if any breakpoints or memchecks are triggered by this instruction
if (isBreakpointNeeded(cpuRegs.pc))
intBreakpoint(false);
intCheckMemcheck();
CBreakPoints::CommitClearSkipFirst(BREAKPOINT_EE);
#endif
const u32 pc = cpuRegs.pc;
// We need to increase the pc before executing the memRead32. An exception could appears
// and it expects the PC counter to be pre-incremented
cpuRegs.pc += 4;
// interprete instruction
cpuRegs.code = memRead32( pc );
const OPCODE& opcode = GetCurrentInstruction();
#if 0
static long int runs = 0;
//use this to find out what opcodes your game uses. very slow! (rama)
runs++;
//leave some time to startup the testgame
if (runs > 1599999999)
{
//find all opcodes beginning with "L"
if (opcode.Name[0] == 'L')
{
Console.WriteLn ("Load %s", opcode.Name);
}
}
#endif
#if 0
static long int print_me = 0;
// Based on cycle
// if ( cpuRegs.cycle > 0x4f24d714 )
// Or dump from a particular PC (useful to debug handler/syscall)
if (pc == 0x80000000)
{
print_me = 2000;
}
if (print_me)
{
print_me--;
disOut.clear();
disR5900Fasm(disOut, cpuRegs.code, pc);
CPU_LOG( disOut.c_str() );
}
#endif
cpuBlockCycles += opcode.cycles * (2 - ((cpuRegs.CP0.n.Config >> 18) & 0x1));
opcode.interpret();
#ifdef PCSX2_RECOMPILER_TESTS
// One sample per retired instruction (including branch delay slots, which also
// flow through execI). cpuRegs.pc now points at the next instruction to execute,
// so a sample with pc=X is "architectural state just before executing X" — the
// same point the JIT block hook captures for block entry X. Off unless
// ee_divtrace::g_enabled was set for this frame (single relaxed load otherwise).
// Test-hook only — release builds drop the per-instruction probe entirely.
if (ee_divtrace::g_enabled.load(std::memory_order_relaxed))
ee_divtrace::RecordSample(cpuRegs.pc);
#endif
}
static bool execCachedI(u32 pc, u32 code, const OPCODE& opcode)
{
// A previous instruction can redirect execution without using an opcode
// marked as a branch (for example, an exception). Stop at that boundary.
if (cpuRegs.pc != pc)
return false;
cpuRegs.pc = pc + 4;
cpuRegs.code = code;
cpuBlockCycles += opcode.cycles * (2 - ((cpuRegs.CP0.n.Config >> 18) & 0x1));
opcode.interpret();
#ifdef PCSX2_RECOMPILER_TESTS
if (ee_divtrace::g_enabled.load(std::memory_order_relaxed))
ee_divtrace::RecordSample(cpuRegs.pc);
#endif
return (opcode.flags & IS_BRANCH) == 0 && cpuRegs.pc == (pc + 4);
}
static __fi void _doBranch_shared(u32 tar)
{
branch2 = cpuRegs.branch = 1;
execI();
// branch being 0 means an exception was thrown, since only the exception
// handler should ever clear it.
if( cpuRegs.branch != 0 )
{
if (Cpu == &intCpu)
{
if (intLastBranchTo == tar && EmuConfig.Speedhacks.WaitLoop)
{
intUpdateCPUCycles();
bool can_skip = true;
if (tar != 0x81fc0)
{
if ((cpuRegs.pc - tar) < (4 * 10))
{
for (u32 i = tar; i < cpuRegs.pc; i += 4)
{
if (PSM(i) != 0)
{
can_skip = false;
break;
}
}
}
else
can_skip = false;
}
if (can_skip)
{
if (static_cast<s64>(cpuRegs.nextEventCycle - cpuRegs.cycle) > 0)
cpuRegs.cycle = cpuRegs.nextEventCycle;
else
cpuRegs.nextEventCycle = cpuRegs.cycle;
}
}
}
intLastBranchTo = tar;
cpuRegs.pc = tar;
cpuRegs.branch = 0;
}
}
static void doBranch( u32 target )
{
_doBranch_shared( target );
intUpdateCPUCycles();
intEventTest();
}
void intDoBranch(u32 target)
{
//Console.WriteLn("Interpreter Branch ");
_doBranch_shared( target );
if( Cpu == &intCpu )
{
intUpdateCPUCycles();
intEventTest();
}
}
void intSetBranch()
{
branch2 = /*cpuRegs.branch =*/ 1;
}
////////////////////////////////////////////////////////////////////
// R5900 Branching Instructions!
// These are the interpreter versions of the branch instructions. Unlike other
// types of interpreter instructions which can be called safely from the recompilers,
// these instructions are not "recSafe" because they may not invoke the
// necessary branch test logic that the recs need to maintain sync with the
// cpuRegs.pc and delaySlot instruction and such.
namespace R5900 {
namespace Interpreter {
namespace OpcodeImpl {
/*********************************************************
* Jump to target *
* Format: OP target *
*********************************************************/
// fixme: looking at the other branching code, shouldn't those _SetLinks in BGEZAL and such only be set
// if the condition is true? --arcum42
void J()
{
doBranch(_JumpTarget_);
}
void JAL()
{
// 0x3563b8 is the start address of the function that invalidate entry in TLB cache
if (EmuConfig.Gamefixes.GoemonTlbHack) {
if (_JumpTarget_ == 0x3563b8)
GoemonUnloadTlb(cpuRegs.GPR.n.a0.UL[0]);
}
_SetLink(31);
doBranch(_JumpTarget_);
}
/*********************************************************
* Register branch logic *
* Format: OP rs, rt, offset *
*********************************************************/
void BEQ() // Branch if Rs == Rt
{
if (cpuRegs.GPR.r[_Rs_].SD[0] == cpuRegs.GPR.r[_Rt_].SD[0])
doBranch(_BranchTarget_);
else
intEventTest();
}
void BNE() // Branch if Rs != Rt
{
if (cpuRegs.GPR.r[_Rs_].SD[0] != cpuRegs.GPR.r[_Rt_].SD[0])
doBranch(_BranchTarget_);
else
intEventTest();
}
/*********************************************************
* Register branch logic *
* Format: OP rs, offset *
*********************************************************/
void BGEZ() // Branch if Rs >= 0
{
if(cpuRegs.GPR.r[_Rs_].SD[0] >= 0)
{
doBranch(_BranchTarget_);
}
}
void BGEZAL() // Branch if Rs >= 0 and link
{
_SetLink(31);
if (cpuRegs.GPR.r[_Rs_].SD[0] >= 0)
{
doBranch(_BranchTarget_);
}
}
void BGTZ() // Branch if Rs > 0
{
if (cpuRegs.GPR.r[_Rs_].SD[0] > 0)
{
doBranch(_BranchTarget_);
}
}
void BLEZ() // Branch if Rs <= 0
{
if (cpuRegs.GPR.r[_Rs_].SD[0] <= 0)
{
doBranch(_BranchTarget_);
}
}
void BLTZ() // Branch if Rs < 0
{
if (cpuRegs.GPR.r[_Rs_].SD[0] < 0)
{
doBranch(_BranchTarget_);
}
}
void BLTZAL() // Branch if Rs < 0 and link
{
_SetLink(31);
if (cpuRegs.GPR.r[_Rs_].SD[0] < 0)
{
doBranch(_BranchTarget_);
}
}
/*********************************************************
* Register branch logic Likely *
* Format: OP rs, offset *
*********************************************************/
void BEQL() // Branch if Rs == Rt
{
if(cpuRegs.GPR.r[_Rs_].SD[0] == cpuRegs.GPR.r[_Rt_].SD[0])
{
doBranch(_BranchTarget_);
}
else
{
cpuRegs.pc +=4;
intEventTest();
}
}
void BNEL() // Branch if Rs != Rt
{
if(cpuRegs.GPR.r[_Rs_].SD[0] != cpuRegs.GPR.r[_Rt_].SD[0])
{
doBranch(_BranchTarget_);
}
else
{
cpuRegs.pc +=4;
intEventTest();
}
}
void BLEZL() // Branch if Rs <= 0
{
if(cpuRegs.GPR.r[_Rs_].SD[0] <= 0)
{
doBranch(_BranchTarget_);
}
else
{
cpuRegs.pc +=4;
intEventTest();
}
}
void BGTZL() // Branch if Rs > 0
{
if(cpuRegs.GPR.r[_Rs_].SD[0] > 0)
{
doBranch(_BranchTarget_);
}
else
{
cpuRegs.pc +=4;
intEventTest();
}
}
void BLTZL() // Branch if Rs < 0
{
if(cpuRegs.GPR.r[_Rs_].SD[0] < 0)
{
doBranch(_BranchTarget_);
}
else
{
cpuRegs.pc +=4;
intEventTest();
}
}
void BGEZL() // Branch if Rs >= 0
{
if(cpuRegs.GPR.r[_Rs_].SD[0] >= 0)
{
doBranch(_BranchTarget_);
}
else
{
cpuRegs.pc +=4;
intEventTest();
}
}
void BLTZALL() // Branch if Rs < 0 and link
{
_SetLink(31);
if(cpuRegs.GPR.r[_Rs_].SD[0] < 0)
{
doBranch(_BranchTarget_);
}
else
{
cpuRegs.pc +=4;
intEventTest();
}
}
void BGEZALL() // Branch if Rs >= 0 and link
{
_SetLink(31);
if(cpuRegs.GPR.r[_Rs_].SD[0] >= 0)
{
doBranch(_BranchTarget_);
}
else
{
cpuRegs.pc +=4;
intEventTest();
}
}
/*********************************************************
* Register jump *
* Format: OP rs, rd *
*********************************************************/
void JR()
{
// 0x33ad48 and 0x35060c are the return address of the function (0x356250) that populate the TLB cache
if (EmuConfig.Gamefixes.GoemonTlbHack) {
const u32 add = cpuRegs.GPR.r[_Rs_].UL[0];
if (add == 0x33ad48 || add == 0x35060c)
GoemonPreloadTlb();
}
doBranch(cpuRegs.GPR.r[_Rs_].UL[0]);
}
void JALR()
{
const u32 temp = cpuRegs.GPR.r[_Rs_].UL[0];
if (_Rd_) _SetLink(_Rd_);
doBranch(temp);
}
} } } // end namespace R5900::Interpreter::OpcodeImpl
// --------------------------------------------------------------------------------------
// R5900cpu/intCpu interface (implementations)
// --------------------------------------------------------------------------------------
static void intReserve()
{
// fixme : detect cpu for use the optimize asm code
}
static void intReset()
{
cpuRegs.branch = 0;
branch2 = 0;
NoJITImprovements::ResetEEBlockCache();
}
void intEventTest()
{
// Perform counters, ints, and IOP updates:
_cpuEventTest_Shared();
if (intExitExecution)
{
intExitExecution = false;
if (CHECK_EEREC)
writebackCache();
fastjmp_jmp(&intJmpBuf, 1);
}
}
static void intSafeExitExecution()
{
// If we're currently processing events, we can't safely jump out of the interpreter here, because we'll
// leave things in an inconsistent state. So instead, we flag it for exiting once cpuEventTest() returns.
if (eeEventTestIsActive)
intExitExecution = true;
else
{
if (CHECK_EEREC)
writebackCache();
fastjmp_jmp(&intJmpBuf, 1);
}
}
static void intCancelInstruction()
{
// See execute function.
fastjmp_jmp(&intJmpBuf, 0);
}
static void intExecute()
{
// This will come back as zero the first time it runs, or on instruction cancel.
// It will come back as nonzero when we exit execution.
if (fastjmp_set(&intJmpBuf) != 0)
return;
for (;;)
{
if (!VMManager::Internal::HasBootedELF())
{
// Avoid reloading every instruction.
u32 elf_entry_point = VMManager::Internal::GetCurrentELFEntryPoint();
u32 eeload_main = g_eeloadMain;
u32 eeload_exec = g_eeloadExec;
while (true)
{
execI();
if (cpuRegs.pc == EELOAD_START)
{
// The EELOAD _start function is the same across all BIOS versions afaik
const u32 mainjump = memRead32(EELOAD_START + 0x9c);
if (mainjump >> 26 == 3) // JAL
g_eeloadMain = ((EELOAD_START + 0xa0) & 0xf0000000U) | (mainjump << 2 & 0x0fffffffU);
eeload_main = g_eeloadMain;
}
else if (cpuRegs.pc == eeload_main)
{
eeloadHook();
if (VMManager::Internal::IsFastBootInProgress())
{
// See comments on this code in iR5900.cpp's recRecompile()
const u32 typeAexecjump = memRead32(EELOAD_START + 0x470);
const u32 typeBexecjump = memRead32(EELOAD_START + 0x5B0);
const u32 typeCexecjump = memRead32(EELOAD_START + 0x618);
const u32 typeDexecjump = memRead32(EELOAD_START + 0x600);
if ((typeBexecjump >> 26 == 3) || (typeCexecjump >> 26 == 3) || (typeDexecjump >> 26 == 3)) // JAL to 0x822B8
g_eeloadExec = EELOAD_START + 0x2B8;
else if (typeAexecjump >> 26 == 3) // JAL to 0x82170
g_eeloadExec = EELOAD_START + 0x170;
else
Console.WriteLn("intExecute: Could not enable launch arguments for fast boot mode; unidentified BIOS version! Please report this to the PCSX2 developers.");
eeload_exec = g_eeloadExec;
}
elf_entry_point = VMManager::Internal::GetCurrentELFEntryPoint();
}
else if (cpuRegs.pc == eeload_exec)
{
eeloadHook2();
}
else if (cpuRegs.pc == elf_entry_point)
{
VMManager::Internal::EntryPointCompilingOnCPUThread();
break;
}
}
}
else
{
while (true)
{
if (!NoJITImprovements::ExecuteEEBlock(cpuRegs.pc, execCachedI))
execI();
}
}
}
}
static void intStep()
{
// Arm the cancel target: intCancelInstruction (TLB miss / vtlb_Miss path)
// longjmps to intJmpBuf, which only intExecute used to arm — a cancel
// during a single Step (debugger stepping, recompiler_tests interp
// oracle) jumped through an unarmed buffer straight to PC=0. A cancelled
// instruction has already vectored via cpuException, so returning here
// with pc on the exception vector is exactly one completed "step".
if (fastjmp_set(&intJmpBuf) != 0)
return;
execI();
}
static void intClear(u32 Addr, u32 Size)
{
NoJITImprovements::InvalidateEEBlockCache(Addr, Size);
}
static void intShutdown() {
NoJITImprovements::ResetEEBlockCache();
}
R5900cpu intCpu =
{
intReserve,
intShutdown,
intReset,
intStep,
intExecute,
intSafeExitExecution,
intCancelInstruction,
intClear
};