jak-project/game/mips2c/readme.md
2023-07-01 13:30:11 -04:00

5.2 KiB

Using Mips2C

The Mips2C convert very literally translates MIPS assembly to C. Each op is converted to a function call:

  c->load_symbol(v1, cache.math_camera);         // lw v1, *math-camera*(s7)
  c->lqc2(vf26, 732, v1);                        // lqc2 vf26, 732(v1)
  c->lqc2(vf27, 732, v1);                        // lqc2 vf27, 732(v1)
  c->vadd_bc(DEST::xy, BC::w, vf26, vf26, vf0);  // vaddw.xy vf26, vf26, vf0
  c->vadd_bc(DEST::x, BC::w, vf26, vf26, vf0);   // vaddw.x vf26, vf26, vf0
  c->lw(v1, 72, a2);                             // lw v1, 72(a2)

This is roughly the same thing that the PCSX2 recompiler would do. However, if compiler optimizations are turned on, Mips2C code can be very efficient, as the compiler is often smart enough to avoid loading/storing consecutive uses of a MIPS register. In draw-string, clang with -O3 is about 2x faster than OpenGOAL.

It also handles branches and delay slots by using goto:

  bc = c->sgpr64(a3) != 0;                 // bne a3, r0, L22
  c->load_symbol(a3, cache.font12_table);  // lw a3, *font12-table*(s7)
  if (bc) {
    goto block_2;
  }  // branch non-likely

Currently supported features are:

  • All of the instructions used in draw-string
  • Some of the vector float ops, including use of accumulator and Q
  • Some of the 128-bit integer ops
  • Use of the stack, but you must know the maximum stack size used by the function (not its children)
  • Use of symbols
  • Returning a value

The Mips2C converter is intended for complicated assembly functions. Compared to the decompiler, it is much more likely to "just work". It is currently the best option for functions which:

  • Have huge amounts assembly branching, making register to variable a huge mess
  • Rely on strange details of 128-bit GPR behavior that is not easy to express in OpenGOAL
  • Are not understood
  • Fails CFG, or other decompiler passes

To use it, add the function's name to the mips2c list in hacks.jsonc.

Not all instructions are implemented yet, but it is generally very easy to add them. It should be possible to use the stack, but this is untested. You must provide the stack size manually.

There are some limitations:

  • Calling GOAL functions is not yet implemented. It is possible but tricky.
  • There is no support for static data yet.
  • Likely branches are not yet implemented, but should be easy.
  • The output is very hard to understand
  • 128-bit arguments and return values are not supported yet

Mips2C code linking

At link-time, the mips2c code will cache symbol lookups. It will create a Cache structure that contains all the symbols used by the function instead of actually patching the code. To set this up, you must call the link() function that is autogenerated. There is a system to make this easy: you register a callback that the linker will call when linking the appropriate file.

First, at the bottom of the mips2 output there will be something like:

// FWD DEC:
namespace draw_string { extern void link(); }

this must be copy-pasted into the top of mips2c_table.cpp (and can be removed from the .cpp file if desired)

Second, add a new entry to the gMips2CLinkCallbacks table in that same file:

{"font", {draw_string::link}}

the first thing in the list is the name of the source file that should contain the function (without .gc), and the second thing should have the same name as the namespace added before.

When the linker links the font object file, it will call the link function defined there. This will add the function to the table of available mips2c functions. Note: this does not define the function.

Note: you will need to add a third argument to gLinkedFunctionTable.reg( in the auto-generated code with the maximum amount of stack space that the function can use (not including functions it calls, just local use).

Accessing the m2c from GOAL

Replace the defun with:

(define my-func (the (function <whatever>) (__pc-get-mips2c "draw-string")))

You can use the same idea for methods with method-set!. The method name will be the decompiler name.

Running Mips2C code

When Mips2C code is linked (for the first time), a small dynamically generated function object is created. This is a very short stub that jumps to a common implementation in mips2c_call_systemv, that actually sets up the call.

The setup code saves the appropriate registers for the OS, allocates an ExecutionContext on the stack, initializes the argument registers, allocates a "fake stack array" on the stack with the requested size, and calls the C++ function.

The arguments can then be accessed through the register array. The sp register is set to point to the "fake stack" on the stack. In this way, it is possible to call other GOAL functions or suspend and all the stack stuff will just work.

Unfortunately, throwing all the registers on the stack takes a huge amount of stack space. It will be somewhere between 1 and 2 kB. For one or two functions this is probably fine, but suspends inside the mips2c will probably fail, for example.

With some clever tricks it might be possible to do better, but it doesn't seem worth it at this time.

On exit, the assembly function will grab the return value from v0 and put it in rax.