b79f28fe07
This PR is a combination of https://github.com/open-goal/jak-project/pull/2507 and some additional changes to port Shadow VU1 to OpenGL. As far as I can tell, it's working. --------- Co-authored-by: Hat Kid <6624576+Hat-Kid@users.noreply.github.com> |
||
---|---|---|
.. | ||
jak1_functions | ||
jak2_functions | ||
mips2c_private.h | ||
mips2c_table.cpp | ||
mips2c_table.h | ||
readme.md |
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_linux
, 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 suspend
s 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
.