Infinite Mac OS X #

tl;dr: Infinite Mac can now run early Mac OS X, with 10.1 and 10.3 being the best supported versions. It’s not particularly snappy, but as someone who lived through that period, I can tell you that it wasn’t much better on real hardware. Infinite HD has also been rebuilt to have some notable indie software from that era.

Mac OS X 10.1 running NetNewsWire Lite and Terminal

Porting PearPC

I’ve been tracking DingusPPC progress since my initial port and making the occasional contribution myself, with the hope of using it to run Mac OS X in Infinite Mac. While it has continued to improve, I reached a plateau last summer; my attempts would result in either kernel panics or graphical corruption. I tried to reduce the problem a bit via a deterministic execution mode, but it wasn’t really clear where to go next. I decided to take a break from this emulator and explore alternate paths of getting Mac OS X to run.

PearPC was the obvious choice – it was created with the express purpose of emulating Mac OS X on x86 Windows and Linux machines in the early 2000s. By all accounts, it did this successfully for a few years, until interest waned after the Intel switch. I had earlier dismissed it as a “dead” codebase, but I decided that the satisfaction of getting something working compensated for dealing with legacy C++ (complete with its own string class, sprintf implementation, and GIF decoder). An encouraging discovery was that kanjitalk755 (the de-facto Basilisk II and SheepShaver maintainer) had somewhat recently set up an experimental branch of PearPC that built and ran on modern macOS. I was able to replicate their work without too much trouble, and with that existence proof I started on my sixth port of an emulator to WebAssembly/Emscripten and the Infinite Mac runtime.

In some ways PearPC not being actively developed made things easier – I didn’t have to worry about merging in changes from upstream, or agonize over how to structure my modifications to make them easier to contribute back. It was also helpful that PearPC was already a multi-platform codebase and thus had the right layers of abstraction to make adding another target pretty easy. As a bonus, it didn’t make pervasive use of threads or other harder-to-port concepts. Over the course of a few days, I was able to get it to build, output video, load disk images, and get mouse and keyboard input hooked up. It was pretty satisfying to have Mac OS X 10.2 running in a browser more reliably than it previously had.

Performance

While PearPC ran 10.2 more reliably, it felt slower than DingusPCC. I had spent some time last year making some optimizations to the latter, partly inspired by the TinyPPC emulator in this SheepShaver fork (aren’t all these names fun?). I ported DingusPPC’s benchmark harness and then set about replicating the performance work in PearPC (both emulators are pure interpreters driven by a lookup table, so the process was relatively straightforward). I was able to shave off about 15 seconds from the 10.2 boot time – it helps from a saving lives perspective, but is still not enough given that it takes almost 2 minutes to be fully operational. In the end, I copped out and added a UI disclaimer that Mac OS X can be slow to boot. I also got flashbacks to the “is it snappy yet?” discussions from the early days of Mac OS X – it was indeed slow, but not this slow.

Performance is still not as good as DingusPPC’s – the biggest bottleneck is the lack of any kind of caching in the MMU, so all loads and stores are expensive since they involve complex address computations. DingusPPC has a much more mature tiered cache that appears to be quite effective. More generally, while PearPC may be more stable than DingusPPC at running 10.2-10.4, it’s a much less principled codebase (I came across many mystery commits) and it “cheats” in many ways (it has a custom firmware and video driver, and only the subset of PowerPC instructions that are needed for Mac OS X are implemented). I’m still holding out hope for DingusPPC to be the fast, stable, and correct choice for the long term.

A Side Quest

I implemented the “unified decoding table” approach in PearPC’s interpreter one opcode family at a time. When I got to the floating point operations, I assumed it was going to be another mechanical change. I was instead surprised to see that behavior regressed – I got some rendering glitches in the Dock, and the Finder windows would not open at all. After some debugging, I noticed that the dispatching for opcode groups 59 and 63 didn’t just do a basic lookup on the relevant instruction bits. It first checked the FP bit of the Machine State Register (MSR), and if it was not set it would throw a “floating point unavailable” exception.

I initially thought this was the emulator being pedantic – all PowerPC chips used in Macs had an FPU, so this should never happen. However, setting a breakpoint showed that the exception was being hit pretty frequently during Mac OS X startup. The xnu kernel sources of that time period are available, and though I’m not familiar with the details, there are places where the FP bit is cleared and a handler for the resulting exception is registered. I assume this is an optimization to avoid having to save/restore FPU registers during context switches (if they’re not being used). The upshot was that once I implemented the equivalent FP check in my optimized dispatch code, the rendering problems went away.

This reminded me of the rendering glitches that I had encountered when trying to run Mac OS X under DingusPPC. Even when booting from the 10.2 install CD (which does not kernel panic) I would end up with missing text and other issues:

Mac OS X 10.2 installer showing text rendering glitches

Checking the DingusPPC sources showed that it never checked the FP bit, and always allowed floating point instructions to go through. I did a quick hack to check it and raise an exception if needed, and the glitches went away!

Mac OS X 10.2 installer correctly rendering text

The proper implementation was a bit more complicated, and I ended up revising it a bit to avoid a performance hit (and another contributor did another pass). But at the end of it all, DingusPPC became a lot more stable, which was a nice side effect. Better yet, it can run 10.1 reliably, which PearPC cannot. I ended up using a combination of both emulators to run a broader subset of early Mac OS X (unfortunately 10.0 is still unstable, and the Public Beta kernel panics immediately, but I’m holding out hope for the future).

Rebuilding Infinite HD

Part of the appeal of Infinite Mac is that the emulated machines also have an “Infinite HD” mounted with a lot of era-appropriate software to try. With Mac OS X running, it was time to build an alternate version that went beyond the 80s and 90s classic Mac apps I had collected. I had my favorites, but I also put out a call for suggestions and got plenty of ideas.

For actually building the disk image, I extended the automated approach that I first launched the site with. Disk images were even more popular in the early days of Mac OS X than they are today, so I added a way to import .dmgs as additional folders in the generated image. However, I quickly discovered that despite having the same extension, there are many variants, and the hdiutil that ships with modern macOS cannot always mount images generated more than 20 years ago. In the end I ended up with a Rube Goldberg approach that first extracts the raw partition via dmg2img and then recreates a “modern” disk image that can be mounted and copied from.

As for getting the actual software, the usual sites like Macintosh Garden do have some from that era, but it’s not a priority for them. Early to mid 2000s Mac OS X software appears to be a bit of a blind spot – it’s too new to be truly “retro”, but too old to still be available from the original vendor (though there are exceptions). I ended up using the Wayback Machine a lot. As a bonus, I also installed the companion “Developer” CDs for each Mac OS X version, so tools like Project Builder and Interface Builder are also accessible.

Mac OS X 10.4 running Delicious Library, CandyBar, PCalc and Pixelmator

The only limitation that I ran into is that my disk build process is centered around HFS, but HFS+ was the default of that time period, and it introduced more advanced capabilities like longer file names containing arbitrary Unicode characters. Files from disk images that rely HFS+ features do not translate losslessly, but luckily this was not an issue for most software. To actually mount multiple drives (up to 3, between the boot disk, Infinite HD, and Saved HD), I ended up borrowing a clever solution from a DingusPPC fork: a multi-partition disk image is created on the fly from an arbitrary number of partition images that are specified at startup.

Aqua

To make the addition of Mac OS X to Infinite Mac complete, I also wanted to have an Aqua mode for the site’s controls, joining the classic, Platinum, and NeXT appearances. That prompted the question: which Aqua?

Screenshots of the logout dialog in Mac OS X 10.1 to 10.4
Aqua: the early years

Though the more subdued versions from 10.3 and 10.4 are my favorites, I decided to go with the 10.0/10.1 one since it has the biggest nostalgia factor. I wanted to use the exact same image assets as the OS, and since they make heavy use of semi-transparency, regular screenshots were not going to be good enough. I used resource_dasm and pxm2tga to extract the original assets from Extras.rsrc and create my own version of Aqua:

Infinite Mac custom instance configuration dialog, rendered with an Aqua appearance

If the recent rumors of a big UI revamp do come true, it’ll be nice to have this reference point of its ancestor.

Odds and Ends

The ability to mount multiple images means that you can also have a Mac OS 9 partition and start the Classic compatibility environment (this only works under 10.1 – PearPC never supported Classic). You can thus emulate classic Mac apps inside an emulated Mac OS X inside a WebAssembly virtual machine:

Mac OS X 10.1 running Stickies, Scrapbook and Calculator under Classic

There was a recent storm in a teacup about a Calculator behavior change. Using these Mac OS X images, it’s possible to verify that versions through 10.3 didn’t have the “repeatedly press equals” behavior, but 10.4 did.

Since Mac OS X boot is rather slow, I wanted to have a way to show more progress. PearPC has a built-in way to trigger verbose mode, but DingusPPC did not, so I added a way to specify Open Firmware variables at startup. This is now exposed in the custom instance dialog via the “Debug Mode” switch.

Though I’ve moved away from custom domain names, I thought macosx.app would make a nice addition to my collection. Unfortunately it’s taken, though in a rather weird way. I even contacted the YouTuber whose video it redirects to, and he said he was not the one that registered it. It expires in a couple of months, so maybe I’ll be able to grab it.

The End Of The Line?

“When Alexander saw the breadth of his domain, he wept for there were no more worlds to conquer.”
Hans Gruber Plutarch Some Frenchman

Mac OS X support catches Infinite Mac up to the modern day, unless I happen to get access to some time travel mechanics. There are of course two more CPU transitions to go through and numerous small changes, but Tiger is fundamentally recognizable to any current-day macOS user.

Except that in the retrocomputing world, it’s always possible to go deeper or more obscure. A/UX is not something that I’m very familiar with, but it was a contemporary of classic Mac OS and would be interesting to compare to NeXTStep. Shoebill runs it, and the codebase looks approachable enough to port. Then there’s Lisa, the Pippin (DingusPPC has some nascent support), and further afield the Newton (via Einstein?). We’ll see what moves me next.

A Post-Credits Sequence

When I first began exploring ways of running Mac OS X, I mentioned that QEMU seemed too daunting to port to WebAssembly given my limited time. Furthermore, the performance of the qemu.js experiment from a few years ago made it seem like even if it did run, it would be much too slow to be usable. However, I recently became aware of qemu-wasm via this FOSDEM presentation. The performance of its Linux guest demos is encouraging: I ran an impromptu bennmark of computing an MD5 checksum of 100 MB of data and it completed it in 8 seconds (vs. 13 for DingusPPC and 18 for PearPC). There’s still a big gap between that and a graphical guest like Mac OS X, but it’s nice to have this existence proof.

Gardening Week: Evergreen Engineering #

New company, new corporate blog for me to post on. Gardening Week is a post about an engineering team ritual that we've developed at Sierra. Once again you can tell that it was not ghost-written because I got to cram in a lot of links to obscure articles.

More seriously, it's been great to be at Sierra from the start and get to influence the engineering culture in such a fundamental way. I know tech company blog posts can paint overly rosy pictures, but Gardening Week is genuinely one of my favorite parts about working here.

Infinite Mac: Macintosh Garden Library #

The Macintosh Garden is a great resource in the retro Mac community. It has an archive of nearly every piece of software released in the 80s and 90s, complete with screenshots, manuals, and metadata like year of release and operating system requirements. From its debut Infinite Mac would let you use files from the Garden: download it to your computer, and then drag it in to have it appear in the “Downloads” folder.  But while doable, it’s not the same as being discoverable or pleasant to use if you wanted to do this more than a few times. 

Inspired by the CD-ROM library feature, I decided to investigate what it would take to add a “Macintosh Garden” drawer to the site. The goal was to allow any item in the Garden’s catalog to be loaded into the emulated Mac with one click (at least for the versions that support “The Outside World”, which is most from System 7 to Mac OS 9). I reached out to the Garden’s maintainer, who was on board with the project and even provided a JSON dump of the site’s catalog of 20,000 applications and games. Building the UI was a fun exercise in making the CD-ROM drawer into a reusable component - another contribution to my collection of Classic- and Platinum-themed UI controls.

In keeping with the secondary goal of Infinite Mac bringing the best of web technologies to retro-computing, I wanted the Macintosh Garden drawer to be as fast as possible. The entire catalog was 86 megabytes of JSON, which would take a while to load, even with gzip compression (27 MB). I decided to create a custom data format, with only a small index file being necessary to render the drawer list view and support search-as-you-type. It contains plain JavaScript arrays with known indices for the title, author, and other data. This approach (inspired by the JsPbLite format) minimizes redundancy while keeping decoding simple. The file is 1.5 MB, and only 439 KB when compressed with gzip, which - combined with preloading - makes the drawer pretty snappy.

A separate “details” file with descriptions, download URLs, and other information is loaded by the Cloudflare worker, which serves pieces of it on-demand. The worker also handles proxying of downloads from the Garden, both to avoid running into CORS issues and because Cloudflare’s caching should help with frequently-used items. For downloads that are large CD-ROM images, the HTTP range-based streaming approach from the CD-ROM library is also used to help with performance.

The search UI supports operators, which were implemented by a simple predicate function run as part of a linear search (the data set is small enough that fancier indexing is not required). As a small UI touch, I finally got to use scroll snap for something (the carousel view for screenshots). I also added support for triggering downloads of an item via a URL parameter, so it’s possible to share links to a favorite item.

In terms of other Infinite Mac-related work, it’s been relatively quiet. I have been tracking DingusPPC development, and it’s now possible to (sometimes, very slowly) boot Mac OS X 10.2. I also made some small quality-of-life improvements: more control over scaling, it’s harder to accidentally close the site via Command-W, and era-appropriate fonts are now used.

Weather Text: My Own Bespoke Weather App #

I was a happy user of the Dark Sky weather app for many years. Even more than the localized and timely notifications (I live in a place with predictable weather) I appreciated its Apple Watch complications. I specifically used the three-line textual summary shown in the middle of the Modular face.

Apple Watch Modular face with Dark Sky complication
My preferred Apple Watch face, circa 2022

Apple acquired Dark Sky in 2020, incorporated many of its features into the built-in Weather app (with iOS 16/watchOS 10), and shut down the original app at the end of 2022. Unfortunately the complication layout was not carried over – the only large complications are multi-hour/day graphs that have too much information and are hard to see at a glance. On my iPhone, I first switched to Weather Line (RIP) and later to Weather Strip, but neither had a watch app. I considered CARROT Weather, which has customizable complications, as a possible replacement. However, it couldn’t replicate the same layout, and paying for a separate subscription just for a watch app seemed wasteful.

On the other hand my time is free, and I figured that with WeatherKit (the one good thing to come out of the Dark Sky acquisition), I could build my own complication that showed exactly what I wanted. I procrastinated doing this for more than a year (squinting at the Weather complication every morning and thinking “I should do something about this”) but I reached a lull in Infinite Mac development and decided to once again work on an Apple platform developed in this century. It turns out that my procrastination paid off: watchOS 9 introduced WidgetKit for complications and Xcode 15 made wireless debugging (the only option for Apple Watch development) more reliable.

After fighting a bit with my old nemesis, provisioning profiles, and figuring out how to get location access in a widget (which may change in the future), I had something up and running. WeatherKit makes it particularly easy to replicate the Dark Sky complication – there’s even a property to get an SF Symbol for the current conditions and pre-computed sunrise/sunset times

Weather Text is the resulting app - it has a minimal watch UI to request location access and show a preview, but the main focus is the complication/widget (which also shows up in Smart Stack).

Weather Text app screen Apple Watch Modular face with Weather Text complication

I decided not to put the app on the App Store. WeatherKit has a limited free quota, and while it was unlikely that the app would suddenly boom in popularity, I didn’t want to worry about it. I could have made the app paid up-front to deter casual installs, but my App Store account is not set up for payments, and I didn’t want to go through the hassle of that.

The app is instead available via TestFlight, which is hopefully enough of a deterrent that I’ll remain under the free limit (and builds expiring every 90 days also gives me additional control). I would have thought that only going through the lightweight TestFlight approval would exempt me from review shenanigans. However, I still had to change my original icon, despite the presence of plenty of other dark background icons, including the original Dark Sky one.

Weather Text is a home cooked meal/hammer kind of app, mostly meant for me (though I’m not the only one who misses Dark Sky’s complications). I don’t have grand ambitions for it, it’s not a forever project (modulo minimal upkeep to make sure it works with new OS releases). Though modern platforms don’t have quite as much end-user programmability as they used to (though widget.json/Widget Construction Set and Scriptable look interesting), as a middle-aged gentleman programmer it’s nice to still be able to make them fit exactly to my needs.

Infinite Mac: Turning To The Dark Side #

tl;dr: No, not Windows¹, but I’ve decided to expand Infinite Mac to the the black hardware that Mac users secretly lusted after in the 80s and 90s: NeXT.  There is now a runnable collection of all notable NeXTStep versions, going from the initial 0.8 preview in 1988 to the final OPENSTEP 4.2 release in 1997.

NeXTSTEP 3.3 running DOOM, DoomEd and WorldWideWeb

Porting Previous

About a year ago I came across the Previous emulator – it appeared to be a faithful simulation of the NeXT hardware and thus capable of running NeXTStep. While including it in Infinite Mac would be scope-creep, NeXT’s legacy is in many ways more relevant to today’s macOS than classic Mac OS. It also helped that it’s under active development by its original creator (see the epic thread in the NeXT Computers forums), and thus a modern, living codebase².

Previous is the fifth emulator that I’ve ported to WebAssembly/Emscripten and the Infinite Mac runtime, and it’s gotten easier. As I'm doing this work, I’m developing more and more empathy for those doing Mac game ports – some things are really easy and others become yak shaves due to the unintended consequences of choices made by the original developers. Previous is available on multiple platforms and has good abstractions, so overall it was a pretty pleasant experience.

The main challenge was the pervasive use of the SDL library – I don’t want any of it as a platform abstraction, since I have my own. I chose to replace the entry-point and several subsystems to avoid bringing in too much SDL code. Even with that, I still needed to have some stub files for dependencies that I couldn’t omit altogether. On the other hand, it was a pleasant surprise that adding sound support was trivial — the APIs that it needed mapped 1:1 with the ones I’d already exposed from the Infinite Mac runtime.

With the initial emulator being brought up, there were some more fun tasks, like adding a NeXT-style monitor frame and a NeXT appearance to the Infinite Mac controls (working on them is giving me Kaleidoscope scheme flashbacks).

Infinite Mac custom instance configuration dialog, rendered with a NeXTStep appearance

Once I had Previous running I did some profiling and was discovered the same excessive wasm-to-js/js-to-wasm ping-ponging that I noticed in DingusPPC – it also uses setjmp/longjmp to implement an exception handling mechanism for the MMU. The fix was also very similar: intentionally add an extra wrapping function that contains the CPU emulation loop.

The performance impact of the fix was not as noticeable because Previous tries to accurately simulate the performance characteristics of the original hardware (this is also why boot times are much longer). In fact, I had originally missed the speed checking that the native main loop does, and the emulator was running too fast, leading to timing-dependent things like double-clicking not working.

Speaking of mouse input, Previous uses relative mouse input — it accurately simulates real hardware so all it can do is behave as though a physical mouse is moving and generating updates. This was the case with DingusPPC too, and while the Pointer Lock API makes this work reasonably well on desktop browsers, it’s not a good fit for the mobile web. To make input more accessible on touch-enabled devices, I added a virtual trackpad mode that turns swipes and taps on the screen into mouse movements and clicks. This also helps with double-clicking and making pre-Mac OS 8.0 pull-down menus more usable, though it is somewhat finicky to trigger the drag-lock gesture (I now see why it’s so hard for other touchpad vendors to replicate all of Apple’s gestures).

As part of doing some of the integration work I became aware of just how much of a minority player NeXT was in the 80s and 90s computing world (even more so than Macs). For example, they had their text encoding/character set, and while Python (and other systems) have native support for the contemporary MacRoman encoding, I had to add my own to handle NeXT’s. Similarly, there is a convenient Python library for dealing with HFS disk images, but I could not find any for the UFS variant that NeXT used³.

The disk images with NeXTStep installs are quite large (hundreds of megabytes per version), and the Workers KV-based storage that I had been using was becoming increasingly ill-suited for them (and expensive). As part of this project I also switched to Cloudflare R2 (their S3-like object storage system). On one hand I’m not thrilled with the site becoming dependent on additional services, but on the other I do want to keep costs down. Luckily the dependency is not invasive — it would be trivial to store the disk chunks anywhere else (or to keep storing them as static files, the way I do in the local dev server).

Exploring NeXTStep

Although I was familiar with NeXTStep at a theoretical level and had spent a few hours using a slab in the mid 1990s, this was my first time using it in-depth. My first observation was that having it side-by-side with the contemporary Mac system software makes it even more apparent how much of an advance it was.

Notable OS releases of 1988: System 6.0 and NeXTStep 0.8
System 6.0 and NeXTStep 0.8: two very different releases from 1988

As I was preparing the system images with the various NeXTStep/NexTSTEP/OPENSTEP releases⁴, it was fascinating to see NeXT’s various strategy shifts over the years represented in the canned email from Steve Jobs that was present in the Mail app:
Steve Jobs email from NeXTStep 2.0 Steve Jobs email from NeXTStep 3.0 Steve Jobs email from NeXTSTEP 3.3
Strategy du jour: productivity (NeXTStep 2.0), multimedia and internationalization (NeXTStep 3.0), object-oriented development (NeXTSTEP 3.3)

I was also amused to notice a parallel with the early Mac system releases: the initial release is hard to find in pristine form (a copy of System 0.97 was only tracked down late last year). In both cases it’s it because of the very small number of copies produced, and because they shipped on read/write media (floppy disks for the Mac, magneto-optical disks in NeXT’s case), and thus ended up being used for personal storage too. For NeXTStep 0.8 the archive of images that forum member mikeboss created was the best source that I could find, though I’m happy to switch things over if a better one turns up.

As part of installing all of the OS versions I discovered that starting with NeXTStep 3.0 there is support for the HFS file system used by contemporary Macs. This means the Saved HD drive that has persistent storage also ends up working here. It can even be used as a virtual sneakernet to move data between NeXT and Mac installations on Infinite Mac (which means that it’s possible to get data in and out of the machines via Macs which have “The Outside World“ drive).

To make the systems more than a quick curiosity, I also created a NeXT version of the “Infinite HD” drive with pre-installed software. I had read stories about Doom and WorldWideWeb being developed on NeXTStep, and it was nice to make them accessible. I also added a NeXT version of the CD-ROM library feature, with one-click access to disks that have been preserved at the Internet Archive.

NeXT CD-ROM Library on Infinite Mac

As part of finding more NeXT software to try out, I came across Daydream, which allows Mac software to run on black hardware. It’s technically not an emulator, instead it takes advantage of the fact that NeXT and Macs both used the Motorola 68K CPUs. It effectively turns your NeXT machine into a Mac that you dual-boot via the help of an adapter kernel. The original version needed a hardware dongle with genuine Mac ROMs, but a few years ago the NeXT Computers forum resurrected it such that it can run entirely via software. It’s this variant (known as Darkmatter) that is included on Infinite HD. In some ways this justifies the whole Previous side-quest — it’s just a more convoluted way to run the same classic Mac software as the rest of the Infinite Mac emulators.

Darkmatter running System 7.1 emulated on NeXT hardware emulated in Previous in a browser

Things happen

Christopher Nolan (sort of) knows about Infinite Mac. I did a minor site redesign. After recalling the KanjiTalk New Year’s Day easter egg, I added custom date/time support so can you see it anytime you want. Infinite Mac is being used as a tool to make Playdate game art and other toys. You can now load files with resource forks straight from the macOS Finder. Apeiron now works. And most excitingly, DingusPPC has progressed to point where the Mac OS X 10.2 installer starts up.

Update: See also the discussion on Hacker News.

  1. Also, PCjs already has the Windows niche well-covered
  2. andreas_g is the Previous author, and he was very helpful in the thread I started about my porting work
  3. I did take a stab at writing a HexFiend template for UFS/NeXT partition maps
  4. See these pages for details on the capitalization rules.

Infinite Mac: The Case Of The Missing Text #

I had previously mentioned that I ended up fixing some DingusPPC CPU emulation bugs. This was my first time working at this (lower) level of the Infinite Mac emulators, and I thought it would be interesting to write up such an investigation.

There was an issue that was immediately apparent when I first got DingusPPC building: while starting up from the 7.1.2 Disk Tools floppy would result in a functional-looking Finder, doing the same from the 7.1.2 install CD would end up with many rendering glitches — notably no text, but also missing icons.

System 7 Finder window
7.1.2 Disk Tools Finder
System 7 Finder window with no text or icons drawn
7.1.2 Install CD Finder

Classic System Software is mostly a closed-source black box (outside of leaks), and while DingusPPC is open source, it’s a codebase that is still pretty foreign to me. In the absence of other ideas, I figured that creating a more reduced test case would be a good use of time. This reminded me of my investigations into Chromium/WebKit rendering bugs, where half the battle was reducing a complex web page down to a minimal test case.

I examined the two system folders and observed that the CD-ROM one was quite a bit larger, with additional extensions, a different enabler¹, and a bigger System suitcase.

System Folder from the 7.1.2 Disk Tools floppy, list view System Folder from the 7.1.2 CD, list view

I created a read-write copy of the CD-ROM system folder and used a native build of Basilisk II to operate on it, removing or switching out files one a time to morph it into the floppy version, and looking for any differences when booting it in DingusPPC. It turned out that switching out the “Minimal PowerPC Enabler” on the floppy with the full “PowerPC Enabler” one from the CD-ROM was the key difference — with the full enabler text would not render.

I then opened the two enablers in ResEdit and compared them — the full one had a bunch more resources. I thus repeated the process, deleting extra resources one at a time to morph the full enabler into the minimal one.

ResEdit view of resources in the Minimal PowerPC Enabler ResEdit view of resources in the regular PowerPC Enabler

It turned out that the culprit was the ntrb resources, specifically removing ID 16 (StdTextTraps) and 18 (QuickDrawTextTraps) would restore text rendering. They contain PowerPC executable code — the giveaway is the Joy!peff prefix that is the header of the Preferred Executable Format. “Traps” refers to the system/Toolbox calls. My assumption is that the ROM of the first generation of Power Macintoshes was finalized relatively early, and contained mostly 68K code that would need to be emulated². The PowerPC enabler contained native implementations of more of the Toolbox, which were loaded in via these ntrb resources. These particular resources had native implementations of the text subsystem, which was very much in line with where the buggy behavior was.

Between resource_dasm and some help from DingusPPC Discord member joevt I was able to get a list of the 14 Toolbox traps that were being reimplemented as native PowerPC versions. The one that immediately stood out was StdTxMeas – here’s a description of it from Inside Macintosh: Text:

The QuickDraw text routines use two of the bottleneck routines extensively—one to measure text (StdTxMeas) and one to draw it (StdText). Most of the high-level QuickDraw text routines call the low-level routines. The use of bottleneck routines provides flexibility to QuickDraw and applications that need to alter or augment the basic behavior of QuickDraw.

While I had significantly narrowed where things were going wrong, I was reaching the limits of what I could via basic file system and ResEdit hacking. Ideally I would have a simple test program that just called StdTxMeas, so that I could examine its behavior in isolation.

It was thus time to crack open CodeWarrior and write a simple test program. I used the SillyBalls sample code as a starting point — there certainly used to be a lot of boilerplate in getting a basic window up on the screen. I created a simple test function that draws a string and then its bounding box as determined by StdTxMeas. Lacking the ability to render debugging text, I used FrameRect as the output mechanism.

void TestStdTxMeas(WindowPtr mainWindowPtr) {
    Rect measureRect;
    Str255 testString = "\pHello World";
    Point number = {1, 1};
    Point denom = {1, 1};
    FontInfo fontInfo;
    int width;
    
    SetPort(mainWindowPtr);
    MoveTo(20, 20);
    TextSize(12);
    
    DrawString(testString);
    
    width = StdTxMeas(testString[0], testString + 1, &number, &denom, &fontInfo);
    
    // +1 for the width so that if we get a zero we can still see the rect.
    SetRect(&measureRect, 20, 8, 20 + width + 1, 24);
    FrameRect(&measureRect);
}

I set the program as the startup item (so that I wouldn’t have to navigate blindly in a half-drawn Finder window) and observed its behavior under both emulated and native implementation of the text traps.

QuickDrawTester window showing expected output of "Hello World" surrounded by a rectangle QuickDrawTester window showing incorrect output with just a black line

Sure enough, it looked like when using native PowerPC code, StdTxMeas would end up returning 0 instead of the expected value. I suspected a bug in DingusPPC’s PowerPC CPU emulation, but it was unclear where. I had some false starts:

  1. I tried to disassemble the StdTxMeas implementation – I got lost when going through too many layers of indirection due to the calling convention.
  2. I looked at its 68K implementation and checked some of the intermediate building blocks – the width table looked correct.
  3. I tried to get MacsBug running so that I could single-step through it – I was not able to get it to succesfully suspend execution under DingusPPC.

As a variant of attempt 3, I decided to make DingusPPC output the instruction stream that it was executing, hopefully there would be a clue where the problem was. To signpost the instructions that represented the implementation of StdTxMeas, I did two otherwise no-op divisions by specific literals (the MoveTo call is to ensure that the compiler didn’t decide to optimize them out).

GetPen(&pen);
pen.h = pen.h / 32749;

width = StdTxMeas(testString[0], testString + 1, &number, &denom, &fontInfo);

pen.v = pen.v / 32719;
MoveTo(pen.h, pen.v);

Checking the CodeWarrior dissembly I could see the expected li (load immediate) and divw (divide word) op codes:

// pen.h = pen.h / 32749;
    
000000A0: A861003A  lha      r3,58(SP)
000000A4: 38807FED  li       r4,32749
000000A8: 7C6323D6  divw     r3,r3,r4
000000AC: B061003A  sth      r3,58(SP)

I could thus look for the 32,749 divisor in DingusPPC’s divw implementation and then set a flag to trigger its built-in disassembler (and turn it off when the divison by 32,719 occurred). 

Out of this I had a list of 635 instructions that were executed, which was a lot, but still made for a starting point. Looking at counts of the types of instructions, there were 59 unique ones, with loads and stores being the most common. I figured that the most popular instructions were least likely to be buggy, otherwise more things would have been broken. Looking at the bottom of list, two stood out: nabs (negative absolute) and doz  (difference or zero). They both had no coverage in DingusPPC’s test suite³ and were obscure — they are actually part of the POWER instruction set that was the predecesor of PowerPC, and were only included in the PowerPC 601 chip to help with IBM’s migration to it.

Looking at the implementation of nabs, it seemed relatively straightforward — it would always set the sign bit to to 1, thus making the number always negative:

ppc_result_d = (0x80000000 | ppc_result_a);

However, negative numbers are not actually represented as positive numbers with the sign bit set, they use two’s complement. Using Julia Evans’s handy integer.exposed we can see the representation of 123, -123 and 123 with its sign bit flipped, and thus why this implementation is not right.

I switched it out to the more readable explicit sign test and negation:

ppc_result_d = ppc_result_a & 0x80000000 ? ppc_result_a : -ppc_result_a;

Things still weren’t working, but as I had previously mentioned, doz was also suspicious (and it was the instruction that was executed right before nabs). I noticed a copy/paste error in which register the result was being stored in, and once I fixed that, everything began to work!

Correctly rendered System 7.1.2 Finder, showing the contents of the "Power Macintosh CD" drive

This whole investigation took about a week’s worth of evening hacking time, and there was a bit of luck involved in the end. I very easily could have ended up on a wild goose chase looking at some of the other infrequent instructions, and given up before I got to nabs and doz. Or the implementation bugs could have been much more subtle. However, it worked out, and it was really satisfying to be able to contribute to the DingusPPC core.

  1. System enablers were used to patch in support for Macintosh models released after a base version of the OS shipped.
  2. Much of the Toolbox code was written in 68K assembly and so could not be easily ported when Apple made the 68K-to-PowerPC transition in 1994. Emulation was thus the solution not just for older software, but also for large parts of the OS too. It was not until the transition to Mac OS X in 2001 that the need for 68K emulation went away.
  3. The DingusPPC test suite is partially based on the one for the Dolphin emulator. Since that emulates a G3-class CPU, it does not need to worry about the quirks of first-generation PowerPC CPUs.

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.