Infinite Mac: DingusPPC Explorations #

One of my goals for Infinite Mac is to learn more about computer architecture and other fundamentals. While fiddling with actual old hardware is not as interesting to me (the one classic Mac I own is a PowerBook 550c that I turn on about once a year), I do enjoy operating at lower levels of the stack than I normally encounter. Interacting with the emulators that I’ve ported to Emscripten has definitely exposed me to more of this world, but it’s still been pretty superficial. Even the FPU fidelity tweaks from last year amounted to just changing some build flags once I understood what was going on.

That being said, I’m not sure that spending more time with the current set of emulators would be the best use of my time. Basilisk II, SheepShaver and Mini vMac are mature (old enough to drink or rent a car) codebases. That in and of itself is not bad, but the original authors are gone, the vision has drifted, they have fragmented into forks or states of semi-abandonment and have fundamental limitations (aggressive ROM patching and lack of an MMU).

With that in mind, I went looking for other options. The Mac Emulation Matrix spreadsheet lists many choices, and I was particularly interested in options that would eventually allow (early) Mac OS X to be runnable. The obvious choice was QEMU – it has very broad guest OS support and is very actively developed. However, it’s also a beast of a project to build and navigate around; it didn’t seem like it would be something I would able to make much progress on while working on it for a few hours a week. There is also some work on Power Macintosh support in MAME, but it’s a similar large codebase, and it seemed to be much further behind QEMU, compatibility-wise. I also briefly considered PearPC (which is much more focused on early PowerMacs than QEMU), but it’s also in a state of abandonment (development mostly stopped in 2005, with a brief resurrection in 2015).

I then came across DingusPPC, development of which has been going on for a few years (chronicled in this Emaculation thread and these Reddit posts). It had picked up steam recently, getting to a somewhat usable state, with some hope of booting Mac OS X too. The codebase was much smaller than QEMU’s (as was the resulting binary - 1.5MB vs. 17MB) and seemed reasonably modern. I crossed my fingers that this was a KHTML-vs.-Gecko-in-2003 decision and decided to give DingusPPC a try.

My first task was figuring out what worked under DingusPPC — it emulates lots of machines but some are more fleshed out than others. I settled on the Power Macintosh 6100 running System 7.1.2 (via the Disk Tools floppy) both because it seemed to have had the most attention from the developers and because it’s a combination that’s unsupported by SheepShaver or QEMU (both of which emulate PCI-era Macs). Once I had a working native build and machine/OS configuration, I started to work on getting it to build under Emscripten. DingusPPC doesn’t have much cross-platform support beyond what’s provided for free by SDL, and SDL’s Emscripten compatibility layer does not work for Infinite Mac (it assumes direct DOM access is possible from the generated code, but I run all emulators in a worker for better performance). I ended up with an approach inspired by Chromium’s, where some classes have per-platform implementations which are included automatically based on filename suffixes.

Usually when bringing up a new emulator I implement video output first, because it makes debugging much easier — if the ROM is being read correctly I should at least see a happy or sad Mac. I did the same thing with DingusPPC (DPPC from here on out), but I was disappointed to end up with a black screen.

Black screen when tryig to boot DingusPPC
Black screens - it’s not just for graphics programmers.

To get a high-level overview of what was happening, I enabled DPPC’s CPU profiling support and added a periodic dump when host events were read (i.e. every 11ms). I could then compare the counters when booting the same configuration in both the native and Emscripten builds, and see where the divergence occurred. It became apparent that after around 300ms the Emscripten build was stuck in a loop, since the number of supervisor instructions remained flat, while it would occasionally increase in the native one.

I was stumped for a while until I increased the logging level to maximum verbosity, and noticed that a AMIC: Sound out DMA control set to 0x0 line was logged right before the divergence started. I had seen that the native build starts a background thread to pull data from the sound DMA channel, but I had left all of the sound support in the Emscripten build stubbed out, since it’s usually something that I tackle much later. In this case sound turns out the load-bearing — if the DMA channel does not get drained, the boot process hangs. I added a basic polling loop to read from the DMA channel and send it to the JS side, and I got the flashing question mark I was expecting:

Flashing question mark boot screen

The reason why the flashing question was expected is because DPPC had no way to read the startup floppy disk image — it needs to be bridged to the JS APIs that handle streaming of content over the network. I moved all of DPPC’s disk image reading behind an abstraction, added a JS-backed implementation, and then I had a successful boot from the floppy.

Welcome to Power Macintosh startup screen
System 7.1.2 was the only release to say “Welcome to Power Macintosh” when starting up one of those machines.

Once I had the system booting, input was the next task. I was able to send mouse updates pretty easily, but it became immediately apparent that the position was handled differently. Other emulators I’ve ported operate in terms of absolute coordinates, but DingusPPC pretends to have a physical mouse attached, and only sends deltas to the guest OS. While adding support for deltas was easy, I ran into the issue that the guest OS treats them as raw inputs, and then computes an extra acceleration curve based on them, thus movements were not 1:1. For now I chose to implement pointer lock support (another case where a modern web API makes things easier), but the likely better long term fix is to make DingusPPC support the extended ADB protocol, which includes the ability to send absolute coordinates (originally meant for graphics tablets).

I then went to make keyboard input work, which seemed easy enough, but it didn’t seem to work. A quick check in the native build showed that it wasn’t working there either, because the ADB keyboard implementation was actually a stub. I figured this was a good first contribution to upstream to the DPPC project, so I began to spend some quality time with tech note HW01 and the relevant chapter of Inside Macintosh. The protocol was straightforward enough, but it took me a while to catch on to the fact that there was a separate polling loop that needed to be triggered. I did eventually succeed, and the keyboard now works in both the native and Infinite Mac builds of DPPC (though I did have to make some followup fixes).

Key Caps desk accessory

During all of this development I observed that DingusPPC was extremely slow (as in, more than 3 minutes to go from boot to the Finder running). While this could be attributed to its CPU emulator being a pure interpreter and the fact that it more accurately emulates the timings of a floppy drive, the Emscripten build was particularly slow. I ran a profile and was surprised to see that a lot of time was being spent going between JavaScript and WebAssembly.

Bottom-profile results showing top functions
Top Functions
Flame graph showing call stacks
Sampled call stacks

While I do end up calling to JavaScript from the input, disk and display implementations, those hops are relatively rare. These were happening in the core CPU emulation loop, and thus every instruction. The js-to-wasm and wasm-to-js functions and the bouncing back-and-forth is done by Emscripten to implement setjmp/longjmp support, and DPPC uses those to implement exceptions in the CPU. While there is an option to use native Wasm exceptions, it’s only supported by relatively modern browsers, and I try be pretty conservative about which browsers are supported (the non-SharedArrayBuffer fallback mode is still being used by 5% of users).

I made an attempt at retrofitting DPPC to not use setjmp/longjmp, but it became extremely invasive due to the need to abort execution in all of the codepaths that exceptions could happen. I then took a closer look at the stacks that that the profiler showed and noticed that ppc_exec_inner was not in them — it was getting inlined into the ppc_exec outer loop, which is also where the setjmp call was. I had a hunch that forcing the function to not be inlined would change when the JS ↔ WASM hops would happen, and I was right — they only happened once when starting the main loop, and boot times became 2.5 times faster with this 1-line change.

All my work until this point had been from floppy or CD-ROM images, but I was hoping to make hard drive images work too. It turned out that DPPC expects full device images (with a partition map), whereas I had previously operated only on bare HFS partitions/disk images. Coincidentally around this time the creator of BlueSCSI filed an issue asking for device image support, and had some technical pointers. I was able to automatically wrap my existing images and have them work for both BlueSCI uses and as boot devices for DPPC. Unfortunately DPPC’s SCSI hard drive support is still nascent, so while the hard drive boot process begins, it’s not able to complete.

The current status is that there are a few experimental DPPC-based machines that can be run, currently only accessible via the custom instance option. Some notable combinations:

Only the first three boot successfully, but it’s still pretty satisfying to to have gotten this far. Code-wide, I still have my own fork, but I’ve been able to upstream 20 or so changes, including quite a few that help the native build too. I hope to blog about the CPU emulation ones soon — they do scratch my itech of learning more about computer internals. The DPPC tracking issue has some notes on remaining work, and I’ll be rebasing my fork as the project makes progress.

On the non-DingusPPC front, I’ve made a few small improvements to Infinite Mac:

  • The aforementioned device image support means that it’s possible to import and export .hda images for use with BlueSCSI and other physical devices, here’s a demo from a user that tried it.
  • I added custom display size support (for Basilisk II and SheepShaver-based machines), so that you can pretend to have Portrait Display or have the Mac take up your entire window or screen.
  • I kept running into instability with the Infinite HD drive under System 6, seemingly due to a corrupt desktop database. The simplest solution turned out to be to generate a smaller disk image for older OSes (since a lot of 90s-era software didn’t run on them anyway). Chances are the Finder from this era was not really set up for a 2GB disk with tens of thousands of files.

Finally, I would remiss if I didn’t mention Software Applications Incorporated’s website, which takes Infinite Mac in a very interesting direction.

1 Comment

Sorry for being off-topic for almost everything you posted (which was awesome to read), but do you have an idea how Software Applications Incorporated's website managed to enable TCP/IP Networking? I was never able to get it to work with any Systems Software on infinitemac.org

Post a Comment