xesite/blog/super-bootable-64-2020-05-0...

379 lines
11 KiB
Markdown
Raw Permalink Normal View History

---
title: Super Bootable 64
date: 2020-05-06
series: howto
tags:
- witchcraft
- supermario64
- nixos
---
[Super Mario 64][sm64] was the launch title of the [Nintendo 64][n64] in 1996.
This game revolutionized an entire generation and everything following it by
delivering fast, smooth and fun 3d platforming gameplay to gamers all over the
world. This game is still played today by speedrunners, who do everything from
beating it while collecting every star, the minimum amount of stars normally
required, 0 stars and [without pressing the A jump button][wfrrpannenkoek].
[sm64]: https://en.wikipedia.org/wiki/Super_Mario_64
[n64]: https://en.wikipedia.org/wiki/Nintendo_64
[wfrrpannenkoek]: https://youtu.be/kpk2tdsPh0A
This game was the launch title of the Nintendo 64. As such, the SDK used to
develop it was pre-release and [had an optimization bug that forced the game to
be shipped without optimizations due to random crashiness issues][mvgo0] (watch
the linked video for more information on this than I can summarize here).
Remember that the Nintendo 64 shipped games on write-once ROM cartridges, so any
bug that could cause the game to crash randomly was fatal.
[mvgo0]: https://youtu.be/NKlbE2eROC0
When compiling something _without_ optimizations, the output binary is
effectively a 1:1 copy of the input source code. This means that exceptionally
clever people could theoretically go in, decompile your code and then create
identical source code that could be used to create a byte-for-byte identical
copy of your program's binary. But surely nobody would do that, that would be
crazy, wouldn't it?
2020-10-02 22:20:52 +00:00
![Noooo! You can't just port a Nintendo 64 game to LibGL! They're
completely different hardware! It wouldn't respect the wishes of the creators!
2020-10-02 22:20:52 +00:00
Hahaha porting machine go brrrrrrrr](/static/blog/portingmachinegobrrr.png)
Someone did. The fruits of this effort are available [here][sm64dc]. This was
mostly a proof of concept and is a masterpiece in its own right. However,
because it was decompiled, this means that the engine itself could theoretically
be ported to run on any other platform such as Windows, Linux, the Nintendo
Switch or even a [browser][sm64browser].
[sm64dc]: https://github.com/n64decomp/sm64
[sm64browser]: https://froggi.es/mario/
[Someone did this][sm64pcnews] and ended up posting it on 4chan. Thanks to a
friend, I got my hands on the Linux-compatible source code of this port and made
an archive of it [on my git server][sm66pcsauce]. My fork of it has only
minimal changes needed for it to build in NixOS.
[sm64pcnews]: https://www.videogameschronicle.com/news/a-full-mario-64-pc-port-has-been-released/
[sm66pcsauce]: https://tulpa.dev/saved/sm64pc
[nixos-generators][nixosgenerators] is a tool that lets you create custom NixOS
system definitions based on a NixOS module as input. So, let's create a bootable
ISO of Super Mario 64 running on Linux!
[nixosgenerators]: https://github.com/nix-community/nixos-generators
## Setup
You will need an amd64 Linux system. NixOS is preferable, but any Linux system
should _theoretically_ work. You will also need the following things:
- `sm64.us.z64` (the release rom of Super Mario 64 in the US version 1.0) with
an sha1 sum of `9bef1128717f958171a4afac3ed78ee2bb4e86ce`
- nixos-generators installed (`nix-env -f
https://github.com/nix-community/nixos-generators/archive/master.tar.gz -i`)
So, let's begin by creating a folder named `boot2sm64`:
```console
$ mkdir ~/code/boot2sm64
```
Then let's create a file called `configuration.nix` and put some standard
boilerplate into it:
```nix
# configuration.nix
{ pkgs, lib, ... }:
{
networking.hostName = "its-a-me";
}
```
And then let's add [dwm][dwm] as the window manager. This setup will be a little
bit more complicated because we are going to need to add a custom configuration
as well as a patch to the source code for auto-starting Super Mario 64. Create a
folder called `dwm` and run the following commands in it to download the config
we need and the autostart patch:
[dwm]: https://dwm.suckless.org/
```console
$ mkdir dwm
$ cd dwm
$ wget -O autostart.patch https://dwm.suckless.org/patches/autostart/dwm-autostart-20161205-bb3bd6f.diff
$ wget -O config.h https://gist.githubusercontent.com/Xe/f5fae8b7a0d996610707189d2133041f/raw/7043ca2ab5f8cf9d986aaa79c5c505841945766c/dwm_config.h
```
And then add the following before the opening curly brace:
```nix
{ pkgs, lib, ... }:
let
dwm = with pkgs;
let name = "dwm-6.2";
in stdenv.mkDerivation {
inherit name;
src = fetchurl {
url = "https://dl.suckless.org/dwm/${name}.tar.gz";
sha256 = "03hirnj8saxnsfqiszwl2ds7p0avg20izv9vdqyambks00p2x44p";
};
buildInputs = with pkgs; [ xorg.libX11 xorg.libXinerama xorg.libXft ];
prePatch = ''sed -i "s@/usr/local@$out@" config.mk'';
postPatch = ''
cp ${./dwm/config.h} ./config.h
'';
patches = [ ./dwm/autostart.patch ];
buildPhase = " make ";
meta = {
homepage = "https://suckless.org/";
description = "Dynamic window manager for X";
license = stdenv.lib.licenses.mit;
maintainers = with stdenv.lib.maintainers; [ viric ];
platforms = with stdenv.lib.platforms; all;
};
};
in {
environment.systemPackages = with pkgs; [ hack-font st dwm ];
networking.hostName = "its-a-me";
}
```
Now let's create the mario user:
```nix
{
# ...
users.users.mario = { isNormalUser = true; };
system.activationScripts = {
base-dirs = {
text = ''
mkdir -p /nix/var/nix/profiles/per-user/mario
'';
deps = [ ];
};
};
services.xserver.windowManager.session = lib.singleton {
name = "dwm";
start = ''
${dwm}/bin/dwm &
waitPID=$!
'';
};
services.xserver.enable = true;
services.xserver.displayManager.defaultSession = "none+dwm";
services.xserver.displayManager.lightdm.enable = true;
services.xserver.displayManager.lightdm.autoLogin.enable = true;
services.xserver.displayManager.lightdm.autoLogin.user = "mario";
}
```
The autostart file is going to be located in `/home/mario/.dwm/autostart.sh`. We
could try and place it manually on the filesystem with a NixOS module, or we
could use [home-manager][hm] to do this for us. Let's have home-manager do this
for us. First, install home-manager:
[hm]: https://rycee.gitlab.io/home-manager/
```console
$ nix-channel --add https://github.com/rycee/home-manager/archive/release-20.03.tar.gz home-manager
$ nix-channel --update
```
Then let's add home-manager to this config:
```nix
{
# ...
imports = [ <home-manager/nixos> ];
home-manager.users.mario = { config, pkgs, ... }: {
home.file = {
".dwm/autostart.sh" = {
executable = true;
text = ''
#!/bin/sh
export LIBGL_ALWAYS_SOFTWARE=1 # will be relevant later
'';
};
};
};
}
```
Now, for the creme de la creme of this project, let's build Super Mario 64. You
will need to get the base rom into your system's Nix store somehow. A half
decent way to do this is with [quickserv][quickserv]:
[quickserv]: https://tulpa.dev/Xe/quickserv
```console
$ nix-env -if https://tulpa.dev/Xe/quickserv/archive/master.tar.gz
$ cd /path/to/folder/with/baserom.us.z64
$ quickserv -dir . -port 9001 &
$ nix-prefetch-url http://127.0.0.1:9001/baserom.us.z64
```
This will pre-populate your Nix store with the rom and should return the
following hash:
```
148xna5lq2s93zm0mi2pmb98qb5n9ad6sv9dky63y4y68drhgkhp
```
If this hash is wrong, then you need to find the correct rom. I cannot help you
with this.
Now, let's create a simple derivation for the Super Mario 64 PC port. I have a
tweaked version that is optimized for NixOS, which we will use for this. Add the
following between the `dwm` package define and the `in` statement:
```nix
# ...
sm64pc = with pkgs;
let
baserom = fetchurl {
url = "http://127.0.0.1:9001/baserom.us.z64";
sha256 = "148xna5lq2s93zm0mi2pmb98qb5n9ad6sv9dky63y4y68drhgkhp";
};
in stdenv.mkDerivation rec {
pname = "sm64pc";
version = "latest";
buildInputs = [
gnumake
python3
audiofile
pkg-config
SDL2
libusb1
glfw3
libgcc
xorg.libX11
xorg.libXrandr
libpulseaudio
alsaLib
glfw
libGL
unixtools.hexdump
];
src = fetchgit {
url = "https://tulpa.dev/saved/sm64pc";
rev = "c69c75bf9beed9c7f7c8e9612e5e351855065120";
sha256 = "148pk9iqpcgzwnxlcciqz0ngy6vsvxiv5lp17qg0bs7ph8ly3k4l";
};
buildPhase = ''
chmod +x ./extract_assets.py
cp ${baserom} ./baserom.us.z64
make
'';
installPhase = ''
mkdir -p $out/bin
cp ./build/us_pc/sm64.us.f3dex2e $out/bin/sm64pc
'';
meta = with stdenv.lib; {
description = "Super Mario 64 PC port, requires rom :)";
};
};
# ...
```
And then add `sm64pc` to the system packages:
```nix
{
# ...
environment.systemPackages = with pkgs; [ st hack-font dwm sm64pc ];
# ...
}
```
As well as to the autostart script from before:
```nix
{
# ...
home-manager.users.mario = { config, pkgs, ... }: {
home.file = {
".dwm/autostart.sh" = {
executable = true;
text = ''
#!/bin/sh
export LIBGL_ALWAYS_SOFTWARE=1
${sm64pc}/bin/sm64pc
'';
};
};
};
}
```
Finally let's enable some hardware support so it's easier to play this bootable
game:
```nix
{
# ...
hardware.pulseaudio.enable = true;
virtualisation.virtualbox.guest.enable = true;
virtualisation.vmware.guest.enable = true;
}
```
Altogether you should have a `configuration.nix` that looks like
[this][confignix].
[confignix]: https://gist.github.com/Xe/935920193cfac70c718b657a088f3417#file-configuration-nix
So let's build the ISO!
```console
$ nixos-generate -f iso -c configuration.nix
```
Much output later, you will end up with a path that will look something like
this:
```
/nix/store/fzk3psrd3m6x437m6xh9pc7bnv2v44ax-nixos.iso/iso/nixos.iso
```
This is your bootable image of Super Mario 64. Copy it to a good temporary
folder (like your downloads folder):
```console
cp /nix/store/fzk3psrd3m6x437m6xh9pc7bnv2v44ax-nixos.iso/iso/nixos.iso ~/Downloads/mario64.iso
```
Now you are free to do whatever you want with this, including [booting it in a
virtual machine][bootinvmmp4].
[bootinvmmp4]: /static/blog/boot2mario.mp4
This is why I use NixOS. It enables me to do absolutely crazy things like
creating a bootable ISO of Super Mario 64 without having to understand how to
create ISO files by hand or how bootloaders on Linux work in ISO files.
It Just Works.