My favourite PowerMac is a Nintendo Wii

(This is a guest post by Rairii)

Mac OS 7.6 booting on the Wii
Mac OS 9.2.2 booted on the Wii

So today I reached a nice milestone in one of my projects: my work porting classic Mac OS to PowerPC systems with what I affectionately call ArtX chipsets (Nintendo GameCube, Wii, eventually Wii U; the GameCube’s Flipper integrated chipset was very much designed by ArtX as it was effectively finished when ATI acquired the company, and the Wii’s Vegas (and its later revision Bollywood) and Wii U’s Latte are effectively straight successors) reached the desktop for the first time, most likely the first time since the CHRP clone era that someone outside of Apple brought up classic Mac OS on a new system to the desktop. Despite still being nowhere near ready for a public release, I want to talk about the challenges I faced with this project so far.

But first, why port classic Mac OS to this hardware in the first place? I’d already ported NT of course, and during that work I acquired some PowerPC Mac laptops which I also ended up porting NT to, work on one port improved the others and so on. This got me acquainted with the classic Mac OS, and I fell in love with the jank as it were (hacks upon hacks upon hacks going back to the 1980s is something I’d definitely describe as “jank”). Not to mention the m68k part of the TBXI is called “SuperMario”, and was for the entire PowerPC era. I wonder if a developer was a fan in the early 1990s?

Also, for the record: I started this project on 2025-06-22 (when I forked upstream openbios), no kind of coding agent or LLM was used, all coding, reversing and debugging work done by myself. I do these projects for fun and to learn, if I used “AI” it wouldn’t be fun and I wouldn’t learn anything.

Let’s start with the initial boot. “New World” classic Mac OS starts out with a bootloader called the “Trampoline”, which runs on an Open Firmware environment. So I needed to port OpenBIOS. Luckily, around this time Goldfish64 had started his own port of Mac OS X to the Wii, and he’d made an OpenBIOS port that I could use some drivers from, although for various reasons I didn’t just want to use his port directly (which was only targeting Wii/Wii U, I wanted to target GameCube too). I also decided to reuse some existing code to make some driver development easier (specifically the display driver), and thus changed my existing NT ARC firmware loader to load a big endian ELF instead.

I needed some small number of disk drivers for initial bringup, so decided to port my two drivers for devices on the EXI bus (this is just Nintendo and Macronix’s name for SPI), the “SD Gecko” (which is just a straight through adapter for a GameCube memory card slot allowing for SD over SPI), and the “EXI-IDE” (my driver is actually not correct to real hardware, but to my inaccurate emulation in my Dolphin tree allowing for testing under emulator with the low level debugging support that implies).

I wasn’t sure what kind of on-disk format I wanted to use (these drivers on my earlier NT port ran entirely using disk images on a FAT filesystem, due to the requirement for compatibiltiy with existing homebrew loaders that load executables from FAT filesystems, but this would naturally be limited to 4GB images for FAT32, and later versions of classic Mac OS needed more), so for initial testing I just took the “Mac OS ROM” TBXI file and placed it directly on a FAT filesystem as \ppc\bootinfo.txt for OpenBIOS to load it on boot.

So with that I could get the Trampoline running. I decided that I should try and apply as many patches to the image as possible as close to run-time as possible, so mostly untouched existing CD images could be used eventually.

The first issue I had was that the Trampoline makes some assumptions of the physical address space layout that are not true for this system. The physical address of a buffer gets calculated as an invalid address, so I patched that to an address guaranteed to be valid, and also the corresponding code setting up the memory chunk list/page table entries for the kernel.

Additionally, the Trampoline expects that physical addresses from 0x8000_0000 onwards corresponds to memory mapped I/O (on existing systems this code ran on, this would correspond to PCI-bus address space). This is not the case here, and in fact on Wii U, this address would correspond to part of RAM. There was also another assumption that broke on these systems, with holes between areas of RAM in the physical address space, some of which were partially filled by memory-mapped I/O.

The way I fixed this was to patch the initialisation of this MMIO mapping in the kernel memory chunk list, to a map of physical address 0x0100_0000 (partway through one area of RAM) to virtual address 0xE000_0000, with length 0x1000_0000. This specifically maps the whole of the MMIO, plus the end of one RAM area and (for post-GameCube systems) the start of another. (I may change this in future, as on the Wii U, physical address range 0xD000_0000-0xEFFF_FFFF is mapped to a part of the Radeon R7xx GPU called “HDP”, which can be used to map a view of physical memory with specific swizzling and/or endianness swapping, such that a texture in some GPU-specific format can appear to be a linear framebuffer. It depends on whether I need to use this block ) The function used for initialising a mapping in the Trampoline changed at some point and I ended up detecting the older version and hooking the function so I could work around it (earlier versions used a 16-bit page count, later versions used 32-bit). Additionally, older versions of the Trampoline sets up unconditional 1:1 mappings for physical memory above 0x8000_0000, which needs to be removed as they conflict with the mappings set up earlier.

There follows various assorted patches that assume specific hardware. These include: patching out setting some bitflags which correspond to the presence of a microcontroller found on all PowerPC Macintosh hardware, attempting to recalculate the bus frequency using a specific hardware timer, and various register writes and fixups related to a specific interrupt controller device (it’s little endian because it’s on the PCI bus, and naturally, not present here).

Finally, the final call to jump into the loaded kernel initialisation code is hooked so additional patches can be made later after everything is loaded into memory.

Additionally, the device tree must be hacked up a bit to be in a shape the Trampoline will accept. The interrupt controller is faked to be one of the recognised ones (the little endian one from above, because at a register level it’s closest to the Flipper interrupt controller present on all the ArtX chipsets, albeit on Wii U it was changed somewhat to add support for the additional PowerPC cores). The address of the frame buffer in the device tree is changed to the uncached virtual address (in the 0xExxx_xxxx mapping), and finally a fake Mac99-compatible power management device is added to the device tree, so the Trampoline prepares a power management driver for it, that can be replaced later.

As mentioned above, the kernel initialisation is hooked so extra patches can be made. On the kernel side, there is only one patch to be made: the kernel contains several interrupt controller drivers, the one chosen is patched to match the Flipper interrupt controller, with one of the unused ones sacrificed to gain more space for this. The main difference here is that the expected interrupt controller (whose driver is patched) has a legacy mode where it raises a PowerPC interrupt whenever the interrupts change (not just when interrupts are raised) so this needs to be worked around by masking the raised interrupts in the kernel and then unmasking them later on in drivers’ interrupt handlers.

Additional patches are made in the “SuperMario” m68k code, and in PowerPC native executables. The m68k code has various codepaths that read and write directly to native interrupt controller registers, these need to be patched. The function pointers for enabling and disabling audio interrupts need to be stubbed out (there’s already an entry in that table that points to a stub, which is helpful). A function called early in the boot process to initialise the ADB and power management microcontroller does not check if the hardware actually exists, and if it doesn’t it clobbers parts of low memory. A function for initialising hardware interrupts has a bug due to a copy-paste blunder, the intent was to skip past the code for initialising the interrupt for serial port 2 if the hardware isn’t present, but the developers forgot to change the jump label when copy-pasting the code from serial port 1, which causes an infinite loop if serial port 2 doesn’t exist. As for native code, there’s more code that reads and writes to interrupt controller registers which needs to be correctly patched. The USBShimKeyboard driver initialisation code fails to handle the state where no USB controller exists and in that case will call a NULL pointer inside the m68k emulator. The power management driver mentioned above is also replaced entirely here (currently I use the stub driver from BlueBox but plan to write my own later to add proper support for shutting down and restarting).

All of this is enough to get the TBXI to boot to blinking floppy – after that I wrote a display driver (thanks to Elliott Nunn for his virtio driver, it was very helpful) so I could see it correctly, followed by a driver for the gamecube controller ports (porting from my NT driver for the same) so I could move the mouse cursor around without USB.

At this point I started to implement a block IO driver (for EXI devices), but ended up getting stuck thinking about how to deal with how the disk format should be laid out and how the driver should work. Then Mac OS for CHRP got dumped, and I got sidetracked with patching up the later TBXI images to boot it, thinking running 7.x would be useful on a GameCube which doesn’t even have enough RAM to support 8.0 at minimum requirements, and after that I got sidetracked with other projects. Almost a year later I came back to it, implemented my preferred disk format in OpenBIOS and decided to hack up a driver faking SCSI (some other drivers did this, most notably classic Mac OS drivers for various SATA controllers). There were a few issues I found when writing the fake-SCSI driver too: I had to add another patch to the m68k code to force loading the disk driver (and I mean this, a driver loaded from the disk itself, basically implementing the actual disk devices on top of some partitioned block device) as a newer type rather than the older type (the older type was effectively obsolete when the first PowerPC systems released anyway, and I don’t even support the old-style calls from my fake SCSI driver, which is probably why it crashed trying to load it as an older type driver), and also found an interesting quirk where the optical disc driver (loaded from the block device itself) emulates 512-byte sectors on 2048- or 2352-byte sector block devices by making a read call setting up 3 scatter-gather buffers, except it never checked if the underlying SCSI driver actually supports scatter-gather, so I had to implement support for that on reads.

I still saw a blinking floppy though, some debugging showed me what was being rejected, a PowerPC function StartLib!GetStartupDevice was being called to get the boot device, and it made some assumptions of both the device tree and that there was some kind of built in SCSI device that needed separate handling. The first one was simple enough (fix the device tree in my driver’s initialisation code), the second one was more interesting in that I had to grovel into internal structures to fix an index in 3 different places (including the device tree) to ensure it was non-zero and so the correct code path would be executed.

With this done I saw a happy Mac icon for the first time, but then got rejected by the HFS boot sector (something errored and it returned). This turned out to be a bug in my own driver: I was debugging in dolphin with a hacked up EXI-IDE device; reading and writing data in PIO mode for IDE devices deals in 16-bit words, and I didn’t expect my code to get passed a completely unaligned odd buffer address (low 4 bits equal to 1), so I had to modify my low level driver to not even attempt EXI DMA for such an address. The bug manifested as a byte effectively failing to be written out, with remaining data getting offset by one byte; at least this happened when reading the header of a resource fork, and what failed was the validity checks for this data, making it much easier to debug (I ended up dumping RAM and comparing with the expected data from the disk image I was using).

After this, Mac OS 9.2.2 started to boot in my hacked up Dolphin tree, very slowly. It took an hour before it finally crashed, and then I tried 7.6, which also crashed, but earlier. I realised that it was probably crashing trying to load MacsBug, removed it, and after about 20 minutes I was staring at the desktop.

Mac OS 7.6 booted in my hacked up Dolphin tree

The following morning I tested on real hardware (using SD over SPI). This basically booted 7.6 within 40 seconds, and this was supposed to be the slowest access mode for an SD card! My previous experience was writing an NT driver for SD over SPI though, and perhaps that is more of an outlier. See this video installing NT on a GameCube using such a block device, and note that the NT text setup file copy is sped up 64 times – it’s VERY slow. The difference here is that I get to hog the whole CPU when performing a transfer – in fact there’s no other option, if I return a value specifying the transfer is asynchronous, the caller proceeds to busy-wait for interrupts in a loop waiting for the transfer to complete (although the busy-wait does check for all controllers present on the system).

Later on I added some hacked up crash handler so I could try to debug what was going on with MacsBug… it turned out to be an emulation error, no such crash occured when loading MacsBug on real hardware. I then found out the reason Mac OS 9.2.2 was crashing on boot was because of Open Transport, removing it from the Extensions folder caused it to boot perfectly fine. My guess is that Open Transport can’t handle no serial or network device being present at all.

As I mentioned, my port is still nowhere near release. Next up will be bringing up the Wii’s SD controller, then the USB controller (I’ve already done some reversing work related to that, I know there’s a vibecoded EHCI driver now, that’ll probably only aid in reversing work really), then getting the port brought up on the Wii U. At some point I want to play around with patches for blowing through at least some of the RAM barriers too.

Leave a Reply