ps2-recomp
RustLLVMMIPSStatic recompilation of a PlayStation 2 game to a native executable
A from-scratch static recompiler and runtime that lifts PlayStation 2 machine code — EE (R5900 MIPS), IOP (R3000A), and VU microcode — to LLVM IR and compiles it to a native cross-platform executable. No JIT, no interpreter on the hot path.
~50k LOC
Rust
8,497
Functions mapped
macOS · Linux · Windows
Targets
From MIPS to LLVM IR to native
Every guest function is decoded, lifted instruction-by-instruction into LLVM IR against a guest-context struct, verified, and compiled straight to a native object. Scroll to drive the pipeline on a demo function:
$ ps2recomp emit-ll --elf demo.elf --map demo_funcs.csv --addr 0x100000
view the full emitted module (425 lines, verbatim recompiler output)
; ModuleID = 'dbg'
source_filename = "dbg"
%Context = type { [32 x <2 x i64>], <2 x i64>, <2 x i64>, [32 x i32], i32, i32, i32, i32, i32, ptr, [32 x <2 x i64>], [32 x i32], <2 x i64>, [32 x i32], [256 x <2 x i64>], ptr, i64, [32 x <2 x i64>], <2 x i64>, [1024 x <2 x i64>], [16 x i32], i32, i32, i32, i32, i32, i32, i32, i32, i32, [16384 x i8] }
define void @func_00100000(ptr %0) {
entry:
%sf = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 7
%pc = load i32, ptr %sf, align 4
switch i32 %pc, label %resume_default [
i32 1048576, label %bb_00100000
i32 1048580, label %bb_00100004
i32 1048584, label %bb_00100008
i32 1048588, label %bb_0010000c
i32 1048592, label %bb_00100010
i32 1048596, label %bb_00100014
i32 1048600, label %bb_00100018
i32 1048604, label %bb_0010001c
i32 1048608, label %bb_00100020
i32 1048612, label %bb_00100024
i32 1048616, label %bb_00100028
i32 1048620, label %bb_0010002c
i32 1048624, label %bb_00100030
i32 1048628, label %bb_00100034
i32 1048632, label %bb_00100038
i32 1048636, label %bb_0010003c
i32 1048640, label %bb_00100040
i32 1048644, label %bb_00100044
i32 1048648, label %bb_00100048
]
resume_default: ; preds = %entry
ret void
bb_00100000: ; preds = %entry
%gpr = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 29
%g32 = load i32, ptr %gpr, align 4
%addiu = add i32 %g32, -32
%sext = sext i32 %addiu to i64
%gpr1 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 29
store i64 %sext, ptr %gpr1, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_00100004
bb_00100004: ; preds = %bb_00100000, %entry
%gpr2 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 29
%g323 = load i32, ptr %gpr2, align 4
%addr = add i32 %g323, 28
%gpr4 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 16
%g64 = load i64, ptr %gpr4, align 4
%tn = trunc i64 %g64 to i32
%sprwin = and i32 %addr, -16384
%isspr = icmp eq i32 %sprwin, 1879048192
%phys = and i32 %addr, 536870911
%physhi = lshr i32 %phys, 28
%inio = icmp eq i32 %physhi, 1
%notspr = xor i1 %isspr, true
%isio = and i1 %inio, %notspr
br i1 %isio, label %st_io, label %st_mem
bb_00100008: ; preds = %st_cont, %entry
%gpr9 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 4
%g3210 = load i32, ptr %gpr9, align 4
%addr11 = add i32 %g3210, 16
%sprwin12 = and i32 %addr11, -16384
%isspr13 = icmp eq i32 %sprwin12, 1879048192
%phys14 = and i32 %addr11, 536870911
%physhi15 = lshr i32 %phys14, 28
%inio16 = icmp eq i32 %physhi15, 1
%notspr17 = xor i1 %isspr13, true
%isio18 = and i1 %inio16, %notspr17
br i1 %isio18, label %ld_io, label %ld_mem
bb_0010000c: ; preds = %ld_cont, %entry
%gpr37 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 9
store i64 0, ptr %gpr37, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_00100010
bb_00100010: ; preds = %bb_0010000c, %entry
%gpr38 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 4
%g3239 = load i32, ptr %gpr38, align 4
%addiu40 = add i32 %g3239, 32
%sext41 = sext i32 %addiu40 to i64
%gpr42 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 10
store i64 %sext41, ptr %gpr42, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_00100014
bb_00100014: ; preds = %bb_00100024, %bb_00100010, %entry
%gpr43 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 10
%g3244 = load i32, ptr %gpr43, align 4
%addr45 = add i32 %g3244, 0
%sprwin46 = and i32 %addr45, -16384
%isspr47 = icmp eq i32 %sprwin46, 1879048192
%phys48 = and i32 %addr45, 536870911
%physhi49 = lshr i32 %phys48, 28
%inio50 = icmp eq i32 %physhi49, 1
%notspr51 = xor i1 %isspr47, true
%isio52 = and i1 %inio50, %notspr51
br i1 %isio52, label %ld_io53, label %ld_mem54
bb_00100018: ; preds = %ld_cont55, %entry
%gpr79 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 9
%g3280 = load i32, ptr %gpr79, align 4
%gpr81 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 11
%g3282 = load i32, ptr %gpr81, align 4
%addu = add i32 %g3280, %g3282
%sext83 = sext i32 %addu to i64
%gpr84 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 9
store i64 %sext83, ptr %gpr84, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_0010001c
bb_0010001c: ; preds = %bb_00100018, %entry
%gpr85 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 10
%g3286 = load i32, ptr %gpr85, align 4
%addiu87 = add i32 %g3286, 4
%sext88 = sext i32 %addiu87 to i64
%gpr89 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 10
store i64 %sext88, ptr %gpr89, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_00100020
bb_00100020: ; preds = %bb_0010001c, %entry
%gpr90 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 8
%g3291 = load i32, ptr %gpr90, align 4
%addiu92 = add i32 %g3291, -1
%sext93 = sext i32 %addiu92 to i64
%gpr94 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 8
store i64 %sext93, ptr %gpr94, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_00100024
bb_00100024: ; preds = %bb_00100020, %entry
call void @rt_advance_count(ptr %0, i32 2)
%gpr95 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 8
%g6496 = load i64, ptr %gpr95, align 4
%bne = icmp ne i64 %g6496, 0
br i1 %bne, label %bb_00100014, label %bb_0010002c
bb_00100028: ; preds = %entry
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_0010002c
bb_0010002c: ; preds = %bb_00100028, %bb_00100024, %entry
%gpr97 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 9
%g3298 = load i32, ptr %gpr97, align 4
%sll = shl i32 %g3298, 1
%sext99 = sext i32 %sll to i64
%gpr100 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 9
store i64 %sext99, ptr %gpr100, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_00100030
bb_00100030: ; preds = %bb_0010002c, %entry
%gpr101 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 12
store i64 3473408, ptr %gpr101, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_00100034
bb_00100034: ; preds = %bb_00100030, %entry
%gpr102 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 12
%g32103 = load i32, ptr %gpr102, align 4
%addr104 = add i32 %g32103, 7232
%gpr105 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 9
%g64106 = load i64, ptr %gpr105, align 4
%tn107 = trunc i64 %g64106 to i32
%sprwin108 = and i32 %addr104, -16384
%isspr109 = icmp eq i32 %sprwin108, 1879048192
%phys110 = and i32 %addr104, 536870911
%physhi111 = lshr i32 %phys110, 28
%inio112 = icmp eq i32 %physhi111, 1
%notspr113 = xor i1 %isspr109, true
%isio114 = and i1 %inio112, %notspr113
br i1 %isio114, label %st_io115, label %st_mem116
bb_00100038: ; preds = %st_cont117, %entry
%gpr136 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 9
%g32137 = load i32, ptr %gpr136, align 4
%addu138 = add i32 0, %g32137
%sext139 = sext i32 %addu138 to i64
%gpr140 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 2
store i64 %sext139, ptr %gpr140, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_0010003c
bb_0010003c: ; preds = %bb_00100038, %entry
%gpr141 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 29
%g32142 = load i32, ptr %gpr141, align 4
%addr143 = add i32 %g32142, 28
%sprwin144 = and i32 %addr143, -16384
%isspr145 = icmp eq i32 %sprwin144, 1879048192
%phys146 = and i32 %addr143, 536870911
%physhi147 = lshr i32 %phys146, 28
%inio148 = icmp eq i32 %physhi147, 1
%notspr149 = xor i1 %isspr145, true
%isio150 = and i1 %inio148, %notspr149
br i1 %isio150, label %ld_io151, label %ld_mem152
bb_00100040: ; preds = %ld_cont153, %entry
%gpr177 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 29
%g32178 = load i32, ptr %gpr177, align 4
%addiu179 = add i32 %g32178, 32
%sext180 = sext i32 %addiu179 to i64
%gpr181 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 29
store i64 %sext180, ptr %gpr181, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_00100044
bb_00100044: ; preds = %bb_00100040, %entry
call void @rt_advance_count(ptr %0, i32 2)
%gpr182 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 31
%g32183 = load i32, ptr %gpr182, align 4
%sf184 = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 7
store i32 %g32183, ptr %sf184, align 4
ret void
bb_00100048: ; preds = %entry
call void @rt_advance_count(ptr %0, i32 1)
br label %tail_0010004c
st_io: ; preds = %bb_00100004
%stz = zext i32 %tn to i64
call void @rt_store32(ptr %0, i32 %addr, i64 %stz)
br label %st_cont
st_mem: ; preds = %bb_00100004
%eaa = and i32 %addr, -1
%sprwin5 = and i32 %addr, -16384
%isspr6 = icmp eq i32 %sprwin5, 1879048192
br i1 %isspr6, label %ea_spr, label %ea_ram
st_cont: ; preds = %ea_cont, %st_io
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_00100008
ea_spr: ; preds = %st_mem
%sproff = and i32 %eaa, 16383
%sproffz = zext i32 %sproff to i64
%sf7 = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 15
%spr = load ptr, ptr %sf7, align 8
%sprmem = getelementptr inbounds i8, ptr %spr, i64 %sproffz
br label %ea_cont
ea_ram: ; preds = %st_mem
%ramoff = and i32 %eaa, 33554431
%ramoffz = zext i32 %ramoff to i64
%sf8 = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 9
%ram = load ptr, ptr %sf8, align 8
%rammem = getelementptr inbounds i8, ptr %ram, i64 %ramoffz
br label %ea_cont
ea_cont: ; preds = %ea_ram, %ea_spr
%eaptr = phi ptr [ %sprmem, %ea_spr ], [ %rammem, %ea_ram ]
store i32 %tn, ptr %eaptr, align 4
br label %st_cont
ld_io: ; preds = %bb_00100008
%ioload = call i64 @rt_load32(ptr %0, i32 %addr11)
%iotr = trunc i64 %ioload to i32
br label %ld_cont
ld_mem: ; preds = %bb_00100008
%eaa19 = and i32 %addr11, -1
%sprwin20 = and i32 %addr11, -16384
%isspr21 = icmp eq i32 %sprwin20, 1879048192
br i1 %isspr21, label %ea_spr22, label %ea_ram23
ld_cont: ; preds = %ea_cont24, %ld_io
%ldv = phi i32 [ %iotr, %ld_io ], [ %ld, %ea_cont24 ]
%sx = sext i32 %ldv to i64
%gpr36 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 8
store i64 %sx, ptr %gpr36, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_0010000c
ea_spr22: ; preds = %ld_mem
%sproff25 = and i32 %eaa19, 16383
%sproffz26 = zext i32 %sproff25 to i64
%sf27 = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 15
%spr28 = load ptr, ptr %sf27, align 8
%sprmem29 = getelementptr inbounds i8, ptr %spr28, i64 %sproffz26
br label %ea_cont24
ea_ram23: ; preds = %ld_mem
%ramoff30 = and i32 %eaa19, 33554431
%ramoffz31 = zext i32 %ramoff30 to i64
%sf32 = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 9
%ram33 = load ptr, ptr %sf32, align 8
%rammem34 = getelementptr inbounds i8, ptr %ram33, i64 %ramoffz31
br label %ea_cont24
ea_cont24: ; preds = %ea_ram23, %ea_spr22
%eaptr35 = phi ptr [ %sprmem29, %ea_spr22 ], [ %rammem34, %ea_ram23 ]
%ld = load i32, ptr %eaptr35, align 4
br label %ld_cont
ld_io53: ; preds = %bb_00100014
%ioload56 = call i64 @rt_load32(ptr %0, i32 %addr45)
%iotr57 = trunc i64 %ioload56 to i32
br label %ld_cont55
ld_mem54: ; preds = %bb_00100014
%eaa58 = and i32 %addr45, -1
%sprwin59 = and i32 %addr45, -16384
%isspr60 = icmp eq i32 %sprwin59, 1879048192
br i1 %isspr60, label %ea_spr61, label %ea_ram62
ld_cont55: ; preds = %ea_cont63, %ld_io53
%ldv76 = phi i32 [ %iotr57, %ld_io53 ], [ %ld75, %ea_cont63 ]
%sx77 = sext i32 %ldv76 to i64
%gpr78 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 11
store i64 %sx77, ptr %gpr78, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_00100018
ea_spr61: ; preds = %ld_mem54
%sproff64 = and i32 %eaa58, 16383
%sproffz65 = zext i32 %sproff64 to i64
%sf66 = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 15
%spr67 = load ptr, ptr %sf66, align 8
%sprmem68 = getelementptr inbounds i8, ptr %spr67, i64 %sproffz65
br label %ea_cont63
ea_ram62: ; preds = %ld_mem54
%ramoff69 = and i32 %eaa58, 33554431
%ramoffz70 = zext i32 %ramoff69 to i64
%sf71 = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 9
%ram72 = load ptr, ptr %sf71, align 8
%rammem73 = getelementptr inbounds i8, ptr %ram72, i64 %ramoffz70
br label %ea_cont63
ea_cont63: ; preds = %ea_ram62, %ea_spr61
%eaptr74 = phi ptr [ %sprmem68, %ea_spr61 ], [ %rammem73, %ea_ram62 ]
%ld75 = load i32, ptr %eaptr74, align 4
br label %ld_cont55
st_io115: ; preds = %bb_00100034
%stz118 = zext i32 %tn107 to i64
call void @rt_store32(ptr %0, i32 %addr104, i64 %stz118)
br label %st_cont117
st_mem116: ; preds = %bb_00100034
%eaa119 = and i32 %addr104, -1
%sprwin120 = and i32 %addr104, -16384
%isspr121 = icmp eq i32 %sprwin120, 1879048192
br i1 %isspr121, label %ea_spr122, label %ea_ram123
st_cont117: ; preds = %ea_cont124, %st_io115
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_00100038
ea_spr122: ; preds = %st_mem116
%sproff125 = and i32 %eaa119, 16383
%sproffz126 = zext i32 %sproff125 to i64
%sf127 = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 15
%spr128 = load ptr, ptr %sf127, align 8
%sprmem129 = getelementptr inbounds i8, ptr %spr128, i64 %sproffz126
br label %ea_cont124
ea_ram123: ; preds = %st_mem116
%ramoff130 = and i32 %eaa119, 33554431
%ramoffz131 = zext i32 %ramoff130 to i64
%sf132 = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 9
%ram133 = load ptr, ptr %sf132, align 8
%rammem134 = getelementptr inbounds i8, ptr %ram133, i64 %ramoffz131
br label %ea_cont124
ea_cont124: ; preds = %ea_ram123, %ea_spr122
%eaptr135 = phi ptr [ %sprmem129, %ea_spr122 ], [ %rammem134, %ea_ram123 ]
store i32 %tn107, ptr %eaptr135, align 4
br label %st_cont117
ld_io151: ; preds = %bb_0010003c
%ioload154 = call i64 @rt_load32(ptr %0, i32 %addr143)
%iotr155 = trunc i64 %ioload154 to i32
br label %ld_cont153
ld_mem152: ; preds = %bb_0010003c
%eaa156 = and i32 %addr143, -1
%sprwin157 = and i32 %addr143, -16384
%isspr158 = icmp eq i32 %sprwin157, 1879048192
br i1 %isspr158, label %ea_spr159, label %ea_ram160
ld_cont153: ; preds = %ea_cont161, %ld_io151
%ldv174 = phi i32 [ %iotr155, %ld_io151 ], [ %ld173, %ea_cont161 ]
%sx175 = sext i32 %ldv174 to i64
%gpr176 = getelementptr inbounds %Context, ptr %0, i32 0, i32 0, i32 16
store i64 %sx175, ptr %gpr176, align 4
call void @rt_advance_count(ptr %0, i32 1)
br label %bb_00100040
ea_spr159: ; preds = %ld_mem152
%sproff162 = and i32 %eaa156, 16383
%sproffz163 = zext i32 %sproff162 to i64
%sf164 = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 15
%spr165 = load ptr, ptr %sf164, align 8
%sprmem166 = getelementptr inbounds i8, ptr %spr165, i64 %sproffz163
br label %ea_cont161
ea_ram160: ; preds = %ld_mem152
%ramoff167 = and i32 %eaa156, 33554431
%ramoffz168 = zext i32 %ramoff167 to i64
%sf169 = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 9
%ram170 = load ptr, ptr %sf169, align 8
%rammem171 = getelementptr inbounds i8, ptr %ram170, i64 %ramoffz168
br label %ea_cont161
ea_cont161: ; preds = %ea_ram160, %ea_spr159
%eaptr172 = phi ptr [ %sprmem166, %ea_spr159 ], [ %rammem171, %ea_ram160 ]
%ld173 = load i32, ptr %eaptr172, align 4
br label %ld_cont153
tail_0010004c: ; preds = %bb_00100048
%sf185 = getelementptr inbounds nuw %Context, ptr %0, i32 0, i32 7
store i32 1048652, ptr %sf185, align 4
ret void
}
declare void @rt_advance_count(ptr, i32)
declare void @rt_store32(ptr, i32, i64)
declare i64 @rt_load32(ptr, i32)
The assembly is a hand-written demo function — not game code. The LLVM IR shown is excerpted verbatim from the recompiler's emit-ll output for those exact instruction words (full module above); the binary stage is illustrative.
Highlights
- 01Lifts every guest PS2 function to LLVM IR in-process via inkwell (LLVM 21), verifies each module, and emits PC native objects directly.
- 02CPU-parameterized lifter shared between the R5900 EE core and R3000A IOP, including 128-bit MMI SIMD, exact PS2 non-IEEE float semantics, and VU microcode.
- 03Floating-point and SIMD behavior validated bit-exact against a real PS2 running a custom oracle daemon over LAN.
- 04Graphics Synthesizer reimplemented as a true hardware renderer on the SDL3 GPU API (Metal, Vulkan, D3D12) rather than a software rasterizer.
- 05~50,000 lines of Rust across a 10-crate workspace: recompiler, generic LLVM backend, runtime, and host layers.
