Saturday, July 2, 2022

A brief dive into Power Mac INITs and NVRAM scripts, or, teaching Mac OS 9 new device tricks

Although I'd much rather use a real Power Mac, and of emulators I tend to use my own bespoke hopped-up fork of SheepShaver for the POWER9 CPU with my daily driver, QEMU is still important for Mac OS 9 emulation because it handles the full system rather than the quasi-paravirtualization approach of SheepShaver. Indeed, certain classes of application can only run in that context.

However, because QEMU is a much lower-level emulator, that means that things like mice are also emulated, and that tends to chug a bit even with QEMU's JIT (currently KVM-PR, the virtualization system for Power ISA, does not work properly with Mac OS 9 in QEMU for reasons that have not yet been determined). If you use the default mouse support, the mouse is entirely maintained by the operating system and the polling frequency is just slow enough to be frustrating, and you have to grab and ungrab it all the time. The normal solution is to use an absolute pointing device like the QEMU tablet and solve all these problems at once, but the classic Mac OS doesn't support it and the existing Wacom driver doesn't work. (SheepSforza, on the other hand, hooks the system mouse and system mouse pointer into the guest OS, so it's much more responsive and transparent if I do say so myself.)

To support this and other QEMU virtual devices requires updating the guest Mac. And, happily, there are now two approaches at least for doing so for the QEMU tablet, which make an interesting comparison on how we could get other devices supported on the classic Mac OS.

The original solution was kanjitalk755's extension (yes, the same dude who updated SheepShaver, and upon whose updates SheepSforza is based). This would be considered the conventional approach and is almost a canonically simple INIT, so it also makes a good pedagogical example.

On startup, the extension's main() is run. This is actually 68K code, coming from a resource called (unimaginatively) INIT. This block of code knows nothing about USB; its entire reason for existence is to display the extension icon and create a Universal Procedure Pointer for a single PowerPC function in this file. UPPs are needed to tell the operating system the type of code it will be running (remember that the "normal" state of the classic Mac OS, even on Power Macs, is to run 68K code), and since this code is 68K, we need to tell the nanokernel it will be running native code instead.

You'll notice that it must call main() in that second file to start up the USB driver and naturally it does, but the function name main doesn't appear anywhere in this call, so how does it know what function to ask for? In simple terms, it finds the right function because there's nothing else to find. It gets this pointer by using the Mac OS Code Fragment Manager (all PowerPC code, even in resources, is managed by CFM) on a second resource 'PPC ' — in typical Mac applications PowerPC code is stored in the data fork, but PowerPC code can run perfectly well from a resource too. Here it is in ResEdit.

There is only one such resource, and if you look through the entire second file, only one function is not declared static: main(). So when we call GetMemFragment, even though we just pass a single Pascal string \p, we're guaranteed to get a pointer to the only non-static function there because there's nothing else for the CFM to return. The initializer then declares it to be a PowerPC function and calls it.

Now in native code, we locate any class driver for USB HID peripherals and find the device dispatch table as documented by Apple (PDF). Assuming the guest Mac is configured to use an ADB keyboard and mouse (the default if the emulated VIA is the CUDA) and no other interfering HIDs, this pointer will be to the class driver attached to the QEMU virtual tablet, which is USB. Using the dispatch table, the extension then gets and stores the report descriptor and installs a report handler. Control then returns to the INIT resource, which displays the icon and returns control to the operating system.

With the driver thus installed, as the QEMU emulated tablet sends reports our handler locates the logical cursor device using the Cursor Device Manager, moves its pointer and sets its buttons, and makes sure it calls any other previous report handlers. Rather than looking for the QEMU tablet's specific vendor and product code, it simply distinguishes the reports it wants by checking the length of the report.

This is all very Mac-like and clean, and a straightforward example of how one might write a simple Mac OS USB HID driver generally. About all it's missing is code to uninstall the handler, which it could do by calling pHIDRemoveReportHandler in the HIDDeviceDispatchTable struct; it could also look for hot plugging (which, theoretically, could happen within QEMU) or specific vendor/product IDs by using USBInstallDeviceNotification with a callback instead. That said, it has some disadvantages, two minor and one major.

The first minor disadvantage is that you'll need a 68K compiler to build it from scratch and later versions of CodeWarrior don't have one. Theoretically you could use SC from the Macintosh Programmer's Workshop to build the 68K portion, but you'll need to do some modification for the missing A4Stuff.h, and then you'd need to come up with the MPW incantation to link it all together with the PowerPC code resource because there's no 68K linker anymore in CW either. That said, you could simply build the PowerPC resource on its own and then use ResEdit as your "ghetto linker" to replace the 'PPC ' resource in the pre-built extension. Naturally this isn't particularly elegant nor a completely reproducible build, but at least this way you can still make changes to it or use it as the scaffolding for your own extension (change the icon too in ResEdit while you're at it).

The second minor disadvantage is because this driver is not particularly picky, it can conflict with other USB human interface devices that may be connected, which is why you can't run QEMU with the VIA set to PMU as that configuration provides USB keyboards and mice (ergo CUDA, which provides an ADB mouse and keyboard that won't interfere). That means it won't work as is on a real Power Mac with a USB keyboard and mouse either, only one with ADB devices. Again, it should be possible to fix this by refactoring around USBInstallDeviceNotification with a callback and the right vendor and product IDs, or using an approach that we'll discuss momentarily.

The major disadvantage is what happens when you must boot with extensions off. Since QEMU in tablet mode doesn't provide a mouse by default, that means you would essentially be limited to the keyboard without reconfiguration. I don't think I need to tell any regular reader here that the classic Mac OS is a lot harder to work with when it's limited to a keyboard.

That brings us to a rather more unusual solution, Elliot Nunn's take. Nunn's version is a radically different approach: it installs a fake ROM resource containing a USB driver specific to the QEMU tablet, and to ensure it is available on startup, is actually embedded in an NVRAM script that Open Firmware runs. You provide this as the NVRAMRC to QEMU with a lon-gass command line argument (which is partially in Base64 and compressed with LZSS to be under a 2K Open Firmware limit bug).

There are several distinct parts to this version. Recall that Open Firmware's "native" tongue is Forth. Stored in NVRAM is a small Forth script that decodes and decompresses a payload (marked with a ~ character). The script is executed by Open Firmware on startup, causing this payload to be installed as an Open Firmware property and run in the early boot process before any of Mac OS 9's built-in USB class drivers. It finds the ROM driver resource USBHIDMouseModule referenced by a known location, sets the low-memory global RomMapInsert at 0xb9e to true, and patches into the 68K system trap 0xA06A HSetState (PDF, part of the Memory Manager). Every time this trap is called, if its patch routine (written in 68K assembly) sees that the handle argument matches that resource, it substitutes its own handle.

The handle points to this driver, which is primarily PowerPC, but has a 68K component for a particular reason. Unlike the driver above which simply worms into an existing HID class driver, this driver is a USB driver all its own (it has to be, it's replacing the one in ROM). It knows to look for the specific vendor and product, but it makes sure it patches the device class to "vendor specific" so that any later driver loaded from disk won't get priority over the built-in driver. This patcher is written in 68K assembly purely to save space and called with (you guessed it) a UPP during initialization. With this new driver installed USB events are handled by a more formal state machine, within which a USB interrupt read event handles the report and moves the cursor. The cursor movement method here is also more interesting: instead of using the Cursor Device Manager, it sets low globals to the new position of the mouse pointer and directly calls the vertical blank manager routine CrsrVBLTask through its vector at JCrsrTask to redraw the pointer right then and there. This is how the mouse pointer was moved on systems that lacked the Cursor Device Manager but all classic-capable Power Macs still support this method for compatibility.

But building it is the really fun part. MPW supports build scripts, which are analogous to things like Makefiles. The master build script starts off by calling the build script to build the USB driver. This creates a single ndrv component in a conventional manner, compiling it with the PowerPC compiler MrC and linking it with PPCLink. The next step is the early loader, which needs to know its own length, because the USB driver is concatenated to it. It builds itself with a dry run dummy value first and then compiles in its own length on the second run, appending the driver to itself.

Obviously MPW doesn't know how to do the compression and Base 64 encoding, so the next step is to build the LZSS compressor (note the use of either the SC or C compilers to make the tool, in case one or the other bugs out). The compressor isn't a classic Mac application; it just has a regular main() and uses stdio.h like anything you'd run from a command line anywhere else, and such programs can be run directly within MPW as tools. So we'll link it as an MPW tool and immediately call it to compress the combined loader-driver. The Base 64 encoder-argument generator is compiled in the same way, which trims comments and extraneous whitespace out of the Forth script and substitutes in the encoded compressed binary at the right location. It then emits this final "object" over standard output to the MPW worksheet as the command line options to pass to QEMU.

It too has some drawbacks. First, that's a lot to pass on the command line, although modern systems won't have a problem with it. Second, while the trap used to swap in the new driver is a documented and perfectly valid 68K trap, this trap does get called for other things, so there ends up being a small amount of additional overhead. Additionally and analogously to the original driver, this driver does not cleanly handle other USB HIDs, although its severe OF-imposed space constraints also mean there's little room to handle anything else anyway. (You would do so in the state machine code; exercise left for the reader. You could marry such a driver to the conventional extension approach used in the previous iteration for a best-of-both-worlds hybrid.) On the other hand, this more low-level approach means the driver is always available, even if extensions are disabled.

Much of the "institutional memory" on creating classic Mac OS system extensions and components is fading from Google and other sites, and Apple has taken down a great deal of it as well (if it posted it at all). Trying to document what's out there and preserve it for retrocomputing posterity gives us a chance to support new and better environments and devices either on real machines or in emulation. The classic Mac OS was a strange OS internally, but a real joy to actually use, and better system integration makes it that much more accessible to curious future generations.

1 comment:

Comments are subject to moderation. Be nice.