Super Mario 64 is one of my all-time favorite games. Last year, the source code for the decompilation project was made available to the public. I won’t go into detail on the decompilation project itself (see this post here for more information), but instead will focus on getting Super Mario 64 compiled on Void Linux.

Getting Started

A couple of dependencies will need to be installed that will be used throughout the installation process. Note that root privileges are only required in commands where sudo is used - the rest of the commands can be run as an unprivileged user.

sudo xbps-install git
sudo xbps-install make
sudo xbps-install wget

mkdir ~/dev ~/src

For this guide, the sm64 source code will be checked out into ~/dev, and the required dependencies that are installed manually will be in ~/src, of which there are 2:

  1. qemu-irix
  2. binutils-mips64

qemu-irix

To install this package, I found it easiest to take the qemu-irix release as a .deb file from the n64decomp GitHub organization and extract the deb myself.

In order to do this, you must first install a couple of dependencies:

sudo xbps-install binutils # installs 'ar'
sudo xbps-install libglib-devel
sudo xbps-install xz

mkdir ~/src/qemu-irix

Then, pull the package and “install” it (in this case, just extract it):

cd ~/src/qemu-irix
wget https://github.com/n64decomp/qemu-irix/releases/download/v2.11-deb/qemu-irix-2.11.0-2169-g32ab296eef_amd64.deb
ar x qemu-irix-2.11.0-2169-g32ab296eef_amd64.deb
tar xf data.tar.xz

Finally, test the installed qemu-irix program to ensure it runs:

$ ./usr/bin/qemu-irix -version
qemu-irix version 2.11.50 (v2.11.0-2169-g32ab296eef-dirty)
Copyright (c) 2003-2017 Fabrice Bellard and the QEMU Project developer

binutils-mips64

This package will be installed manually, so a C compiler is required. Install gcc with:

xbps-install gcc

mkdir ~/src/binutils

Then, pull the source code and prepare it for compilation:

cd ~/src/binutils

wget ftp://ftp.gnu.org/gnu/binutils/binutils-2.35.tar.xz
tar xf binutils-2.35.tar.xz
cd binutils-2.35/

sed -i "/ac_cpp=/s/\$CPPFLAGS/\$CPPFLAGS -O2/" libiberty/configure

I don’t know if the sed command is strictly necessary - I based this process off of the mips64-elf-binutils PKGBUILD file used by the package in the AUR (for Arch Linux).

Next, compile the package:

./configure --target=mips64-elf --prefix=/usr --with-sysroot=/usr/mips64-elf --with-gnu-as --with-gnu-ld --enable-64-bit-bfd --enable-multilib --enable-plugins --disable-gold --disable-nls --disable-shared --disable-werror
make

Next, “install” the package into a local directory:

make "DESTDIR=$PWD/dest" install

Finally, test that the program works by executing mips64-elf-as:

$ ./dest/usr/bin/mips64-elf-as -version
GNU assembler (GNU Binutils) 2.35
Copyright (C) 2020 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or later.
This program has absolutely no warranty.
This assembler was configured for a target of `mips64-elf'.

Compile SM64

Finally, we are able to compile Super Mario 64!

Install the required dependencies:

sudo xbps-install audiofile-devel
sudo xbps-install python3

Clone the project from GitHub:

cd ~/dev
git clone git://github.com/n64decomp/sm64.git
cd sm64

In order to avoid any legal issues, the static assets from SM64 are NOT shipped with the decompiled source code. You must supply a rom to use as a “base”. In our case, we will need the following rom with the exact sha1 hash:

$ sha1sum 'Super Mario 64 (U) [!].z64'
9bef1128717f958171a4afac3ed78ee2bb4e86ce  Super Mario 64 (U) [!].z64

I can’t tell you where to get this rom so please don’t ask - but once you have the rom, move it into the decompilation repository and name it as follows:

mv ~/'Super Mario 64 (U) [!].z64' baserom.us.z64

Now we are almost ready to begin the compilation! The final step is to setup our PATH to use the programs we installed above. This step should be done once per session before you want to compile or recompile Mario.

PATH=$PATH:~/src/binutils/binutils-2.35/dest/usr/bin:~/src/qemu-irix/usr/bin

And finally, compile it!

$ make
...

build/us/sm64.us.z64: OK

Success! Part of the make process is that the sha sum being compared to the expected base rom, but we cany verify the output file manually with:

$ sha1sum build/us/sm64.us.z64
9bef1128717f958171a4afac3ed78ee2bb4e86ce  build/us/sm64.us.z64

Test The Rom

All of the above steps can be run on a headless Void Linux installation. The file ./build/us/sm64.us.z64 is created by the compilation process and can be used in any N64 emulator or even sent to a fashcart like the everdrive and ran on a real N64.

We can also test this on Void Linux with mupen64plus, assuming there is a windowing system installed and it is not a server/headless installation.

sudo xbps-install mupen64plus

Then, to test the rom:

mupen64plus --gfx mupen64plus-video-glide64mk2 --audio mupen64plus-audio-sdl --windowed ./build/us/sm64.us.z64

You can pass whatever options you like, but I find these create the most performant result. I also use a raphnet N64 to USB adapter to be able to use an original N64 controller on my computer.

This screenshot shows Mario is working as expected!

good-mario

Rom Hacking

With all of this in place, we now have C source code that can be compiled perfectly into a Super Mario 64 rom file. As such, we are free to make any modifications to the source code we like.

As a simple example I had an idea for a (terrible) basic rom hack. When Mario is a certain distance away from the camera, for performance reasons, his high quality model is replaced with a low-polygon version of his model. This can become apparent during boss fights or when the camera is fixed, such as in the big house on Rainbow Ride. The idea is to make the low-poly version of Mario the default model regardless of camera distance.

In searching the source code to figure out how this logic is handled, I ended up actually learning something I didn’t know! This block comment in ./src/engine/graph_node.h explains what I didn’t know:

/** GraphNode that only renders its children if the current transformation matrix
 *  has a z-translation (in camera space) greater than minDistance and less than
 *  maxDistance.
 *  Usage examples: Mario has three level's of detail: Normal, low-poly arms only, and fully low-poly
 *  The tower in Whomp's fortress has two levels of detail.
 */

It turns out Mario has 3 levels of detail! I wasn’t aware of the “medium” level which is low-poly arms only. So already I’ve learned something new about this game!

Now, onto hacking - ./actors/mario/geo.inc.c controls the Mario model. The variables mario_geo_render_body and mario_geo are the important ones to note here. Starting with the first function:

// 0x17002D7C
const GeoLayout mario_geo_render_body[] = {
   GEO_NODE_START(),
   GEO_OPEN_NODE(),
      GEO_RENDER_RANGE(-2048, 600),
      GEO_OPEN_NODE(),
         GEO_BRANCH(1, mario_geo_load_body), // HIGH POLY (normal mario)
      GEO_CLOSE_NODE(),
      GEO_RENDER_RANGE(600, 1600),
      GEO_OPEN_NODE(),
         GEO_BRANCH(1, mario_geo_load_medium_poly_body), // MEDIUM POLY
      GEO_CLOSE_NODE(),
      GEO_RENDER_RANGE(1600, 32767),
      GEO_OPEN_NODE(),
         GEO_BRANCH(1, mario_geo_load_low_poly_body), // LOW POLY
      GEO_CLOSE_NODE(),
   GEO_CLOSE_NODE(),
   GEO_RETURN(),
};

I’ve added comments to better understand when each model is used. We can modify this to always load the medium poly Mario:

// 0x17002D7C
const GeoLayout mario_geo_render_body[] = {
   GEO_NODE_START(),
   GEO_OPEN_NODE(),
      GEO_RENDER_RANGE(-2048, 600),
      GEO_OPEN_NODE(),
         GEO_BRANCH(1, mario_geo_load_medium_poly_body), // HIGH POLY (normal mario)
      GEO_CLOSE_NODE(),
      GEO_RENDER_RANGE(600, 1600),
      GEO_OPEN_NODE(),
         GEO_BRANCH(1, mario_geo_load_medium_poly_body), // MEDIUM POLY
      GEO_CLOSE_NODE(),
      GEO_RENDER_RANGE(1600, 32767),
      GEO_OPEN_NODE(),
         GEO_BRANCH(1, mario_geo_load_medium_poly_body), // LOW POLY
      GEO_CLOSE_NODE(),
   GEO_CLOSE_NODE(),
   GEO_RETURN(),
};

Also, the variable below it needs to be modified as well:

// 0x17002DD4
const GeoLayout mario_geo[] = {
   GEO_SHADOW(SHADOW_CIRCLE_PLAYER, 0xB4, 100),
   GEO_OPEN_NODE(),
      GEO_SCALE(0x00, 16384),
      GEO_OPEN_NODE(),
         GEO_ASM(0, geo_mirror_mario_backface_culling),
         GEO_ASM(0, geo_mirror_mario_set_alpha),
         GEO_SWITCH_CASE(0, geo_switch_mario_stand_run),
         GEO_OPEN_NODE(),
            GEO_BRANCH(1, mario_geo_load_body), // THIS LINE
            GEO_BRANCH(1, mario_geo_render_body),
         GEO_CLOSE_NODE(),
         GEO_ASM(1, geo_mirror_mario_backface_culling),
      GEO_CLOSE_NODE(),
   GEO_CLOSE_NODE(),
   GEO_END(),
};

Changing the line that I commented earlier to match the polygon level we want:

GEO_BRANCH(1, mario_geo_load_medium_poly_body), // THIS LINE

With these two changes in place, we can now recompile our own personalized version of Super Mario 64 that will only use the medium-poly version of Mario.

$ make
...
tools/n64cksum build/us/sm64.us.bin build/us/sm64.us.z64
build/us/sm64.us.z64: FAILED
sha1sum: WARNING: 1 computed checksum did NOT match
The build succeeded, but did not match the official ROM. This is expected if you are making changes to the game.
To silence this message, use "make COMPARE=0".
make: *** [Makefile:339: all] Error 1

It broke?! Well not quite. The build was successful, but the sha1 hash check I mentioned above failed. This is ok - in fact this is expected - since we are modifying the source and intend to have a different version. We can instead run make with a variable set to not compare hashes after the build succeeds:

make COMPARE=0

Now, booting up the run with mupen64plus we can see what medium-poly Mario looks like.

medium-poly-mario

The model looks mostly the same, but you can really see his arms just aren’t quite right.

This is cool, but I think we can make it more cursed. Let’s swap out the variables to only have low-poly Mario instead. The 2 variables inside ./actors/mario/geo.inc.c should look like:

// 0x17002D7C
const GeoLayout mario_geo_render_body[] = {
   GEO_NODE_START(),
   GEO_OPEN_NODE(),
      GEO_RENDER_RANGE(-2048, 600),
      GEO_OPEN_NODE(),
         GEO_BRANCH(1, mario_geo_load_low_poly_body),
      GEO_CLOSE_NODE(),
      GEO_RENDER_RANGE(600, 1600),
      GEO_OPEN_NODE(),
         GEO_BRANCH(1, mario_geo_load_low_poly_body),
      GEO_CLOSE_NODE(),
      GEO_RENDER_RANGE(1600, 32767),
      GEO_OPEN_NODE(),
         GEO_BRANCH(1, mario_geo_load_low_poly_body),
      GEO_CLOSE_NODE(),
   GEO_CLOSE_NODE(),
   GEO_RETURN(),
};

// This last geo is used to load all of Mario Geo in the Level Scripts

// 0x17002DD4
const GeoLayout mario_geo[] = {
   GEO_SHADOW(SHADOW_CIRCLE_PLAYER, 0xB4, 100),
   GEO_OPEN_NODE(),
      GEO_SCALE(0x00, 16384),
      GEO_OPEN_NODE(),
         GEO_ASM(0, geo_mirror_mario_backface_culling),
         GEO_ASM(0, geo_mirror_mario_set_alpha),
         GEO_SWITCH_CASE(0, geo_switch_mario_stand_run),
         GEO_OPEN_NODE(),
            GEO_BRANCH(1, mario_geo_load_low_poly_body),
            GEO_BRANCH(1, mario_geo_render_body),
         GEO_CLOSE_NODE(),
         GEO_ASM(1, geo_mirror_mario_backface_culling),
      GEO_CLOSE_NODE(),
   GEO_CLOSE_NODE(),
   GEO_END(),
};

Build the new rom with:

$ make COMPARE=0
...
mips64-elf-objcopy --pad-to=0x800000 --gap-fill=0xFF build/us/sm64.us.elf build/us/sm64.us.bin -O binary
tools/n64cksum build/us/sm64.us.bin build/us/sm64.us.z64

Run it with mupen64plus and:

low-poly-mario

Bam! the most cursed romhack of Super Mario 64 done by only modifying 4 lines of code.

Conclusion

This just scratches the surface of what could be done with the SM64 decompilation project. I hope to do more, and I hope this blog post also inspires you to do something with this :).

Some final thoughts I have are: how could this be made more efficient? If we are always using the same model, do we still need the logic to swap the model at a certain camera distance? Most likely not.

But for now the low-polygon version of Super Mario 64 that no one asked for is now here!

Looking Forward

I’m currently working on my first rom hack! The idea is very simple: toggle the water physics on and off completely with the L button. This means the whether will be completely dry, or completely flooded, with the push of a button. It’s already making for some really cool speedrunning strats: