Skip to content

About

Tool to statically recompile N64 games into native executables. Maintained as a dependency of DK64: UnKONGtrolled β€” built with spite, maintained with AI. 🦍

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Β 
Β 

Latest commit

Β 

History

113 Commits

Folders and files

Repository files navigation

N64: Recompiled

A tool to statically recompile N64 binaries into C code that can be compiled for any platform. This fork is maintained as a dependency of Donkey Kong 64: UnKONGtrolled β€” a project that exists out of spite, and is maintained with maximum AI involvement. 🦍

For the full lore on why that project exists, see its CONTRIBUTING.md.


Table of Contents


How it Works

The recompiler works by accepting a list of symbols and metadata alongside the binary, splitting the input binary into functions that are each individually recompiled into a C function named according to the metadata.

Instructions are processed one-by-one and corresponding C code is emitted as each one gets processed. This translation is intentionally very literal in order to keep complexity low. For example, the instruction addiu $r4, $r4, 0x20 β€” which adds 0x20 to the 32-bit value in the low bytes of register $r4 and stores the sign-extended 64-bit result back in $r4 β€” gets recompiled into:

ctx->r4 = ADD32(ctx->r4, 0X20);

The jal (jump-and-link) instruction is recompiled directly into a function call. j or b instructions (unconditional jumps and branches) that can be identified as tail-call optimizations are also recompiled into function calls. Branch delay slots are handled by duplicating instructions as necessary.

The recompiler also attempts to turn a jr instruction into a switch-case statement when it can determine it's being used with a jump table. The recompiler has been primarily tested on binaries built with old MIPS compilers (e.g. mips gcc 2.7.2 and IDO) as well as modern clang targeting MIPS. Modern mips gcc may trip up the recompiler due to certain optimizations, but those cases can generally be avoided with specific compilation flags.

Every output function is currently emitted into its own file. An option to group functions together into output files may be provided in the future to improve build times by reducing file I/O overhead.

Recompiler output can be compiled with any C compiler (tested with MSVC, GCC, and Clang). The output is expected to be used with a runtime that provides the necessary functionality and macro implementations. A runtime is provided in N64ModernRuntime, which can be seen in action in the Zelda 64: Recompiled project.

This is not the first project to use static recompilation on game console binaries β€” a well-known example is jamulator, which targets NES binaries. The more direct inspiration is the IDO static recompilation, which recompiles the SGI IRIX IDO compiler on modern systems to facilitate matching decompilation of N64 games.


Overlays

Statically linked and relocatable overlays can both be handled by this tool. In both cases, the tool emits function lookups for jump-and-link-register instructions (i.e. function pointers or virtual functions), which the runtime can implement using any sort of lookup table. For example, the instruction jalr $25 gets recompiled as:

LOOKUP_FUNC(ctx->r25)(rdram, ctx);

The runtime can then maintain a list of which program sections are loaded and at what address they reside, in order to determine which function to call when a lookup is triggered at runtime.

For relocatable overlays, the tool modifies supported instructions with relocation data (lui, addiu, load and store instructions) by emitting an extra macro that enables the runtime to relocate the instruction's immediate value field. For example, the instruction lui $24, 0x80C0 in a section beginning at 0x80BFA100 with a relocation against a symbol at 0x80BFA730 gets recompiled as:

ctx->r24 = S32(RELOC_HI16(1754, 0X630) << 16);

where 1754 is the index of that section. The runtime implements RELOC_HI16 and RELOC_LO16 to handle modifying the immediate based on the current loaded address of the section.

Support for TLB mapping relocations is planned, adding the ability to provide a list of MIPS32 relocations so the runtime can relocate them on load. Combined with the relocatable overlay functionality, this should allow running most TLB-mapped code without a performance penalty on every RAM access.


How to Use

The recompiler is configured via a TOML file passed as its first argument. The TOML file specifies input and output file paths, and optionally allows you to stub out specific functions, skip recompilation of specific functions, and patch single instructions in the target binary.

There is also planned functionality for emitting hooks in the recompiler output via the TOML ([[patches.func]] and [[patches.hook]] sections), but this is currently unimplemented.

Full documentation on every available option is not yet available, but an example TOML can be found in the Zelda 64: Recompiled project here.

Currently, the only way to provide the required metadata is by passing an ELF file to the tool. The easiest way to get such an ELF is to set up a disassembly or decompilation of the target binary. Support for providing metadata via a custom format β€” removing the need for an ELF β€” is planned for a future release.


Single File Output Mode (for Patches)

The recompiler can be configured to run in single file output mode via an option in the configuration TOML. This emits all functions from the provided ELF into a single output file, intended for compiling patched versions of functions from the target binary.

This mode leverages standard linker behavior (ld, lld, MSVC's link.exe, etc.): since linkers only search a static library for symbols not already found in earlier input files, providing the recompiled patches before the original recompiler output causes patches to take priority over functions with matching names.

This saves significant time when iterating on patches, as it bypasses the need to rerun the recompiler on the target binary or recompile the original recompiler output. An example of this mode in use can be found in Zelda 64: Recompiled here, with the corresponding patch Makefile here.


RSP Microcode Support

RSP microcode can also be recompiled with this tool. Overlay support for RSP microcode is not currently implemented but may be added in the future. Documentation on this functionality is forthcoming.


Planned Features

  • Custom metadata format to provide symbol names, relocations, and other necessary data without requiring an ELF
  • Emitting multiple functions per output file to speed up compilation
  • Support for recording MIPS32 relocations to allow runtimes to relocate them for TLB mapping
  • Ability to recompile into a dynamic language (such as Lua) for loading code at runtime to support mods

Building

This project requires CMake 3.20 or above and a C++ compiler with C++20 support.

This repo uses git submodules β€” clone recursively:

git clone --recurse-submodules https://github.com/christopher-conley/N64Recomp

Or initialize submodules after cloning:

git submodule update --init --recursive

From there, building follows standard CMake conventions:

cmake -B build .
cmake --build build

Libraries Used

  • rabbitizer β€” instruction decoding and analysis
  • ELFIO β€” ELF parsing
  • toml11 β€” TOML parsing
  • fmtlib β€” string formatting

A dependency of Donkey Kong 64: UnKONGtrolled. Built with spite. Maintained with AI. 🦍

About

Tool to statically recompile N64 games into native executables. Maintained as a dependency of DK64: UnKONGtrolled β€” built with spite, maintained with AI. 🦍

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages