jak-project/game/mips2c
water111 71cb1aef6f
[merc2] support vertex updates, use this for blerc in jak 1 and jak 2 (#2179)
This PR adds a feature to merc2 to update vertices. This will be needed
to efficient do effects like blerc/ripple/texture scroll. It's enabled
for blerc in jak 1 and jak 2, but with a few disclaimers:
- currently we still use the mips2c blerc implementation, which is slow
and has some "jittering" because of integer precision. When porting to
PC, there was an additional synchronization problem because blerc
overwrites the merc data as its being read by the renderers. I _think_
this wasn't an issue on PS2 because the blerc dma is higher priority
than the VIF1 DMA, but I'm not certain. Either way, I had to add a mutex
for this on PC to avoid very slight flickering/gaps. This isn't ideal
for performance, but still beats generic by a significant amount in
every place I tested. If you see merc taking 2ms to draw, it is likely
because it is stuck waiting on blerc to finish. This will go away once
blerc itself is ported to C++.
- in jak 1, we end up using generic in some cases where we could use
merc. In particular maia in village3 hut. This will be fixed later once
we can use merc in more places. I don't want to mess with the
merc/generic selection logic when we're hopefully going to get rid of it
soon.
- There is no support for ripple or texture scroll. These use generic on
jak 1, and remain broken on jak 2.
- Like with `emerc`, jak 1 has a toggle to go back to the old behavior
`*blerc-hack*`.
- In most cases, toggling this causes no visual differences. One
exception is Gol's teeth. I believe this is caused by texture coordinate
rounding issues, where generic has an additional float -> int -> float
compared to PC merc. It is very hard to notice so I'm not going to worry
about it.
2023-01-31 18:23:39 -05:00
..
jak1_functions [merc2] support vertex updates, use this for blerc in jak 1 and jak 2 (#2179) 2023-01-31 18:23:39 -05:00
jak2_functions [merc2] support vertex updates, use this for blerc in jak 1 and jak 2 (#2179) 2023-01-31 18:23:39 -05:00
mips2c_private.h [merc2] support vertex updates, use this for blerc in jak 1 and jak 2 (#2179) 2023-01-31 18:23:39 -05:00
mips2c_table.cpp decomp: squid-* files (#2170) 2023-01-28 18:44:10 -05:00
mips2c_table.h [decomp] Jak 2 mips2c, collide-func (#1805) 2022-08-26 18:03:48 -04:00
readme.md [decomp] add mips2c converter (#842) 2021-09-11 20:52:35 -04:00

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 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.