diff --git a/.gitignore b/.gitignore
index 4712504..a1a87fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,9 @@
 # Input elf files
 *.elf
 
+# Output C files
+out/
+
 # Linux build output
 build/
 *.o
diff --git a/RecompPort.vcxproj b/RecompPort.vcxproj
index 9ba36e6..6978eca 100644
--- a/RecompPort.vcxproj
+++ b/RecompPort.vcxproj
@@ -22,6 +22,7 @@
     16.0
     {23C26E84-DC01-43A6-B11B-0B4A2D79A5DD}
     Win32Proj
+    10.0
   
   
   
@@ -75,8 +76,9 @@
       Level3
       ProgramDatabase
       Disabled
-      stdcpp17
-      $(SolutionDir)lib\rabbitizer\include;$(SolutionDir)lib\rabbitizer\cplusplus\include;$(SolutionDir)lib\ELFIO;$(SolutionDir)lib\fmt\include;%(AdditionalIncludeDirectories)
+      stdcpp20
+      $(SolutionDir)lib\rabbitizer\include;$(SolutionDir)lib\rabbitizer\cplusplus\include;$(SolutionDir)lib\ELFIO;$(SolutionDir)lib\fmt\include;$(ProjectDir)include;%(AdditionalIncludeDirectories)
+      true
     
     
       MachineX86
@@ -91,8 +93,9 @@
       MultiThreadedDLL
       Level3
       ProgramDatabase
-      stdcpp17
-      $(SolutionDir)lib\rabbitizer\include;$(SolutionDir)lib\rabbitizer\cplusplus\include;$(SolutionDir)lib\ELFIO;$(SolutionDir)lib\fmt\include;%(AdditionalIncludeDirectories)
+      stdcpp20
+      $(SolutionDir)lib\rabbitizer\include;$(SolutionDir)lib\rabbitizer\cplusplus\include;$(SolutionDir)lib\ELFIO;$(SolutionDir)lib\fmt\include;$(ProjectDir)include;%(AdditionalIncludeDirectories)
+      true
     
     
       MachineX86
@@ -105,8 +108,9 @@
   
   
     
-      stdcpp17
-      $(SolutionDir)lib\rabbitizer\include;$(SolutionDir)lib\rabbitizer\cplusplus\include;$(SolutionDir)lib\ELFIO;$(SolutionDir)lib\fmt\include;%(AdditionalIncludeDirectories)
+      stdcpp20
+      $(SolutionDir)lib\rabbitizer\include;$(SolutionDir)lib\rabbitizer\cplusplus\include;$(SolutionDir)lib\ELFIO;$(SolutionDir)lib\fmt\include;$(ProjectDir)include;%(AdditionalIncludeDirectories)
+      true
     
     
       Console
@@ -115,8 +119,9 @@
   
   
     
-      stdcpp17
-      $(SolutionDir)lib\rabbitizer\include;$(SolutionDir)lib\rabbitizer\cplusplus\include;$(SolutionDir)lib\ELFIO;$(SolutionDir)lib\fmt\include;%(AdditionalIncludeDirectories)
+      stdcpp20
+      $(SolutionDir)lib\rabbitizer\include;$(SolutionDir)lib\rabbitizer\cplusplus\include;$(SolutionDir)lib\ELFIO;$(SolutionDir)lib\fmt\include;$(ProjectDir)include;%(AdditionalIncludeDirectories)
+      true
     
     
       Console
@@ -133,8 +138,10 @@
   
   
     
+    
   
   
+    
     
   
   
diff --git a/RecompPort.vcxproj.filters b/RecompPort.vcxproj.filters
index 4d4d73c..c3edfb2 100644
--- a/RecompPort.vcxproj.filters
+++ b/RecompPort.vcxproj.filters
@@ -18,10 +18,16 @@
     
       Source Files
     
+    
+      Source Files
+    
   
   
     
       Header Files
     
+    
+      Header Files
+    
   
 
\ No newline at end of file
diff --git a/include/recomp_port.h b/include/recomp_port.h
new file mode 100644
index 0000000..6e4d856
--- /dev/null
+++ b/include/recomp_port.h
@@ -0,0 +1,31 @@
+#ifndef __RECOMP_PORT__
+#define __RECOMP_PORT__
+
+#include 
+#include 
+#include 
+
+#ifdef _MSC_VER
+inline uint32_t byteswap(uint32_t val) {
+    return _byteswap_ulong(val);
+}
+#else
+constexpr uint32_t byteswap(uint32_t val) {
+    return __builtin_bswap32(val);
+}
+#endif
+
+namespace RecompPort {
+
+    struct Function {
+        uint32_t vram;
+        uint32_t rom;
+        const std::span words;
+        std::string name;
+    };
+
+
+    bool recompile_function(const Function& func, std::string_view output_path);
+}
+
+#endif
diff --git a/lib/Rabbitizer.vcxproj b/lib/Rabbitizer.vcxproj
index d22a265..38b791e 100644
--- a/lib/Rabbitizer.vcxproj
+++ b/lib/Rabbitizer.vcxproj
@@ -133,15 +133,23 @@
   
   
     true
+    $(SolutionDir)$(Configuration)\rabbitizer_build\
+    $(Configuration)\rabbitizer_build\
   
   
     false
+    $(SolutionDir)$(Configuration)\rabbitizer_build\
+    $(Configuration)\rabbitizer_build\
   
   
     true
+    $(SolutionDir)$(Platform)\$(Configuration)\rabbitizer_build\
+    $(Platform)\$(Configuration)\rabbitizer_build\
   
   
     false
+    $(SolutionDir)$(Platform)\$(Configuration)\rabbitizer_build\
+    $(Platform)\$(Configuration)\rabbitizer_build\
   
   
     
diff --git a/lib/fmtlib.vcxproj b/lib/fmtlib.vcxproj
index b6e558b..4c2cfae 100644
--- a/lib/fmtlib.vcxproj
+++ b/lib/fmtlib.vcxproj
@@ -72,15 +72,23 @@
   
   
     true
+    $(SolutionDir)$(Configuration)\fmtlib_build\
+    $(Configuration)\fmtlib_build\
   
   
     false
+    $(SolutionDir)$(Configuration)\fmtlib_build\
+    $(Configuration)\fmtlib_build\
   
   
     true
+    $(SolutionDir)$(Platform)\$(Configuration)\fmtlib_build\
+    $(Platform)\$(Configuration)\fmtlib_build\
   
   
     false
+    $(SolutionDir)$(Platform)\$(Configuration)\fmtlib_build\
+    $(Platform)\$(Configuration)\fmtlib_build\
   
   
     
diff --git a/recomp.h b/recomp.h
new file mode 100644
index 0000000..675f8ef
--- /dev/null
+++ b/recomp.h
@@ -0,0 +1,94 @@
+#ifndef __RECOMP_H__
+#define __RECOMP_H__
+
+#include 
+
+#define ADD32(a, b) \
+    ((uint64_t)(int32_t)((a) + (b)))
+
+#define SUB32(a, b) \
+    ((uint64_t)(int32_t)((a) - (b)))
+
+#define MEM_D(offset, reg) \
+    (*(int64_t*)((rdram) + (((reg) + (offset)) ^ 3)))
+
+#define MEM_W(offset, reg) \
+    (*(int32_t*)((rdram) + (((reg) + (offset)) ^ 3)))
+
+#define MEM_H(offset, reg) \
+    (*(int16_t*)((rdram) + (((reg) + (offset)) ^ 3)))
+
+#define MEM_B(offset, reg) \
+    (*(int8_t*)((rdram) + (((reg) + (offset)) ^ 3)))
+
+#define MEM_HU(offset, reg) \
+    (*(uint16_t*)((rdram) + (((reg) + (offset)) ^ 3)))
+
+#define MEM_BU(offset, reg) \
+    (*(uint8_t*)((rdram) + (((reg) + (offset)) ^ 3)))
+
+#define S32(val) \
+    ((int32_t)(val))
+    
+#define U32(val) \
+    ((uint32_t)(val))
+
+#define S64(val) \
+    ((int64_t)(val))
+
+#define MUL_S(val1, val2) \
+    ((val1) * (val2))
+
+#define MUL_D(val1, val2) \
+    ((val1) * (val2))
+
+#define DIV_S(val1, val2) \
+    ((val1) / (val2))
+
+#define DIV_D(val1, val2) \
+    ((val1) / (val2))
+
+#define CVT_S_W(val) \
+    ((float)((int32_t)(val)))
+
+#define CVT_D_W(val) \
+    ((double)((int32_t)(val)))
+
+#define CVT_D_S(val) \
+    ((double)(val))
+
+#define CVT_S_D(val) \
+    ((float)(val))
+
+#define TRUNC_W_S(val) \
+    ((int32_t)(val))
+
+#define TRUNC_W_D(val) \
+    ((int32_t)(val))
+
+typedef uint64_t gpr;
+
+typedef union {
+    double d;
+    struct {
+        float fl;
+        float fh;
+    };
+    struct {
+        uint32_t u32l;
+        uint32_t u32h;
+    };
+    uint64_t u64;
+} fpr;
+
+typedef struct {
+    gpr r0,  r1,  r2,  r3,  r4,  r5,  r6,  r7,
+        r8,  r9,  r10, r11, r12, r13, r14, r15,
+        r16, r17, r18, r19, r20, r21, r22, r23,
+        r24, r25, r26, r27, r28, r29, r30, r31;
+    fpr f0,  f2,  f4,  f6,  f8,  f10, f12, f14,
+        f16, f18, f20, f22, f24, f26, f28, f30;
+    uint64_t hi, lo;
+} recomp_context;
+
+#endif
diff --git a/src/main.cpp b/src/main.cpp
index b40653a..b8326f7 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,17 +1,330 @@
+#include 
+#include 
+#include 
+#include 
+
 #include "rabbitizer.hpp"
 #include "elfio/elfio.hpp"
 #include "fmt/format.h"
 
-#include 
+#include "recomp_port.h"
+
+std::unordered_set ignored_funcs {
+    // OS initialize functions
+    "__createSpeedParam",
+    "__osInitialize_common",
+    "__osInitialize_autodetect",
+    "osInitialize",
+    // Audio interface functions
+    "osAiGetLength",
+    "osAiGetStatus",
+    "osAiSetFrequency",
+    "osAiSetNextBuffer",
+    "__osAiDeviceBusy",
+    // Video interface functions
+    "osViBlack",
+    "osViFade",
+    "osViGetCurrentField",
+    "osViGetCurrentFramebuffer",
+    "osViGetCurrentLine",
+    "osViGetCurrentMode",
+    "osViGetNextFramebuffer",
+    "osViGetStatus",
+    "osViRepeatLine",
+    "osViSetEvent",
+    "osViSetMode",
+    "osViSetSpecialFeatures",
+    "osViSetXScale",
+    "osViSetYScale",
+    "osViSwapBuffer",
+    "osCreateViManager",
+    "viMgrMain",
+    "__osViInit",
+    "__osViSwapContext",
+    "__osViGetCurrentContext",
+    // RDP functions
+    "osDpGetCounters",
+    "osDpSetStatus",
+    "osDpGetStatus",
+    "osDpSetNextBuffer",
+    "__osDpDeviceBusy",
+    // RSP functions
+    "osSpTaskLoad",
+    "osSpTaskStartGo",
+    "osSpTaskYield",
+    "osSpTaskYielded",
+    "__osSpDeviceBusy",
+    "__osSpGetStatus",
+    "__osSpRawStartDma",
+    "__osSpRawReadIo",
+    "__osSpRawWriteIo",
+    "__osSpSetPc",
+    "__osSpSetStatus",
+    // Controller functions
+    "osContGetQuery",
+    "osContGetReadData",
+    "osContInit",
+    "osContReset",
+    "osContSetCh",
+    "osContStartQuery",
+    "osContStartReadData",
+    "__osContAddressCrc",
+    "__osContDataCrc",
+    "__osContGetInitData",
+    "__osContRamRead",
+    "__osContRamWrite",
+    // EEPROM functions
+    "osEepromLongRead",
+    "osEepromLongWrite",
+    "osEepromProbe",
+    "osEepromRead",
+    "osEepromWrite",
+    "__osEepStatus",
+    // Rumble functions
+    "osMotorInit",
+    "osMotorStart",
+    "osMotorStop",
+    // PFS functions
+    "osPfsAllocateFile",
+    "osPfsChecker",
+    "osPfsDeleteFile",
+    "osPfsFileState",
+    "osPfsFindFile",
+    "osPfsFreeBlocks",
+    "osPfsGetLabel",
+    "osPfsInit",
+    "osPfsInitPak",
+    "osPfsIsPlug",
+    "osPfsNumFiles",
+    "osPfsRepairId",
+    "osPfsReadWriteFile",
+    "__osPackEepReadData",
+    "__osPackEepWriteData",
+    "__osPackRamReadData",
+    "__osPackRamWriteData",
+    "__osPackReadData",
+    "__osPackRequestData",
+    "__osPfsGetInitData",
+    "__osPfsGetOneChannelData",
+    "__osPfsGetStatus",
+    "__osPfsRequestData",
+    "__osPfsRequestOneChannel",
+    "__osPfsCreateAccessQueue",
+    // Low level serial interface functions
+    "__osSiDeviceBusy",
+    "__osSiGetStatus",
+    "__osSiRawStartDma",
+    "__osSiRawReadIo",
+    "__osSiRawWriteIo",
+    "__osSiCreateAccessQueue",
+    "__osSiGetAccess",
+    "__osSiRelAccess",
+    // Parallel interface (cartridge, DMA, etc.) functions
+    "osCartRomInit",
+    "osLeoDiskInit",
+    "osCreatePiManager",
+    "__osDevMgrMain",
+    "osPiGetCmdQueue",
+    "osPiGetStatus",
+    "osPiReadIo",
+    "osPiStartDma",
+    "osPiWriteIo",
+    "osEPiGetDeviceType",
+    "osEPiStartDma",
+    "osEPiWriteIo",
+    "osEPiReadIo",
+    "osPiRawStartDma",
+    "osPiRawReadIo",
+    "osPiRawWriteIo",
+    "osEPiRawStartDma",
+    "osEPiRawReadIo",
+    "osEPiRawWriteIo",
+    "__osPiRawStartDma",
+    "__osPiRawReadIo",
+    "__osPiRawWriteIo",
+    "__osEPiRawStartDma",
+    "__osEPiRawReadIo",
+    "__osEPiRawWriteIo",
+    "__osPiDeviceBusy",
+    "__osPiCreateAccessQueue",
+    "__osPiGetAccess",
+    "__osPiRelAccess",
+    "__osLeoAbnormalResume",
+    "__osLeoInterrupt",
+    "__osLeoResume",
+    // Threading functions
+    "osCreateThread",
+    "osStartThread",
+    "osStopThread",
+    "osDestroyThread",
+    "osYieldThread",
+    "osSetThreadPri",
+    "osGetThreadPri",
+    "osGetThreadId",
+    "__osDequeueThread",
+    // Message Queue functions
+    "osCreateMesgQueue",
+    "osSendMesg",
+    "osJamMesg",
+    "osRecvMesg",
+    "osSetEventMesg",
+    // Timer functions
+    "osStartTimer",
+    "osSetTimer",
+    "osStopTimer",
+    "__osInsertTimer",
+    "__osTimerInterrupt",
+    "__osTimerServicesInit",
+    "__osSetTimerIntr",
+    // exceptasm functions
+    "__osExceptionPreamble",
+    "__osException",
+    "send_mesg",
+    "handle_CpU",
+    "__osEnqueueAndYield",
+    "__osEnqueueThread",
+    "__osPopThread",
+    "__osNop",
+    "__osDispatchThread",
+    "__osCleanupThread",
+    "osGetCurrFaultedThread",
+    "osGetNextFaultedThread",
+    // interrupt functions
+    "osSetIntMask",
+    "osGetIntMask",
+    "__osDisableInt",
+    "__osRestoreInt",
+    "__osSetGlobalIntMask",
+    "__osResetGlobalIntMask",
+    // TLB functions
+    "osMapTLB",
+    "osUnmapTLB",
+    "osUnmapTLBAll",
+    "osSetTLBASID",
+    "osMapTLBRdb",
+    "osVirtualToPhysical",
+    "__osGetTLBHi",
+    "__osGetTLBLo0",
+    "__osGetTLBLo1",
+    "__osGetTLBPageMask",
+    "__osGetTLBASID",
+    "__osProbeTLB",
+    // Coprocessor 0 functions
+    "__osSetCount",
+    "osGetCount",
+    "__osSetSR",
+    "__osGetSR",
+    "__osSetCause",
+    "__osGetCause",
+    "__osSetCompare",
+    "__osGetCompare",
+    "__osSetConfig",
+    "__osGetConfig",
+    "__osSetWatchLo",
+    "__osGetWatchLo",
+};
 
 int main(int argc, char** argv) {
-    uint32_t word = 0x8D4A7E18; // lw
-    uint32_t vram = 0x80000000;
-    int extraLJust = 5;
-    rabbitizer::InstructionCpu instr(word, vram);
+    if (argc != 2) {
+        fmt::print("Usage: {} [input elf file]\n", argv[0]);
+        std::exit(EXIT_SUCCESS);
+    }
 
-    fmt::print("{}\n", instr.isBranch());
-    fmt::print("{:08X}: {}\n", word, instr.disassemble(extraLJust));
+    ELFIO::elfio elf_file;
+
+    auto exit_failure = [] (const std::string& error_str) {
+        fmt::print(stderr, error_str);
+        std::exit(EXIT_FAILURE);
+    };
+
+    if (!elf_file.load(argv[1])) {
+        exit_failure("Failed to load provided elf file\n");
+    }
+
+    if (elf_file.get_class() != ELFIO::ELFCLASS32) {
+        exit_failure("Incorrect elf class\n");
+    }
+
+    if (elf_file.get_encoding() != ELFIO::ELFDATA2MSB) {
+        exit_failure("Incorrect endianness\n");
+    }
+
+    // Pointer to the symbol table section
+    ELFIO::section* symtab_section = nullptr;
+    // Size of the ROM as determined by the elf
+    ELFIO::Elf_Xword rom_size = 0;
+    // ROM address of each section
+    std::vector section_rom_addrs{};
+    section_rom_addrs.resize(elf_file.sections.size());
+
+    // Iterate over every section to record rom addresses and find the symbol table
+    fmt::print("Sections\n");
+    for (const std::unique_ptr& section : elf_file.sections) {
+        fmt::print("  {}: {} @ 0x{:08X}, 0x{:08X}\n", section->get_index(), section->get_name(), section->get_address(), rom_size);
+        // Set the rom address of this section to the current accumulated ROM size
+        section_rom_addrs[section->get_index()] = rom_size;
+        // If this section isn't bss (SHT_NOBITS) and ends up in the rom (SHF_ALLOC), increase the rom size by this section's size
+        if (section->get_type() != ELFIO::SHT_NOBITS && section->get_flags() & ELFIO::SHF_ALLOC) {
+            rom_size += section->get_size();
+        }
+        // Check if this section is the symbol table and record it if so
+        if (section->get_type() == ELFIO::SHT_SYMTAB) {
+            symtab_section = section.get();
+        }
+    }
+
+    // If no symbol table was found then exit
+    if (symtab_section == nullptr) {
+        exit_failure("No symbol section found\n");
+    }
+
+    ELFIO::symbol_section_accessor symbols{ elf_file, symtab_section };
+
+    fmt::print("Num symbols: {}\n", symbols.get_symbols_num());
+
+    std::vector functions{};
+    functions.reserve(1024);
+
+    for (int sym_index = 0; sym_index < symbols.get_symbols_num(); sym_index++) {
+        std::string   name;
+        ELFIO::Elf64_Addr    value;
+        ELFIO::Elf_Xword     size;
+        unsigned char bind;
+        unsigned char type;
+        ELFIO::Elf_Half      section_index;
+        unsigned char other;
+
+        // Read symbol properties
+        symbols.get_symbol(sym_index, name, value, size, bind, type,
+            section_index, other);
+
+        // Check if this symbol is a function
+        if (type == ELFIO::STT_FUNC) {
+            auto section_rom_addr = section_rom_addrs[section_index];
+            auto section_offset = value - elf_file.sections[section_index]->get_address();
+            const uint32_t* words = reinterpret_cast(elf_file.sections[section_index]->get_data() + section_offset);
+            functions.emplace_back(
+                static_cast(value),
+                static_cast(section_offset + section_rom_addr),
+                std::span{ reinterpret_cast(words), size / 4 },
+                std::move(name)
+            );
+        }
+    }
+
+    fmt::print("Function count: {}\n", functions.size());
+
+    //#pragma omp parallel for
+    for (size_t i = 0; i < functions.size(); i++) {
+        const auto& func = functions[i];
+        if (!ignored_funcs.contains(func.name)) {
+            if (RecompPort::recompile_function(func, "out/" + func.name + ".c") == false) {
+                fmt::print(stderr, "Error recompiling {}\n", func.name);
+                std::exit(EXIT_FAILURE);
+            }
+        }
+    }
+    //RecompPort::recompile_function(functions.back(), "test.c");
 
     return 0;
 }
diff --git a/src/recompilation.cpp b/src/recompilation.cpp
new file mode 100644
index 0000000..ab5e98d
--- /dev/null
+++ b/src/recompilation.cpp
@@ -0,0 +1,719 @@
+#include 
+#include 
+
+#include "rabbitizer.hpp"
+#include "fmt/format.h"
+#include "fmt/ostream.h"
+
+#include "recomp_port.h"
+
+using InstrId = rabbitizer::InstrId::UniqueId;
+
+std::string_view ctx_gpr_prefix(int reg) {
+    if (reg != 0) {
+        return "ctx->r";
+    }
+    return "";
+}
+
+bool process_instruction(size_t instr_index, const std::vector& instructions, std::ofstream& output_file, bool indent, bool emit_link_branch, int link_branch_index, bool& needs_link_branch, bool& is_branch_likely) {
+    const auto& instr = instructions[instr_index];
+    needs_link_branch = false;
+    is_branch_likely = false;
+
+    // Output a comment with the original instruction
+    if (instr.isBranch() || instr.getUniqueId() == InstrId::cpu_j) {
+        fmt::print(output_file, "    // {}\n", instr.disassemble(0, fmt::format("L_{:08X}", (uint32_t)instr.getBranchVramGeneric())));
+    } else if (instr.getUniqueId() == InstrId::cpu_jal) {
+        fmt::print(output_file, "    // {}\n", instr.disassemble(0, "func"));
+    } else {
+        fmt::print(output_file, "    // {}\n", instr.disassemble(0));
+    }
+
+    auto print_indent = [&]() {
+        fmt::print(output_file, "    ");
+    };
+
+    auto print_line = [&](fmt::format_string fmt_str, Ts ...args) {
+        print_indent();
+        fmt::print(output_file, fmt_str, args...);
+        fmt::print(output_file, ";\n");
+    };
+
+    auto print_branch_condition = [&](fmt::format_string fmt_str, Ts ...args) {
+        fmt::print(output_file, fmt_str, args...);
+        fmt::print(output_file, " ");
+    };
+
+    auto print_branch = [&](fmt::format_string fmt_str, Ts ...args) {
+        fmt::print(output_file, "{{\n    ");
+        if (instr_index < instructions.size() - 1) {
+            bool dummy_needs_link_branch;
+            bool dummy_is_branch_likely;
+            process_instruction(instr_index + 1, instructions, output_file, true, false, link_branch_index, dummy_needs_link_branch, dummy_is_branch_likely);
+        }
+        fmt::print(output_file, "        ");
+        fmt::print(output_file, fmt_str, args...);
+        if (needs_link_branch) {
+            fmt::print(output_file, ";\n        goto after_{}", link_branch_index);
+        }
+        fmt::print(output_file, ";\n    }}\n");
+    };
+
+    if (indent) {
+        print_indent();
+    }
+
+    int rd = (int)instr.GetO32_rd();
+    int rs = (int)instr.GetO32_rs();
+    int base = rs;
+    int rt = (int)instr.GetO32_rt();
+    int sa = (int)instr.Get_sa();
+
+    int fd = (int)instr.GetO32_fd();
+    int fs = (int)instr.GetO32_fs();
+    int ft = (int)instr.GetO32_ft();
+
+    uint16_t imm = instr.Get_immediate();
+
+    switch (instr.getUniqueId()) {
+    case InstrId::cpu_nop:
+        fmt::print(output_file, "\n");
+        break;
+    // Arithmetic
+    case InstrId::cpu_lui:
+        print_line("{}{} = {:#X} << 16", ctx_gpr_prefix(rt), rt, imm);
+        break;
+    case InstrId::cpu_addu:
+        print_line("{}{} = ADD32({}{}, {}{})", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_negu: // pseudo instruction for subu x, 0, y
+    case InstrId::cpu_subu:
+        print_line("{}{} = SUB32({}{}, {}{})", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_addiu:
+        print_line("{}{} = ADD32({}{}, {:#X})", ctx_gpr_prefix(rt), rt, ctx_gpr_prefix(rs), rs, (int16_t)imm);
+        break;
+    case InstrId::cpu_and:
+        print_line("{}{} = {}{} & {}{}", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_andi:
+        print_line("{}{} = {}{} & {:#X}", ctx_gpr_prefix(rt), rt, ctx_gpr_prefix(rs), rs, imm);
+        break;
+    case InstrId::cpu_or:
+        print_line("{}{} = {}{} | {}{}", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_ori:
+        print_line("{}{} = {}{} | {:#X}", ctx_gpr_prefix(rt), rt, ctx_gpr_prefix(rs), rs, imm);
+        break;
+    case InstrId::cpu_nor:
+        print_line("{}{} = ~({}{} | {}{})", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_xor:
+        print_line("{}{} = {}{} ^ {}{}", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_xori:
+        print_line("{}{} = {}{} ^ {:#X}", ctx_gpr_prefix(rt), rt, ctx_gpr_prefix(rs), rs, imm);
+        break;
+    case InstrId::cpu_sll:
+        print_line("{}{} = S32({}{}) << {}", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rt), rt, sa);
+        break;
+    case InstrId::cpu_sllv:
+        print_line("{}{} = S32({}{}) << ({}{} & 31)", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rt), rt, ctx_gpr_prefix(rs), rs);
+        break;
+    case InstrId::cpu_sra:
+        print_line("{}{} = S32(S64({}{}) >> {})", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rt), rt, sa);
+        break;
+    case InstrId::cpu_srav:
+        print_line("{}{} = S32(S64({}{}) >> ({}{} & 31)", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rt), rt, ctx_gpr_prefix(rs), rs);
+        break;
+    case InstrId::cpu_srl:
+        print_line("{}{} = S32(U32({}{}) >> {})", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rt), rt, sa);
+        break;
+    case InstrId::cpu_srlv:
+        print_line("{}{} = S32(U32({}{}) >> ({}{} & 31)", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rt), rt, ctx_gpr_prefix(rs), rs);
+        break;
+    case InstrId::cpu_slt:
+        print_line("{}{} = S64({}{}) < S64({}{}) ? 1 : 0", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_slti:
+        print_line("{}{} = S64({}{}) < {:#X} ? 1 : 0", ctx_gpr_prefix(rt), rt, ctx_gpr_prefix(rs), rs, (int16_t)imm);
+        break;
+    case InstrId::cpu_sltu:
+        print_line("{}{} = U64({}{}) < U64({}{}) ? 1 : 0", ctx_gpr_prefix(rd), rd, ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_sltiu:
+        print_line("{}{} = U64({}{}) < {:#X} ? 1 : 0", ctx_gpr_prefix(rt), rt, ctx_gpr_prefix(rs), rs, (int16_t)imm);
+        break;
+    case InstrId::cpu_mult:
+        print_line("uint64_t result = S64({}{}) * S64({}{}); lo = S32(result >> 0); hi = S32(result >> 32)", ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_multu:
+        print_line("uint64_t result = {}{} * {}{}; lo = S32(result >> 0); hi = S32(result >> 32)", ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_div:
+        print_line("lo = S32(S64({}{}) / S64({}{})); hi = S32(S64({}{}) % S64({}{}))", ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt, ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_divu:
+        print_line("lo = S32(U32({}{}) / U32({}{})); hi = S32(U32({}{}) % U32({}{}))", ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt, ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_mflo:
+        print_line("{}{} = lo", ctx_gpr_prefix(rd), rd);
+        break;
+    case InstrId::cpu_mfhi:
+        print_line("{}{} = hi", ctx_gpr_prefix(rd), rd);
+        break;
+    // Loads
+    // TODO ld
+    case InstrId::cpu_lw:
+        print_line("{}{} = MEM_W({:#X}, {}{})", ctx_gpr_prefix(rt), rt, (int16_t)imm, ctx_gpr_prefix(base), base);
+        break;
+    case InstrId::cpu_lh:
+        print_line("{}{} = MEM_H({:#X}, {}{})", ctx_gpr_prefix(rt), rt, (int16_t)imm, ctx_gpr_prefix(base), base);
+        break;
+    case InstrId::cpu_lb:
+        print_line("{}{} = MEM_B({:#X}, {}{})", ctx_gpr_prefix(rt), rt, (int16_t)imm, ctx_gpr_prefix(base), base);
+        break;
+    case InstrId::cpu_lhu:
+        print_line("{}{} = MEM_HU({:#X}, {}{})", ctx_gpr_prefix(rt), rt, (int16_t)imm, ctx_gpr_prefix(base), base);
+        break;
+    case InstrId::cpu_lbu:
+        print_line("{}{} = MEM_BU({:#X}, {}{})", ctx_gpr_prefix(rt), rt, (int16_t)imm, ctx_gpr_prefix(base), base);
+        break;
+    // Stores
+    case InstrId::cpu_sw:
+        print_line("MEM_W({:#X}, {}{}) = {}{}", (int16_t)imm, ctx_gpr_prefix(base), base, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_sh:
+        print_line("MEM_H({:#X}, {}{}) = {}{}", (int16_t)imm, ctx_gpr_prefix(base), base, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_sb:
+        print_line("MEM_B({:#X}, {}{}) = {}{}", (int16_t)imm, ctx_gpr_prefix(base), base, ctx_gpr_prefix(rt), rt);
+        break;
+    // TODO lwl, lwr
+    // examples:
+    // reg =        11111111 01234567
+    // mem @ x =             89ABCDEF
+
+    // LWL x + 0 -> FFFFFFFF 89ABCDEF
+    // LWL x + 1 -> FFFFFFFF ABCDEF67
+    // LWL x + 2 -> FFFFFFFF CDEF4567
+    // LWL x + 3 -> FFFFFFFF EF234567
+
+    // LWR x + 0 -> 00000000 01234589
+    // LWR x + 1 -> 00000000 012389AB
+    // LWR x + 2 -> 00000000 0189ABCD
+    // LWR x + 3 -> FFFFFFFF 89ABCDEF
+    case InstrId::cpu_lwl:
+        print_line("{}{} = MEM_WL({:#X}, {}{})", ctx_gpr_prefix(rt), rt, (int16_t)imm, ctx_gpr_prefix(base), base);
+        break;
+    case InstrId::cpu_lwr:
+        print_line("{}{} = MEM_WR({:#X}, {}{})", ctx_gpr_prefix(rt), rt, (int16_t)imm, ctx_gpr_prefix(base), base);
+        break;
+    case InstrId::cpu_swl:
+        print_line("MEM_WL({:#X}, {}{}) = {}{}", (int16_t)imm, ctx_gpr_prefix(base), base, ctx_gpr_prefix(rt), rt);
+        break;
+    case InstrId::cpu_swr:
+        print_line("MEM_WR({:#X}, {}{}) = {}{}", (int16_t)imm, ctx_gpr_prefix(base), base, ctx_gpr_prefix(rt), rt);
+        break;
+        
+    // Branches
+    case InstrId::cpu_jal:
+        needs_link_branch = true;
+        print_indent();
+        // TODO lookup function name
+        print_branch("{}(rdram, ctx)", "func");
+        break;
+    case InstrId::cpu_jalr:
+        needs_link_branch = true;
+        print_indent();
+        // TODO index global function table
+        print_branch("{}(rdram, ctx)", "func_reg");
+        break;
+    case InstrId::cpu_j:
+    case InstrId::cpu_b:
+        print_indent();
+        print_branch("goto L_{:08X}", (uint32_t)instr.getBranchVramGeneric());
+        break;
+    case InstrId::cpu_jr:
+        print_indent();
+        if (rs == (int)rabbitizer::Registers::Cpu::GprO32::GPR_O32_ra) {
+            print_branch("return");
+        } else {
+            // TODO jump table handling
+        }
+        break;
+    case InstrId::cpu_bnel:
+        is_branch_likely = true;
+        [[fallthrough]];
+    case InstrId::cpu_bne:
+        print_indent();
+        print_branch_condition("if (S32({}{}) != S32({}{}))", ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        print_branch("goto L_{:08X}", (uint32_t)instr.getBranchVramGeneric());
+        break;
+    case InstrId::cpu_beql:
+        is_branch_likely = true;
+        [[fallthrough]];
+    case InstrId::cpu_beq:
+        print_indent();
+        print_branch_condition("if (S32({}{}) == S32({}{}))", ctx_gpr_prefix(rs), rs, ctx_gpr_prefix(rt), rt);
+        print_branch("goto L_{:08X}", (uint32_t)instr.getBranchVramGeneric());
+        break;
+    case InstrId::cpu_bnez:
+        print_indent();
+        print_branch_condition("if (S32({}{}) != 0)", ctx_gpr_prefix(rs), rs);
+        print_branch("goto L_{:08X}", (uint32_t)instr.getBranchVramGeneric());
+        break;
+    case InstrId::cpu_beqz:
+        print_indent();
+        print_branch_condition("if (S32({}{}) == 0)", ctx_gpr_prefix(rs), rs);
+        print_branch("goto L_{:08X}", (uint32_t)instr.getBranchVramGeneric());
+        break;
+    case InstrId::cpu_bgezl:
+        is_branch_likely = true;
+        [[fallthrough]];
+    case InstrId::cpu_bgez:
+        print_indent();
+        print_branch_condition("if (S32({}{}) >= 0)", ctx_gpr_prefix(rs), rs);
+        print_branch("goto L_{:08X}", (uint32_t)instr.getBranchVramGeneric());
+        break;
+    case InstrId::cpu_bgtzl:
+        is_branch_likely = true;
+        [[fallthrough]];
+    case InstrId::cpu_bgtz:
+        print_indent();
+        print_branch_condition("if (S32({}{}) > 0)", ctx_gpr_prefix(rs), rs);
+        print_branch("goto L_{:08X}", (uint32_t)instr.getBranchVramGeneric());
+        break;
+    case InstrId::cpu_blezl:
+        is_branch_likely = true;
+        [[fallthrough]];
+    case InstrId::cpu_blez:
+        print_indent();
+        print_branch_condition("if (S32({}{}) <= 0)", ctx_gpr_prefix(rs), rs);
+        print_branch("goto L_{:08X}", (uint32_t)instr.getBranchVramGeneric());
+        break;
+    case InstrId::cpu_bltzl:
+        is_branch_likely = true;
+        [[fallthrough]];
+    case InstrId::cpu_bltz:
+        print_indent();
+        print_branch_condition("if (S32({}{}) < 0)", ctx_gpr_prefix(rs), rs);
+        print_branch("goto L_{:08X}", (uint32_t)instr.getBranchVramGeneric());
+        break;
+    case InstrId::cpu_break:
+        print_line("do_break();");
+        break;
+
+    // Cop1 loads/stores
+    case InstrId::cpu_mtc1:
+        if ((fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.u32l = {}{}", fs, ctx_gpr_prefix(rt), rt);
+        }
+        else {
+            // odd fpr
+            print_line("ctx->f{}.u32h = {}{}", fs - 1, ctx_gpr_prefix(rt), rt);
+        }
+        break;
+    case InstrId::cpu_mfc1:
+        if ((fs & 1) == 0) {
+            // even fpr
+            print_line("{}{} = ctx->f{}.u32l", ctx_gpr_prefix(rt), rt, fs);
+        } else {
+            // odd fpr
+            print_line("{}{} = ctx->f{}.u32h", ctx_gpr_prefix(rt), rt, fs - 1);
+        }
+        break;
+    case InstrId::cpu_lwc1:
+        if ((ft & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.u32l = MEM_W({:#X}, {}{})", ft, (int16_t)imm, ctx_gpr_prefix(base), base);
+        } else {
+            // odd fpr
+            print_line("ctx->f{}.u32h = MEM_W({:#X}, {}{})", ft - 1, (int16_t)imm, ctx_gpr_prefix(base), base);
+        }
+        break;
+    case InstrId::cpu_ldc1:
+        if ((ft & 1) == 0) {
+            print_line("ctx->f{}.u64 = MEM_D({:#X}, {}{})", ft, (int16_t)imm, ctx_gpr_prefix(base), base);
+        } else {
+            fmt::print(stderr, "Invalid operand for ldc1: f{}\n", ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_swc1:
+        if ((ft & 1) == 0) {
+            // even fpr
+            print_line("MEM_W({:#X}, {}{}) = ctx->f{}.u32l", (int16_t)imm, ctx_gpr_prefix(base), base, ft);
+        } else {
+            // odd fpr
+            print_line("MEM_W({:#X}, {}{}) = ctx->f{}.u32h", (int16_t)imm, ctx_gpr_prefix(base), base, ft - 1);
+        }
+        break;
+    case InstrId::cpu_sdc1:
+        if ((ft & 1) == 0) {
+            print_line("MEM_D({:#X}, {}{}) = ctx->f{}.u64", (int16_t)imm, ctx_gpr_prefix(base), base, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand for sdc1: f{}\n", ft);
+            return false;
+        }
+        break;
+
+    // Cop1 compares
+    case InstrId::cpu_c_lt_s:
+        if ((fs & 1) == 0 && (ft & 1) == 0) {
+            print_line("c1cs = ctx->f{}.fl <= ctx->f{}.fl", fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand for c.lt.s: f{} f{}\n", fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_c_lt_d:
+        if ((fs & 1) == 0 && (ft & 1) == 0) {
+            print_line("c1cs = ctx->f{}.d <= ctx->f{}.d", fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand for c.lt.d: f{} f{}\n", fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_c_le_s:
+        if ((fs & 1) == 0 && (ft & 1) == 0) {
+            print_line("c1cs = ctx->f{}.fl <= ctx->f{}.fl", fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand for c.le.s: f{} f{}\n", fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_c_le_d:
+        if ((fs & 1) == 0 && (ft & 1) == 0) {
+            print_line("c1cs = ctx->f{}.d <= ctx->f{}.d", fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand for c.le.d: f{} f{}\n", fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_c_eq_s:
+        if ((fs & 1) == 0 && (ft & 1) == 0) {
+            print_line("c1cs = ctx->f{}.fl == ctx->f{}.fl", fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand for c.eq.s: f{} f{}\n", fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_c_eq_d:
+        if ((fs & 1) == 0 && (ft & 1) == 0) {
+            print_line("c1cs = ctx->f{}.d == ctx->f{}.d", fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand for c.eq.d: f{} f{}\n", fs, ft);
+            return false;
+        }
+        break;
+    
+    // Cop1 branches
+    case InstrId::cpu_bc1tl:
+        is_branch_likely = true;
+        [[fallthrough]];
+    case InstrId::cpu_bc1t:
+        print_indent();
+        print_branch_condition("if (c1cs)", ctx_gpr_prefix(rs), rs);
+        print_branch("goto L_{:08X}", (uint32_t)instr.getBranchVramGeneric());
+        break;
+    case InstrId::cpu_bc1fl:
+        is_branch_likely = true;
+        [[fallthrough]];
+    case InstrId::cpu_bc1f:
+        print_indent();
+        print_branch_condition("if (!c1cs)", ctx_gpr_prefix(rs), rs);
+        print_branch("goto L_{:08X}", (uint32_t)instr.getBranchVramGeneric());
+        break;
+
+    // Cop1 arithmetic
+    case InstrId::cpu_mov_s:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.fl = ctx->f{}.fl", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for mov.s: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_mov_d:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.d = ctx->f{}.d", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for mov.d: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_neg_s:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.fl = -ctx->f{}.fl", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for neg.s: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_neg_d:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.d = -ctx->f{}.d", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for neg.d: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_abs_s:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.fl = fabsf(ctx->f{}.fl)", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for abs.s: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_abs_d:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.d = fabs(ctx->f{}.d)", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for abs.d: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_sqrt_s:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.fl = sqrtf(ctx->f{}.fl)", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for sqrt.s: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_sqrt_d:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.d = sqrt(ctx->f{}.d)", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for sqrt.d: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_add_s:
+        if ((fd & 1) == 0 && (fs & 1) == 0 && (ft & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.fl = ctx->f{}.fl + ctx->f{}.fl", fd, fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for add.s: f{} f{} f{}\n", fd, fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_add_d:
+        if ((fd & 1) == 0 && (fs & 1) == 0 && (ft & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.d = ctx->f{}.d + ctx->f{}.d", fd, fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for add.d: f{} f{} f{}\n", fd, fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_sub_s:
+        if ((fd & 1) == 0 && (fs & 1) == 0 && (ft & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.fl = ctx->f{}.fl - ctx->f{}.fl", fd, fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for sub.s: f{} f{} f{}\n", fd, fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_sub_d:
+        if ((fd & 1) == 0 && (fs & 1) == 0 && (ft & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.d = ctx->f{}.d - ctx->f{}.d", fd, fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for sub.d: f{} f{} f{}\n", fd, fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_mul_s:
+        if ((fd & 1) == 0 && (fs & 1) == 0 && (ft & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.fl = MUL_S(ctx->f{}.fl, ctx->f{}.fl)", fd, fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for mul.s: f{} f{} f{}\n", fd, fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_mul_d:
+        if ((fd & 1) == 0 && (fs & 1) == 0 && (ft & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.d = MUL_D(ctx->f{}.d, ctx->f{}.d)", fd, fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for mul.d: f{} f{} f{}\n", fd, fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_div_s:
+        if ((fd & 1) == 0 && (fs & 1) == 0 && (ft & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.fl = DIV_S(ctx->f{}.fl, ctx->f{}.fl)", fd, fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for div.s: f{} f{} f{}\n", fd, fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_div_d:
+        if ((fd & 1) == 0 && (fs & 1) == 0 && (ft & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.d = DIV_D(ctx->f{}.d, ctx->f{}.d)", fd, fs, ft);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for div.d: f{} f{} f{}\n", fd, fs, ft);
+            return false;
+        }
+        break;
+    case InstrId::cpu_cvt_s_w:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.fl = CVT_S_W(ctx->f{}.u32l)", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for cvt.s.w: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_cvt_d_w:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.d = CVT_D_W(ctx->f{}.u32l)", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for cvt.d.w: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_cvt_d_s:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.d = CVT_D_S(ctx->f{}.fl)", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for cvt.d.s: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_cvt_s_d:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.fl = CVT_S_D(ctx->f{}.d)", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for cvt.s.d: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_trunc_w_s:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.u32l = TRUNC_W_S(ctx->f{}.fl)", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for trunc.w.s: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    case InstrId::cpu_trunc_w_d:
+        if ((fd & 1) == 0 && (fs & 1) == 0) {
+            // even fpr
+            print_line("ctx->f{}.u32l = TRUNC_W_D(ctx->f{}.d)", fd, fs);
+        } else {
+            fmt::print(stderr, "Invalid operand(s) for trunc.w.d: f{} f{}\n", fd, fs);
+            return false;
+        }
+        break;
+    default:
+        fmt::print(stderr, "Unhandled instruction: {}\n", instr.getOpcodeName());
+        return false;
+    }
+
+    if (emit_link_branch) {
+        fmt::print(output_file, "    after_{}:\n", link_branch_index);
+    }
+
+    return true;
+}
+
+bool RecompPort::recompile_function(const RecompPort::Function& func, std::string_view output_path) {
+    fmt::print("Recompiling {}\n", func.name);
+    std::vector instructions;
+
+    // Open the output file and write the file header
+    std::ofstream output_file{ output_path.data() };
+    fmt::print(output_file,
+        "#include \"recomp.h\"\n"
+        "\n"
+        "void {}(uint8_t* restrict rdram, recomp_context* restrict ctx) {{\n"
+        // these variables shouldn't need to be preserved across function boundaries, so make them local for more efficient output
+        "    uint64_t hi = 0, lo = 0;\n"
+        "    int c1cs = 0; \n", // cop1 conditional signal
+        func.name);
+
+    // Use a set to sort and deduplicate labels
+    std::set branch_labels;
+    instructions.reserve(func.words.size());
+
+    // First pass, disassemble each instruction and collect branch labels
+    uint32_t vram = func.vram;
+    for (uint32_t word : func.words) {
+        const auto& instr = instructions.emplace_back(byteswap(word), vram);
+
+        // If this is a branch or a direct jump, add it to the local label list
+        if (instr.isBranch() || instr.getUniqueId() == rabbitizer::InstrId::UniqueId::cpu_j) {
+            branch_labels.insert((uint32_t)instr.getBranchVramGeneric());
+        }
+
+        // Advance the vram address by the size of one instruction
+        vram += 4;
+    }
+
+    // Second pass, emit code for each instruction and emit labels
+    auto cur_label = branch_labels.cbegin();
+    vram = func.vram;
+    int num_link_branches = 0;
+    int num_likely_branches = 0;
+    bool needs_link_branch = false;
+    bool in_likely_delay_slot = false;
+    for (size_t instr_index = 0; instr_index < instructions.size(); ++instr_index) {
+        bool had_link_branch = needs_link_branch;
+        bool is_branch_likely = false;
+        // If we're in the delay slot of a likely instruction, emit a goto to skip the instruction before any labels
+        if (in_likely_delay_slot) {
+            fmt::print(output_file, "    goto skip_{};\n", num_likely_branches);
+        }
+        // If there are any other branch labels to insert and we're at the next one, insert it
+        if (cur_label != branch_labels.end() && vram >= *cur_label) {
+            fmt::print(output_file, "L_{:08X}:\n", *cur_label);
+            ++cur_label;
+        }
+        // Process the current instruction and check for errors
+        if (process_instruction(instr_index, instructions, output_file, false, needs_link_branch, num_link_branches, needs_link_branch, is_branch_likely) == false) {
+            fmt::print(stderr, "Error in recompilation, clearing {}\n", output_path);
+            output_file.clear();
+            return false;
+        }
+        // If a link return branch was generated, advance the number of link return branches
+        if (had_link_branch) {
+            num_link_branches++;
+        }
+        // Now that the instruction has been processed, emit a skip label for the likely branch if needed
+        if (in_likely_delay_slot) {
+            fmt::print(output_file, "    skip_{}:\n", num_likely_branches);
+            num_likely_branches++;
+        }
+        // Mark the next instruction as being in a likely delay slot if the 
+        in_likely_delay_slot = is_branch_likely;
+        // Advance the vram address by the size of one instruction
+        vram += 4;
+    }
+
+    // Terminate the function
+    fmt::print(output_file, "}}\n");
+
+    return true;
+}