diff --git a/blog/super-bootable-64-2020-05-06.markdown b/blog/super-bootable-64-2020-05-06.markdown new file mode 100644 index 0000000..4b7ef4e --- /dev/null +++ b/blog/super-bootable-64-2020-05-06.markdown @@ -0,0 +1,380 @@ +--- +title: Super Bootable 64 +date: 2020-05-06 +series: howto +tags: + - witchcraft + - supermario64 + - nixos +--- + +# Super Bootable 64 + +[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? + +
![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! +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.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. diff --git a/go.sum b/go.sum index d32b261..967ae53 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,7 @@ github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= @@ -100,6 +101,7 @@ github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNG github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk= github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -151,6 +153,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -161,6 +164,7 @@ google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLY google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/nix/deps.nix b/nix/deps.nix index d2ff85d..0bc6d12 100644 --- a/nix/deps.nix +++ b/nix/deps.nix @@ -113,8 +113,8 @@ fetch = { type = "git"; url = "https://github.com/golang/protobuf"; - rev = "v1.3.2"; - sha256 = "1k1wb4zr0qbwgpvz9q5ws9zhlal8hq7dmq62pwxxriksayl6hzym"; + rev = "v1.4.0"; + sha256 = "1fjvl5n77abxz5qsd4mgyvjq19x43c5bfvmq62mq3m5plx6zksc8"; }; } { @@ -338,8 +338,8 @@ fetch = { type = "git"; url = "https://github.com/prometheus/client_golang"; - rev = "v1.5.1"; - sha256 = "0nkhjpwpqr3iz2jsqrl37qkj1g4i8jvi5smgbvhxcpyinjj00067"; + rev = "v1.6.0"; + sha256 = "0wwkx69in9dy5kzd3z6rrqf5by8cwl9r7r17fswcpx9rl3g61x1l"; }; } { @@ -365,8 +365,8 @@ fetch = { type = "git"; url = "https://github.com/prometheus/procfs"; - rev = "v0.0.8"; - sha256 = "076wblhz8fjdc73fmz1lg0hafbwg1xv8hszm78lbg9anjpfgacvq"; + rev = "v0.0.11"; + sha256 = "1msc8bfywsmrgr2ryqjdqwkxiz1ll08r3qgvaka2507z1wpcpj2c"; }; } { @@ -491,8 +491,8 @@ fetch = { type = "git"; url = "https://go.googlesource.com/sys"; - rev = "e047566fdf82"; - sha256 = "1xazqxggwb834clbdqxl65xkbb45jich0nb09b4gynrp27wyy7h4"; + rev = "1957bb5e6d1f"; + sha256 = "0imqk4l9785rw7ddvywyf8zn7k3ga6f17ky8rmf8wrri7nknr03f"; }; } { @@ -522,6 +522,15 @@ sha256 = "06zl7w4sxgdq2pl94wy9ncii6h0z3szl4xpqds0sv3b3wbdlhbnn"; }; } + { + goPackagePath = "google.golang.org/protobuf"; + fetch = { + type = "git"; + url = "https://go.googlesource.com/protobuf"; + rev = "v1.21.0"; + sha256 = "12bwln8z1lf9105gdp6ip0rx741i4yfz1520gxnp8861lh9wcl63"; + }; + } { goPackagePath = "gopkg.in/alecthomas/kingpin.v2"; fetch = { diff --git a/static/blog/boot2mario.mp4 b/static/blog/boot2mario.mp4 new file mode 100644 index 0000000..259902a Binary files /dev/null and b/static/blog/boot2mario.mp4 differ diff --git a/static/blog/portingmachinegobrrr.png b/static/blog/portingmachinegobrrr.png new file mode 100644 index 0000000..874f11a Binary files /dev/null and b/static/blog/portingmachinegobrrr.png differ