N64Recomp / N64Recomp

Tool to statically recompile N64 games into native executables

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Runtime code replacement/interpreter

Mr-Wiseguy opened this issue · comments

In order to support mods that conditionally edit code (e.g. some randomizers) or distribute simple mods without requiring a separate executable for each, a mechanism for replacing code at runtime must exist.

The first part is actually running the replacement code. The following are all options for implementing that:

  • Implement a simple MIPS interpreter targeting the N64's instruction set. It would only need to support a fairly limited subset of the instruction set. This option requires no preprocessing or runtime recompilation, as the replacement code could be run as-is. This option is the simplest one, but performance could be lackluster if there are many functions being interpreted.
  • Recompile into a dynamic language, i.e. Lua. Normal mods would recompile their code ahead of time and distribute the recompiler output code files, while mods with conditional code changes would use a version of the recompiler made to be used at runtime. There are several downsides to this approach however. This approach would improve runtime speed over the interpreter (LuaJit would be far faster than any interpreter I could write, even if ran in interpreter mode). However, it's also far more complex: it requires being able to retarget the recompiler to a new language and would require bundling the recompiler itself as part of the recomp runtime. There's also the risk that mods could easily include malicious code in their files, so a good sandbox implementation would be needed to prevent that. Finally, the runtime for this dynamic language would bloat the executable.
  • Recompiling mod code via the normal route (i.e. to C) isn't practical for several reasons. The first is that normal mods would include their custom code as a compiled binary, meaning they'd be non-portable and would have a huge security risk as there would be no way to sandbox them. The second reason is that runtime recompilation to C would then also require bundling a compiler (probably clang) to then compile that output C into a binary and load it. That would be a huge added dependency and could also potentially trigger antivirus programs due to native binary generation.

Regardless of which option is chosen to handle running the code, there also needs to be a mechanism for loading the code. Assuming the function set can't be known ahead of time, there are two options:

  • Modify the recompiler to perform all function calls through the function lookup table. This could really hurt performance, even when not using any mods (unless a second mod-support binary was included with each recomp). However, this would be the most portable option and allow using mods on platforms that don't allow runtime code patching (e.g. iOS).
  • Patch replaced functions with a jump hook to the function that runs replacement code when code is loaded. This would not impact performance of the game at all when mods aren't being used, but would exclude mod support on platforms that don't support runtime code patching. Platform-specific assembly would need to be written for supported architectures (which would probably just be x64 at the start with ARM64 later on potentially). This also requires that all functions are large enough to fit the hook. MSVC already meets this requirement due to its 16-byte function alignment and GCC/Clang have options to enable similar functionality, so this is mostly a non-issue.