Skip to content

Latest commit

 

History

History
741 lines (559 loc) · 26 KB

GAME_PORTING_GUIDE.md

File metadata and controls

741 lines (559 loc) · 26 KB

Game Porting Guide

A comprehensive, step-by-step guide to porting a PS3 game to native PC using ps3recomp.


Table of Contents

  1. Overview
  2. Phase 1: Game Selection and Assessment
  3. Phase 2: Binary Acquisition and Decryption
  4. Phase 3: Analysis
  5. Phase 4: Disassembly
  6. Phase 5: Code Generation
  7. Phase 6: Project Setup
  8. Phase 7: Building and Debugging
  9. Phase 8: Stubbing and Implementation
  10. Phase 9: Graphics
  11. Phase 10: Audio and Input
  12. Phase 11: SPU Handling
  13. Phase 12: Testing and Polish
  14. Tips and Best Practices
  15. Case Study: flOw

Overview

Porting a PS3 game with ps3recomp is a per-game project. Unlike emulation (where one binary runs many games), static recompilation requires understanding each game's specific needs. The reward is a truly native port with full modding potential.

What You'll Need

  • A legally obtained copy of the PS3 game
  • Decryption keys or a fSELF version of the game binary
  • Python 3.10+ for the recompiler tools
  • CMake 3.20+ and a C17/C++20 compiler
  • RPCS3 as a reference (for testing and behavior verification)
  • Patience and debugging skills

Expected Timeline

Game Complexity Estimate Examples
Simple PSN title Days to weeks flOw, fl0w, Flower
Standard indie game Weeks to months Journey, Limbo, Braid
Full retail title Months Standard action/adventure games
Complex AAA title Months to years The Last of Us, Uncharted

The main variables are: how many unique system APIs the game uses, whether it uses SPU programs extensively, and how complex its graphics pipeline is.


Phase 1: Game Selection and Assessment

Choose a Good First Target

For your first port, look for games that:

  1. Work well in RPCS3 — this means the game's behavior is well-understood
  2. Are simple — fewer system API calls = fewer HLE modules needed
  3. Use common engines — PhyreEngine, Unreal Engine 3, etc. have known patterns
  4. Don't rely on SPU-heavy compute — audio-only SPU use is much simpler than physics/particle SPU use
  5. Have a small binary — fewer functions to recompile and debug

Pre-Analysis with RPCS3

Before touching ps3recomp, run the game in RPCS3 with logging enabled:

  1. Enable Log Level: All in RPCS3 settings
  2. Play through the game's startup and early gameplay
  3. Check the log for:
    • Which modules are loaded (cellSysmoduleLoadModule)
    • Which NID calls are frequent
    • Any "TODO" or "Unimplemented" warnings
    • SPU thread creation patterns

This tells you which HLE modules the game needs most.

Check Module Coverage

Cross-reference the game's imports against MODULE_STATUS.md:

# After ELF analysis
python tools/prx_analyzer.py game/EBOOT.ELF --stubs

If coverage is 95%+, the game is a good candidate.


Phase 2: Binary Acquisition and Decryption

Getting the ELF

PS3 game binaries are encrypted in SELF (Signed ELF) format. You need a decrypted ELF:

Option A: fSELF (fake SELF)

  • Some development/debug versions of games use fSELF which can be trivially "decrypted"
  • The elf_parser tool handles fSELF automatically

Option B: RPCS3 Decryption

  • RPCS3 can dump decrypted ELFs when loading a game
  • Enable "Dump Executable" in RPCS3 debug settings
  • Look for the decrypted file in RPCS3's cache directory

Option C: ps3recomp's fSELF rebuilder

Only for debug / prototype fSELF binaries, which are unencrypted — this rebuilds the original ELF without keys. A retail EBOOT.BIN is encrypted and needs Option A or B.

python tools/unfself.py EBOOT.BIN --output game/EBOOT.ELF
python tools/unfself.py EBOOT.BIN --info      # describe it, write nothing

Required Files

File Location on Disc Purpose
EBOOT.BIN PS3_GAME/USRDIR/EBOOT.BIN Main game executable
PARAM.SFO PS3_GAME/PARAM.SFO Game metadata (title, ID, version)
*.sprx PS3_GAME/USRDIR/*.sprx Additional game modules (if any)
TROPHY.TRP PS3_GAME/TROPDIR/*/TROPHY.TRP Trophy configuration
Game assets PS3_GAME/USRDIR/* Textures, models, audio, scripts

Phase 3: Analysis

ELF Analysis

elf_parser.py writes JSON to stdout — it has no --output; redirect it.

mkdir -p analysis
python tools/elf_parser.py game/EBOOT.ELF --all      > analysis/elf_info.json
python tools/elf_parser.py game/EBOOT.ELF --imports  > analysis/imports.json
python tools/elf_parser.py game/EBOOT.ELF --exports  > analysis/exports.json

The flags select what to report:

  • (default) — header, entry point, architecture
  • --imports — imported functions (module + NID)
  • --exports — exported symbols (if any)
  • --sections / --segments / --relocs, or --all for everything

Key Questions to Answer

  1. How many imports? — fewer is better; 100-200 is typical
  2. Which modules? — check coverage against ps3recomp's HLE modules
  3. Any unresolved NIDs? — these need stubs or implementations
  4. Entry point? — the address where execution starts
  5. TOC (Table of Contents)? — the r2 value needed for data access
  6. PRX modules? — does the game load additional .sprx files?
  7. Memory layout? — where does code, data, and BSS live?

NID Resolution

# Resolve the NIDs your imports dump listed (one hex NID per line):
python tools/nid_database.py --batch analysis/nids.txt --json > resolved.json
python tools/nid_database.py --lookup 0xAB8B4DA4        # or one at a time

Review the output for unresolved NIDs. For each one:

  • Check RPCS3 for the function name
  • Determine if it's critical or can be stubbed
  • Add it to the game's stubs.cpp if needed

Phase 4: Disassembly

Function Detection

Stage 1 of the pipeline. ppu_loader.py reads the .opd table — the PS3's own list of function descriptors — and writes the function boundaries, the firmware import table, the segment manifest and the entry/TOC pair in one pass:

python tools/ppu_loader.py game/EBOOT.ELF -o analysis/
#   analysis/EBOOT.functions.json   [{start,end,toc,opd}]
#   analysis/EBOOT.imports.json     [{library,nid,stub}]   <- Phase 5 needs this
#   analysis/EBOOT.image.json       PT_LOAD manifest
#   analysis/EBOOT.loader.json      entry OPD -> (code, toc), module TOC

find_functions.py is the heuristic scanner — prologue, leaf and branch-target detection — for what the OPD does not cover. --seed-json merges in the loader's list rather than starting over:

python tools/find_functions.py game/EBOOT.ELF \
    --seed-json analysis/EBOOT.functions.json --output analysis/functions.json

Review the function list:

  • How many functions? (typical: 500–5000 for an indie game, 10000+ for AAA)
  • Are there symbols? (debug builds have names; retail usually stripped)
  • Any suspiciously large "functions"? (might be data misidentified as code)

Full Disassembly

python tools/ppu_disasm.py game/EBOOT.ELF \
    --functions analysis/functions.json \
    --annotate \
    --output disasm/

SPU Program Detection

If the game uses SPU programs:

python tools/extract_spu_images.py game/EBOOT.ELF --output spu_programs/

SPU ELF segments are embedded within the main PPU ELF. Each one needs separate analysis.


Phase 5: Code Generation

PPU Code Lifting

The lifter takes the ELF, not the disassembly listing:

python tools/ppu_lifter.py game/EBOOT.ELF \
    --functions analysis/EBOOT.functions.json \
    --hle-stubs analysis/EBOOT.imports.json \
    --output recomp/

This generates:

  • ppu_recomp_NNN.cpp — batches of recompiled functions (C++, no post-step)
  • ppu_recomp.h — declarations, helpers and ppu_recomp_register()

--hle-stubs is what makes firmware imports callable. Each import stub address listed in EBOOT.imports.json is emitted as its own function whose body is ps3_hle_call(<nid>, ctx). Leave it out and the lifter translates the import trampolines literally — li r12,0; oris r12,r12,hi; lwz r12,lo(r12); ... bctr — against an import table nothing ever patched, and the first import call the game makes lands on a raw instruction word:

[ppu] unresolved indirect call -> 0x39800000

SPU Code Lifting (if applicable)

# spu_disasm writes to stdout; redirect it
python tools/spu_disasm.py spu_programs/spu_0.elf > spu_disasm/spu_0.txt
# SPU lifter generates similar C output for each SPU program

HLE handler table

Lifting produces the game's code. It does not produce the bridge from the game's firmware imports to this runtime's HLE implementations, and that is a separate generated file:

python tools/gen_hle_nids.py --all --out src/gen/ppu_hle_nids.cpp

Compile it into the port. It defines ppu_hle_register_all(), which ppu_hle_init() calls at startup. runtime/ppu/ppu_hle.cpp carries a weak do-nothing version, so leaving this out is not a link error — the port builds, starts, and then behaves as though every firmware import returned 0. The usual first symptom is an indirect call to something that is not an address:

[ppu] unresolved indirect call -> 0x39800000

0x39800000 is the PowerPC instruction li r12,0, the first word of an import stub. The runtime prints a warning at startup when it finds no handlers registered, which names this directly.

The same message means the other half of Phase 5 is missing when the handler table is present: a lift run without --hle-stubs never reaches the table at all. Check both before chasing it as a lifter bug.

Regenerate it whenever you update ps3recomp: the table is generated from the toolkit's registered modules, so one built against a different revision can reference handlers this one does not have.


Phase 6: Project Setup

Create the Project

cp -r ps3recomp/templates/project/ my_game/

Configure

Edit my_game/config.toml (reference only: nothing reads it yet, so the paths also go on the tool command lines):

[input]
elf_path = "../game/EBOOT.ELF"

[modules]
# Enable only the modules your game uses
cellSysutil   = "hle"
cellGcmSys    = "hle"
cellPad       = "hle"
cellAudio     = "hle"
cellFs        = "hle"
cellFont      = "hle"
# ... add all modules from your import analysis

[debug]
log_hle_calls = true
log_missing_nids = true
break_on_unimplemented = true   # Stop on unimplemented calls

Copy Generated Files

cp recomp/*.c recomp/*.cpp my_game/recompiled/
cp data_segments.c my_game/recompiled/

Set Up Game Assets

Create the virtual filesystem:

mkdir -p my_game/hdd0/game/TITLEID/USRDIR
mkdir -p my_game/hdd0/home/00000001/trophy/TITLEID
# Copy game assets
cp -r PS3_GAME/USRDIR/* my_game/hdd0/game/TITLEID/USRDIR/

Phase 7: Building and Debugging

First Build

cd my_game
cmake -B build -G Ninja -DPS3RECOMP_DIR=/path/to/ps3recomp
cmake --build build 2>&1 | tee build.log

Expect compilation errors on first try. Common issues:

Undefined functions:

error: undefined reference to 'recomp_func_00012345'

→ Function was referenced but not in the function list. Add it or add a stub.

Type mismatches:

error: incompatible pointer types

→ The lifter may need manual adjustment for complex calling conventions.

First Run

./build/my_game 2>&1 | tee run.log

Expected output:

=== ps3recomp game runner ===
Initializing runtime...
Loaded 1234 recompiled functions
Creating main PPU thread...
[WARNING] Unimplemented NID 0xABCDEF01 in module cellFoo
[WARNING] Unimplemented NID 0x12345678 in module cellBar

Debugging Strategy

  1. Run under a debugger (GDB/LLDB/Visual Studio)
  2. Set break_on_unimplemented = true to catch missing functions immediately
  3. Compare with RPCS3 — run the same game in RPCS3 with logging and compare the call sequences
  4. Use log output — log_hle_calls = true shows every HLE function call
  5. Binary search for crashes — if the game crashes in recompiled code, narrow down the problematic function by checking the guest address from the backtrace

Phase 8: Stubbing and Implementation

Adding Game-Specific Stubs

In stubs.cpp, add implementations for unresolved NIDs:

#include "ps3emu/ps3types.h"
#include "ps3emu/error_codes.h"
#include <cstdio>

// Stub for a non-critical function
extern "C" int32_t cellFooDoSomething(uint32_t param1, uint32_t param2)
{
    printf("[STUB] cellFooDoSomething(0x%x, 0x%x)\n", param1, param2);
    return CELL_OK;
}

// Stub for a function that returns data
extern "C" int32_t cellBarGetInfo(uint32_t* out_info)
{
    if (out_info) *out_info = 0;
    printf("[STUB] cellBarGetInfo -> 0\n");
    return CELL_OK;
}

Priority Order

  1. Functions that crash if missing — stub immediately
  2. Functions that produce wrong behavior — implement properly
  3. Functions called during init — must at least return CELL_OK
  4. Functions called during gameplay — implement for correctness
  5. Rarely-called functions — stub last

Phase 9: Graphics

The Challenge

RSX graphics is the hardest part of any PS3 port. The game writes NV47xx GPU commands to a command buffer, and these need to be translated to a modern graphics API.

Strategy

Phase 1: Null renderer

  • Set backend = "null" in config
  • Verify all game logic runs correctly without graphics
  • This proves the recompilation and HLE are working

Phase 2: State tracking

  • cellGcmSys already tracks RSX state (tiles, zcull, display buffers)
  • Log all command buffer writes to understand the rendering pipeline
  • Identify the vertex formats, shader programs, and draw call patterns

Phase 3: Graphics backend

  • Implement a Vulkan or D3D12 renderer that consumes the command buffer
  • Start with basic triangle rendering
  • Progressively add shader translation, texture sampling, blending modes

What's Implemented

cellGcmSys (HLE module — state management):

  • Command buffer control (put/get pointers)
  • Local memory allocation (VRAM heap)
  • IO memory mapping (main memory → GPU accessible)
  • Tile and zcull configuration
  • Display buffer registration, flip and VBlank callbacks
  • Timestamps and report data

RSX Command Processor (libs/video/rsx_commands.h/.c):

  • NV47xx FIFO command buffer parsing (type 0 increasing, type 2 non-increasing)
  • State tracking: surfaces, viewport, scissor, clear, blend, depth/stencil, cull, color mask, alpha test
  • Texture state: 16 units with offset/format/address/filter/rect
  • Vertex attributes: 16 attribs with format/offset/stride
  • Shader program registers (fragment address, vertex load slot)
  • Draw dispatch: draw_arrays, draw_indexed
  • Backend callback interface (rsx_backend) with 12 dispatch points

Null backend (libs/video/rsx_null_backend.h/.c):

  • Win32 window creation
  • Displays RSX clear color via GDI
  • FPS counter and debug overlay
  • Proves command flow works before implementing a real backend

What's NOT done yet:

  • D3D12/Vulkan backend (actual triangle rendering)
  • Vertex/fragment program translation (RSX shaders → HLSL/SPIR-V)
  • Texture upload and sampling
  • Framebuffer management and resolve

Phase 10: Audio and Input

Audio

cellAudio provides real audio output. Most games just need:

  1. cellAudioInit() — starts the mixing thread
  2. Write PCM samples to port buffers
  3. cellAudioSetNotifyEventQueue() — sync with the mixing interval

Common issues:

  • Audio crackling → increase buffer size in config
  • No sound → check that the audio backend matches your OS
  • Wrong sample rate → PS3 native is 48000 Hz

Input

cellPad provides real gamepad input via XInput (Windows) or SDL2.

Button mapping (XInput → PS3):

XInput PS3
A Cross
B Circle
X Square
Y Triangle
LB L1
RB R1
LT L2 (analog)
RT R2 (analog)
Back Select
Start Start
Guide PS

Phase 11: SPU Handling

Assessment

Determine how the game uses SPUs:

  1. SPURS framework — most games use this

    • Check for cellSpursInitialize, cellSpursCreateTask calls
    • The SPURS HLE handles management; actual SPU tasks need attention
  2. Raw SPU — some games create SPU threads directly

    • Check for sys_spu_thread_group_create syscalls
    • Each SPU program needs separate analysis
  3. SPU task types — identify what the SPU programs do:

    • Audio mixing (ATRAC3+, PCM mixing) → replace with host decoding
    • Physics (Havok, Bullet) → intercept and run on host CPU
    • Decompression (zlib, LZ) → replace with host zlib
    • Vertex processing → handle in graphics backend
    • Custom compute → most complex; may need SPU interpreter

HLE Approach (Preferred)

If you can identify the SPU task type:

// In stubs.cpp — intercept a known SPU audio mixing task
void hle_spu_audio_mixer(spu_context* ctx)
{
    // Read parameters from SPU local store
    uint32_t src_addr = spu_ls_read32(ctx, 0x100);
    uint32_t dst_addr = spu_ls_read32(ctx, 0x104);
    uint32_t num_samples = spu_ls_read32(ctx, 0x108);

    // Perform the mixing on the host CPU
    float* src = (float*)(vm_base + src_addr);
    float* dst = (float*)(vm_base + dst_addr);
    for (uint32_t i = 0; i < num_samples; i++)
        dst[i] += src[i];
}

Lifting SPU programs

When the HLE route does not fit (custom compute, or code you would rather run than rewrite), lift the SPU programs and let the runtime execute them.

Lifting. tools/extract_spu_images.py <ppu_elf> --output DIR writes every embedded SPU ELF it finds. tools/spu_lifter.py lifts one image to C (spu_recomp.c / spu_recomp.h); tools/build_spu_workloads.py lifts a whole directory (--images DIR --lifted OUTDIR --out spu_workloads.c --constructor) or discovers the images itself (--input FILE, repeatable), giving each image its own symbol prefix so they link together. The project template does this at configure time when PORT_SPU_INPUTS lists the files to scan (templates/project/CMakeLists.txt, cmake/PS3RecompSPU.cmake); set PORT_ENABLE_SPU=OFF to leave SPU lifting out. Details of the lifter: SPU_LIFTER.md.

Workload registration. The generated spu_workloads.c calls spu_workload_register (or spu_workload_register_img, which also binds the image's indirect-branch table id) from runtime/spu/spu_workload.c, keyed by a fingerprint of the image's bytes. cellSpurs looks the image the game passes in up by that fingerprint, so a table generated from a different release of the game never matches.

Job modules. SPURS job modules that are streamed into local store are not separate ELFs, so extract_spu_images.py does not find them. Depending on the title they sit in the PPU binary's data, in the title's data files (tools/extract_jobmods.py handles modules in zlib streams inside SDATA archives), or exist only at run time: SPU_DUMP_MISS=<dir> writes every SPU image the runtime does not recognise to a file, one per fingerprint (runtime/spu/spu_workload.c). Lift the raw .bin files with tools/lift_jobmods.py --jobmods DIR --out OUTDIR, which also writes jobmods_register.c. Job modules are keyed differently from ELF images: each registers its lifted functions under its own image id and the module's first 16 bytes, which are matched when the module is streamed into the job code buffer. --merge-chunks (added in PR #386) passes through to the lifter and emits each run of chunks between call targets as one C function, for speed; every chunk stays registered for indirect entry.

Lifted sources are generated output. Lift again after updating ps3recomp or changing the lifter: sources from an older lifter still compile and link, but can disagree with the current runtime.

Code that is not lifted. SPU code with no lifted entry runs in the SPU interpreter (runtime/spu/spu_interp.c), and a branch from interpreted code to a lifted entry resumes the lifted code; see SPU_INTERPRETER_LIFTED_HANDOFF.md. To see what still runs interpreted, set SPU_INTERP_STATS=<seconds>: the interpreter then prints, once per that many seconds, its step rate and the six busiest 1 KB local-store pages by image id.


Phase 12: Testing and Polish

Testing Checklist

  • Game starts without crashes
  • Main menu is functional
  • Gameplay runs at correct speed
  • Audio plays correctly
  • Input is responsive
  • Save data loads and saves
  • Trophy unlocks work
  • No memory leaks (check with AddressSanitizer/Valgrind)
  • Performance is acceptable (profile with host profiling tools)

Performance Tuning

  1. Compile with optimizations — -O2 or /O2 makes a huge difference
  2. Profile — use perf/VTune/Instruments to find hotspots
  3. Batch DMA — if SPU DMA is a bottleneck, consider batching transfers
  4. Reduce logging — disable log_hle_calls for release builds
  5. Memory access patterns — the endian conversion can be a bottleneck; consider caching

Tips and Best Practices

  1. Start with RPCS3 — always verify game behavior in RPCS3 first
  2. Incremental approach — get one screen working, then the next
  3. Version control everything — commit early and often
  4. Document your findings — each game has quirks; write them down
  5. Check RPCS3's game compatibility wiki — known issues are documented
  6. Join the community — other porters may have solved your problem
  7. Don't fight the lifter — if generated code is wrong, fix the lifter, not individual functions (unless it's truly a one-off issue)
  8. Test on multiple platforms — if you're only on Windows, test on Linux too (or vice versa)

Case Study: flOw

The first game port actively in progress using ps3recomp. This section tracks real-world progress and lessons learned.

Game: flOw by thatgamecompany (NPUA80001) Engine: PhyreEngine (Sony first-party) Repository: sp00nznet/flow

Why flOw?

  • Small PSN title (~10 MB ELF, ~50 MB total)
  • Simple gameplay (2D physics-based, no complex 3D)
  • Works perfectly in RPCS3 (behavior reference available)
  • Limited SPU usage (audio only, no physics/compute)
  • 140 imports across 12 modules — excellent ps3recomp coverage

Current Status (v0.4.0)

Milestone Status
ELF analysis & import resolution Complete — 140/140 NIDs resolved
Function boundary detection Complete — 51,658 functions found (OPD + heuristic)
PPU code lifting Complete — all 51,658 functions lifted to C++
Build & link Complete — 37 MB native x86-64 executable
CRT startup Working — TLS init, mutex creation, enters game code
HLE module bridges 7/12 real — cellSysutil, cellGcmSys, cellAudio, cellPad, cellFs, cellSysmodule, sysPrxForUser
Game main loop In progress — crashes in early init due to address sign-extension
Graphics (RSX → D3D12) Not started
Audio (cellAudio → WASAPI) Wired — real WASAPI backend via ps3recomp
Input (cellPad → XInput) Wired — real XInput backend via ps3recomp

Key Technical Lessons

1. TOC Save in Import Stubs

The PPC64 ELF ABI requires saving the TOC (r2) to sp+40 before inter-module calls. The lifter doesn't emit this instruction, so after an import stub call, the caller's ld r2, 40(r1) reads garbage. Fix: The nid_dispatch() function in import_stubs.cpp saves the current TOC:

static void nid_dispatch(ppu_context* ctx, uint32_t nid, const char* name) {
    vm_write64((uint32_t)ctx->gpr[1] + 0x28, ctx->gpr[2]); /* save TOC */
    void* handler = ps3_resolve_func_nid(nid);
    if (handler) ((int64_t(*)(ppu_context*))handler)(ctx);
}

2. HLE Bridge Pattern

ps3recomp's HLE functions take explicit C parameters, but recompiled code passes everything through ppu_context GPRs. Bridge functions extract parameters per the PPC64 ABI (r3-r10 = args, r3 = return), translate guest pointers (vm_base + addr), and byte-swap struct output for big-endian guest memory:

static int64_t bridge_cellVideoOutGetResolution(ppu_context* ctx) {
    uint32_t resId    = (uint32_t)ctx->gpr[3];
    uint32_t res_addr = (uint32_t)ctx->gpr[4];
    CellVideoOutResolution host_res;
    s32 rc = cellVideoOutGetResolution(resId, &host_res);
    if (rc == CELL_OK && res_addr) {
        vm_write16(res_addr,     host_res.width);   /* big-endian write */
        vm_write16(res_addr + 2, host_res.height);
    }
    ctx->gpr[3] = (uint64_t)(int64_t)rc;
    return rc;
}

3. 32-bit vs 64-bit Address Space

The PS3 uses 32-bit effective addresses in 64-bit GPRs. Arithmetic like sp -= 0xA0 can sign-extend the result to 64 bits. The vm_bridge.cpp truncates addresses with (uint32_t)addr, but code that uses GPRs for address comparison or branching may need masking in the lifter.

4. WASAPI COM GUIDs

When linking cellAudio's WASAPI backend on MSVC, the COM interface GUIDs (IID_IAudioClient, etc.) aren't automatically defined. Fix: Define them manually in cellAudio.c using DEFINE_GUID() macros rather than relying on uuid.lib.

Analysis Numbers

Binary:          EBOOT.elf (10.1 MB, ELF64 big-endian PowerPC64)
Functions found: 51,658 (36,827 OPD + 14,831 heuristic)
Functions lifted: 51,658 (100%)
Generated C++:   156 MB (3.14 million lines)
Native binary:   37 MB (x86-64 Windows)
Import libraries: 12 (cellSysutil, cellGcmSys, cellSysmodule, cellSpurs,
                      cellAudio, cellSync, cellNetCtl, sceNp, sys_net,
                      sys_io, sys_fs, sysPrxForUser)
Import functions: 140 (all resolved)
Empty stubs:     16,718 (branch targets not in function list)
Unlifted insns:  ~24,660 (VMX/AltiVec SIMD — needed for PhyreEngine)