95 Commits
Author SHA1 Message Date
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
Brian Degenhardt b1dbb0af84 Remove the replaced backend's interpreter-fallback glue
The yaps2 recompilers compile every EE opcode natively, so the
per-instruction interpreter fallback the previous arm64 backend relied
on is dead code: drop intExecuteOneInst (Interpreter.cpp/R5900.h) and
the AndroidEEOpHist fallback-opcode histogram. EEDiffVerify stays — its
per-op hooks are worth re-emitting from the new recompiler later.

Also hoist the Qt metatype declarations shared by moc'd headers into
QtMetaTypes.h: with the debugger sources now optional the autogen bucket
layout shifted, exposing a specialization-after-instantiation error when
moc_MainWindow preceded moc_QtHost in mocs_compilation.
2026-07-19 10:32:53 -07:00
Brian Degenhardt 3e077eff9b Merge yaps2: arm64 JIT transplant + test/perf/libretro infrastructure
Merges yaps2/main (github.com/yaps2/yaps2, c16b88cb7) into ARMSX2,
replacing the arm64 recompiler family with the yaps2 JITs and importing
the yaps2 testing, perf, and libretro infrastructure. Common ancestor is
upstream PCSX2 342db5152 (2026-06-19); git auto-merged all but 38 files.

Replaced (deleted in this merge, recoverable from history):
- arm64/aR5900*, aR3000A*, aVU* -> arm64/iR5900*/iR3000A*/microVU*-arm64:
  EE static-pin register file with lazy dirty tracking, dual-residence
  allocator, IOP block linking, native COP2 macro ops, inline unaligned
  fastmem, persisted VU program cache, call-ret shadow ring, VU0 spin
  fast-forward.
- MVU_DIFF shadow-run hooks in shared VU interpreter TUs (superseded by
  the offline vurunner JIT-vs-interp oracle).

Imported from yaps2:
- tests/ctest/core/recompilers: ~80 gtest suites (EE/IOP/VU differential
  harnesses, fuzzers, ABI digest tripwire, capture format pins) plus the
  gs_vertex_tests kernel oracle.
- pcsx2-vurunner / pcsx2-eerunner headless capture-replay runners.
- tools/perf counter-based A/B rigs, perf jitdump productionization,
  PmuCounters, clang-perf/clang-handheld presets.
- pcsx2-libretro core (ENABLE_LIBRETRO, default OFF; rename pending).
- GS vertex-kick fast path (GV series): TBL-based packed parse,
  register-resident kick, scalar-outcode cull, fused draw-rect/FindMinMax.
- Null renderer, VK_KHR_display direct WSI, swapchain PresentStats.
- SPU2 NEON mixer vectorization, EE timer read clamp (NFL 2K5 hang),
  IOP ioman signed-compare fix, assorted UB fixes.

Kept from ARMSX2 in the both-touched files:
- iOS dual-map W^X and fastmem-unavailable resilience (Memory, HostSys,
  vtlb). The split data/code area model is retained; both areas now take
  fixed VA hints so cached VU JIT code stays deterministic on Linux.
- Android thread-affinity model, VMState shutdown early-outs, all
  platform frontends, branding, CI, RetroAchievements identity/policy.
- GSDeviceVK: ARMSX2's push-descriptor decision logic (Mali crash gate,
  proprietary-vs-turnip Adreno split) merged with yaps2's descriptor-pool
  exhaustion recovery (flush + render-pass restart instead of dropped
  binds). Vendor feature policy is the union: Mali fbfetch policy with
  MediaTek/G57/Xclipse gates from ARMSX2; Adreno stencil/ROV/
  test-and-sample-depth hang avoidance and no_ps2_z_quantization from
  yaps2.

Build-system notes:
- The Qt debugger is now gated behind ENABLE_QT_DEBUGGER (default off on
  arm64) so handheld builds drop the KDDockWidgets dependency.
- GSDeviceNone and remaining yaps2 GS code were ported to the newer
  upstream GSTexture Usage-flags API.

The replaced backend's interpreter-fallback glue (intExecuteOneInst,
AndroidEEOpHist) and the EEDiffVerify runtime differ are retained for
now; dead pieces will be removed in a follow-up commit.
2026-07-19 10:24:29 -07:00
Brian DegenhardtandClaude Fable 5 44e4357a09 EE interp: arm the cancel jmp_buf in intStep, not just intExecute
intCancelInstruction (vtlb_Miss TLB-miss path) longjmps to intJmpBuf, which
only intExecute armed. A cancelled instruction during single-stepping — the
debugger's step path, and the recompiler_tests interp oracle — jumped through
the unarmed buffer straight to PC=0. The cancelled instruction has already
vectored via cpuException, so returning from the step with pc on the vector
is exactly one completed step. Found by the EE-SRA 2 workstream-A delay-slot
TLB-miss test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 06:44:20 -07:00
8fb919e6f7 arm64: in-process recompiler differential-test + diagnostics framework
DiffJitVsInterp harness (EE/IOP/VU synthetic JIT-vs-interpreter tests) plus the
shared capture/divergence-localizer infrastructure (vu_capture, ee_divtrace,
microVU_Divtrace, VU1Trace) and the test hooks they install in interp/GIF/COP2 paths.
All hook sites are guarded by PCSX2_RECOMPILER_TESTS and compile out of release builds.

Co-Authored-By: Ryan Walklin <ryan@testtoast.com>
Co-Authored-By: Brian Degenhardt <bmd@bmdhacks.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:27:56 -07:00
David Isztl aa72a6c054 ARM64: Fix frozen cpuRegs.cycle in single-step fallback for taken branches (Burnout CPCOND0 halt) 2026-06-12 20:17:23 +02:00
David Isztl 09fd682677 ARM64: EE dispatcher + delay-slot block compiler (Phase 4.3)
The EE recompiler now actually runs guest code: Cpu = &recCpu boots the BIOS
on ARM64 (Mode Changed to DVD PAL, Pad DS2 Config Finished).

- aR5900.cpp: multi-instruction recCompileBlock (per-block x19+LR prologue/
  epilogue, straight-line ops, handled-branch + delay-slot terminator, length
  cap, exit-before-uncompilable-op); recEmitBranch decodes J/JAL/JR/JALR +
  BEQ/BNE/BLEZ/BGTZ + REGIMM B*Z[AL] onto the Phase 4.1/4.2 generators; C++
  recExecute dispatcher loop (pc -> recGetBlock -> run -> cycle add ->
  _cpuEventTest_Shared, fastjmp exit); recScaleBlockCycles; reset/exit plumbing.
- Interpreter.cpp/R5900.h: intExecuteOneInst() — per-opcode interpreter fallback
  for ops the rec can't compile (COP/FPU/MMI, syscalls, traps, likely/COP
  branches), mirroring execI with the interpreter's own branch/delay-slot handling.
- VMManager.cpp: select recCpu for EE on ARM64 when CHECK_EEREC (IOP+VU still int).

Key fix: blocks clobber LR via vtlb/interp calls, so each block saves/restores
LR itself (a shared enter-trampoline + bare block crashed on the block's RET).

unittests green; arm64 binary; BIOS-boot verified with the rec active.
2026-06-04 14:19:32 +02:00
Ziemas f576962dd8 EE: Switch to 64 bit cycle counter
[SAVEVERSION+]
2026-03-07 09:28:55 -05:00
SternXD d983b2b066 Copyright: Change year from 2002-2025 to 2002-2026 2026-01-15 00:22:32 +01:00
Ty de03d2f672 Debugger: Properly reset the breakpoint skip first when skipped 2026-01-11 14:32:21 -05:00
refractionpcsx2 02f0921b2d EE: Force sync on EE timer read 2025-12-05 11:20:15 -05:00
Ty cb0bf953d3 Interpreter: Fix warning from reading 64 bit GPR into u32 variable.
The PS2 has 32 bit addresses!
2025-11-21 18:20:58 -05:00
refractionpcsx2 2d03b21f2b Formatting: Clean up some if spaces 2025-05-17 22:47:38 +02:00
TheTechnician27 23fd57f641 Copyright: Change year from 2002-2024 to 2002-2025 2025-01-20 05:07:26 +01:00
Ty Lamontagne 313ddb7879 EE Cache: Writeback dirty cache entries when exiting the interpreter. 2024-12-08 17:49:33 -05:00
GovanifY 132431b7c8 headers: relicense to GPL-3.0+
also update to 2024 while i'm at it
2024-07-30 17:17:13 -04:00
Ty Lamontagne de020978e4 Debugger: Use std::string for bp conditions. Implement memory bp conditions 2024-07-11 11:21:39 -04:00
JordanTheToaster ea00a89cff Interpreter: Comment cleanup 2024-02-17 15:15:51 +00:00
Stenzek d9abe10308 Misc: Remove explicit PCH include, switch to SPDX 2023-12-24 14:03:14 +10:00
lightningterror 8d7227b1f9 Interpreter: Fix Wsign-compare warning. 2023-11-08 15:43:52 +01:00
refractionpcsx2 927d8ce237 EE Int: Implement a variation of the wait loop hack 2023-11-03 21:28:39 +00:00
refractionpcsx2 461f3bd89f EE Int: Add EE overclocking to interpreter 2023-11-03 21:28:39 +00:00
refractionpcsx2 ed1791ab02 Int/VU0: Improve VU0 sync, mainly for interpreter.
Also correctly doubles the opcode time when double issue is disabled (interpreter).
2023-11-03 21:28:39 +00:00
Ty Lamontagne be405eb705 Debugger: Visually show what CPU triggered a breakpoint 2023-10-26 19:26:43 +01:00
Mrlinkwii e36cd7fe5c Build: remove -32 notation from core filenames 2023-08-01 16:09:09 +01:00