Pippin Kickstart 1.1

I closed out 2020 by piecing together a minor update to Pippin Kickstart (then spent most of January writing this blog post 😛 ). Version 1.1 patches the SCSI Manager on Pippins with ROM 1.0 (most models with a white case) so that they too may boot from SCSI devices other than the internal CD-ROM drive, such as an external hard drive. If you have a 1.2 or 1.3 Pippin and are happy with Pippin Kickstart 1.0.x, then 1.1 adds no new functionality other than some cute graphics (see below).

Download it here: pippin-kickstart-1.1.zip. Extract and burn pippin-kickstart-1.1.iso to a CD-R using the software of your choice.

Source code is available here, licensed under the GPLv2: https://bitbucket.org/blitter/pippin-kickstart

The Pippin was Bandai and Apple’s ill-fated collaborative attempt to break into the video game console market by marrying two things I love: Macs and video games. Bandai launched the Pippin in 1996 amid fierce competition from other fifth-generation consoles like the Panasonic 3DO, Sega Saturn, Sony PlayStation, and Nintendo 64. Based on Macintosh technology, the Pippin is capable of running Mac software and vice versa, but Apple built some software-based security into the Pippin’s boot process making it difficult to use the Pippin just like any another Mac. In part because of its high price and lack of developer support—both internally and externally—the Pippin was considered a commercial failure and Apple subsequently canceled the project in early 1997, with Bandai following shortly after in 1998.

I grew up playing games on Macs through the 90s and early 2000s, so I’ve always had a soft spot for the classic Mac OS. I learned how to program on a Mac and nurtured those coding skills over several years, which I later parlayed into a modest career in video game development. All the while I noticed that while other vintage consoles were getting renewed attention due to burgeoning homebrew developer scenes of their own, the poor Pippin was being left out in the cold. By the late 2010s I figured that since nobody had paid much attention to Apple’s foray into video games, then I may as well, especially given my nostalgia for classic Mac gaming. So I cracked its signing key in May 2019 and shortly thereafter released a boot disc called Pippin Kickstart that made it easier for folks to test their own Pippin CD-ROMs.

When I released Pippin Kickstart 1.0.1 at the beginning of July 2019, I thought it was a done deal. Owners of 1.2 and 1.3 Pippins could boot from any SCSI device they wanted, and 1.0 Pippin owners could boot from any CD-ROM they wanted, owing to its lack of support for other SCSI devices. Unsigned booting was finally a reality on the Pippin, and I could rest easy not having to worry about this little project anymore.

That is, until September 2020, when @LuigiThirty on Twitter picked up a 1.0 Pippin and wrote about this obstacle she encountered while working on some homebrew:

Not only was her Pippin refusing to boot from a known-good external SCSI hard drive, but the drive wouldn’t work with her Pippin at all. Hard drives are the earliest and most basic SCSI storage devices available for Macs, but even when unformatted or without drivers installed, formatting utilities should still recognize that a SCSI device is attached and available for use. Perhaps the 1.0 Pippin’s ignorance of external SCSI devices had less to do with driver support in ROM and more to do with artificial limitations. As a hacker, artificial limitations offend my sensibilities, so I revisited Pippin Kickstart with an aim to do something about this.

The Problem

The consumer model of the Bandai Pippin is designed to boot exclusively from its internal CD-ROM drive. In late 1996, a revised ROM—1.2—was offered as an upgrade allowing the use of external SCSI devices, but particularly the Deltis 230 MO Docking Turbo which provides a magneto-optical drive “docked” to the underside of the Pippin. A developer dongle could be attached to Pippins equipped with ROM 1.2 to enable booting from external SCSI devices. But the earlier ROM 1.0 completely ignores all SCSI devices (and dongles) other than the internal CD-ROM, precluding altogether the use—not to mention booting—of external SCSI volumes. Signs point to the ROM’s low-level SCSI Manager as the culprit, since while mandatory initial booting from CD-ROM is standard across all retail Pippins, only ROM 1.0 refuses to see additional SCSI devices after the Pippin has fully booted.

However, the “GM Flash” ROM supplied with developer and test Pippin kits is nearly identical to the final 1.0 ROM, save for a few minor changes to enable debugging. Indeed, the GM Flash ROM has an Open Firmware timestamp of 1996-01-28, while the 1.0 ROM has a corresponding timestamp of 1996-01-29—just one day apart, suggesting that the two ROM versions were built from the same codebase. Among the differences, the GM Flash ROM can enumerate all SCSI devices at all times and may attempt to boot from any of them.

The Solution

All known Pippin ROMs load their SCSI Manager code from a ‘nitt’ resource located in ROM. Upon closer examination it appears that the ‘nitt’ resources with ID 43 (hereafter referred to as “‘nitt’ 43”; SCSI Manager 4.3 was codenamed “Cousin Itt.”) in the GM Flash and 1.0 ROMs—where the SCSI Manager code is stored—are the exact same size and, aside from timestamps, differ by only nine bytes. These nine bytes make up code that check a SCSI device’s ID to determine whether or not it should be considered. In the GM Flash version, this code verifies that the ID is between 0 and 7 inclusive (all legal SCSI IDs), whereas in ROM 1.0, this code only passes devices with an ID of 3, the internal CD-ROM drive. Given that the GM Flash and 1.0 ROMs are so closely related, it’s reasonable to hypothesize that the 1.0 ROM can use a SCSI Manager from the GM Flash ROM. Replacing ROM 1.0’s ‘nitt’ 43 with the GM Flash version should therefore be the most straightforward fix. Barring that, patching those nine bytes to match those of the GM Flash ROM should be sufficient to make ROM 1.0’s SCSI Manager functionally equivalent to that of the GM Flash ROM.

Finding The Problem

Pippin Kickstart was first conceptualized as a custom SCSI CD-ROM driver. My thinking was, since Macs automatically load device drivers from the first few partitions of an Apple-formatted disk, why would the Pippin behave differently? The longer story is written in my blog post about that, but the short answer is that the Pippin simply ignores patch and driver partitions. The Pippin has its own .AppleCD driver in ROM, which has to be loaded and active before it can boot, well, anything. Perhaps the thinking at Apple was, “since .AppleCD has to be working to boot the Pippin into an OS, there’s no sense in patching it that early. Let the OS do that if need be.” Since the signing process is only applied to the boot volume and no other partitions, maybe it was a conscious effort to block patches and custom drivers from executing their own unsigned code early enough to work around the Pippin’s security. Whatever the reason, I discovered quickly that Pippin Kickstart’s payload couldn’t sneak in through a back door.

Ultimately, I reverse-engineered the signing keys and implemented Pippin Kickstart as a simple bootloader, signed using Apple’s private key so that it launches on any retail Pippin without resorting to any sneaky tricks. My own code implements its own boot candidate search loop, mimicking the loop in the Pippin ROM’s Start Manager (in fact calling some support functions in ROM as necessary) but omitting an authentication and driver check. I implemented my own loop because the ROM’s loop, being read-only, can’t be patched in-place. But since the ROM’s search loop is part of the early startup code, frozen in ROM rather than loaded as a resource, the locations of code in ROM needed by Pippin Kickstart are always at known, fixed addresses. They never change, so I can hardcode them directly into Pippin Kickstart’s logic.

🎵 Call me (call me) on the line / Call me, call me any, anytime 🎵

It’s a different story with the SCSI Manager. The SCSI Manager is a low-level library used to enumerate and wrangle access to any and all SCSI devices attached to a Mac or Mac-based system like the Pippin. If you want to get into the party where the SCSI devices are, the SCSI Manager is both the bouncer and the emcee. In order to get to the point where any user-provided code—including Pippin Kickstart—can be loaded at all, it has to be read from a drive, a process which the ROM starts by first querying the SCSI Manager for where and how it can ask a drive for anything. The SCSI Manager therefore has to be loaded and active before the Start Manager even checks to see if it can boot from anything. Furthermore, as I point out above, the 1.0 ROM’s SCSI Manager appears to reject external devices even after we’re done booting, so the SCSI Manager has to persist as long as it may be in use.

The Pippin’s earliest boot code in ROM—from the time it powers on—executes natively on the PowerPC, configuring some low-level hardware and initializing a 68K emulator. But soon after that it enters the emulator to launch the boot code located in the Toolbox, which is predominantly written in 68K assembly. In fact most of the Pippin’s ROM targets the 68K instruction set architecture (or “ISA”), but some portions target the Pippin’s native PowerPC ISA. To understand why this startup code (and, by extension, Pippin Kickstart) runs in emulation rather than natively, we need to look back in time to when the Pippin’s software was being designed.

In 1994, Apple released their first PowerPC-based Mac. The Power Macintosh 6100/60 sports a PowerPC 601 processor running at 60 MHz and shipped with System 7.1.2. The development of the 6100 is a very interesting tale, with some parallels to how the most recent Apple Silicon-based Macs came to be, particularly when it comes to emulation. The Mac had a relatively rich library of both first-party and third-party software, a relatively mature OS that was going to be ten years old by the Power Macs’ release, and a developer community that was used to working with the Mac and how to flex its muscles. Throwing that all away and starting up again from scratch—especially with Windows 95’s release on the horizon—would not have made the best business sense given the short period of time in which Apple had to make the transition. Thus the decision was made to use emulation to run the Mac’s existing software library—including most of its operating system—on the new PowerPC-based machines, with the idea that modules of the OS could be replaced piece by piece over time rather than all at once. In turn, existing software and development knowhow would continue to retain its value, and developers were under less pressure to produce PowerPC-native versions of their software right away. This strategy paid off in a big way; the new Power Macs were a hit, and soon became the basis for the Pippin platform.

The Power Macintosh 6100/66av, a.k.a. the Pippin prototype prototype

Due mostly to time pressure, only the most-often used portions of the Mac’s System Software were rewritten as native PowerPC code for the initial lineup of Power Macs. Namely, QuickDraw was given the native treatment, while components that still had many 68K dependencies—such as the SCSI Manager and disk drivers that had heretofore been written to work with it (targeting the 68K, mind you)—were left emulated, for the time being anyway. It didn’t take long for Apple to let the SCSI Manager into the native club. Just 15 months after the first Power Macs hit the market, the Power Macintosh 9500 arrived on the scene in June 1995, utilizing the PCI standard as the first of the “second-generation” Power Macs and featuring a native SCSI Manager 4.3 built into its ROM. The new PCI-based Power Macs—particularly the Power Mac 7500—lent many of their features and specifications to what eventually became the final hardware and software design of the Pippin.

The opportunity to transition to a new processor architecture in turn gave Apple the opportunity to learn from the effects of previous design decisions and implement more modern corresponding changes. The original Macintosh operating system was designed in 1983 to run one application at a time on a computer with no built-in storage, 128 kilobytes of RAM, no virtual memory, and a 68000 processor which was limited to relative branches up to a range of 32 kilobytes in either direction. User-provided 68K code must be loaded into RAM before it can be executed; Macintosh engineers invented the Segment Loader as a sort of virtual memory to allow larger applications to be broken into code resources swapped in and out 32K at a time. Enterprising hackers later figured out how to work around this to get much larger segment sizes, but segments still have to be loaded into RAM regardless. Generally, all the code associated with a 68K application has to live in that application. “Dynamic” or “shared” code libraries were not explicitly supported by the operating system, so the best one could hope for were system extensions/patches offering either new official system APIs, or third-party de facto standard interfaces informally agreed upon by multiple applications. This was not the most stable environment in which to develop scalable software.

The original Macintosh, a.k.a. the Pippin’s granddaddy

By contrast, the upcoming Power Macs would feature cooperative multitasking with System 7, paged memory support, an internal hard drive, multiple megabytes of RAM, and a completely different ISA which could support much larger branch sizes. Apple had almost ten years of Macintosh development experience by the time they began designing software for the upcoming Power Macs, and had taken lessons to heart in terms of how to improve their operating system to better scale to modern software development practices. The new PowerPC code that would run natively on the improved operating system would not use the old-style Segment Loader. Instead, code blocks on the hard drive could be mapped and executed directly as pages in memory without having to copy them to physical RAM. Self-contained blocks of PowerPC code, known as “fragments,” are defined in terms of multiple “sections”—both code and data—and could be paged/loaded into memory once and then optionally shared with multiple applications, either as application plugins or standalone libraries in their own right. Each fragment could have its own block of globals and constants loaded into RAM for it to use, with their initial values specified in the fragment’s definition. The standard interface in the Mac OS to fetch these fragments and their entry point(s) at runtime became known as the Code Fragment Manager, or “CFM.”

There are three ways to load a fragment, and the CFM provides three respective functions to accomplish each one:

68K application code lives by and large in that application’s resource fork, leaving the data fork typically empty; one or more ‘CODE’ resources are swapped in and out at runtime by the Segment Loader, using a jump table in ‘CODE’ 0 as an initial reference point. To support “fat” binaries that can run on 68K Macs but also natively on Power Macs, a PowerPC-aware application stores its code in the otherwise unused data fork of an application, using a ‘cfrg’ resource as an initial reference point. When booting from ROM, there is no concept of “resource forks” or “data forks” until Mac filesystem code is loaded, but the Resource Manager doesn’t require resources to live in a file to be located and used. There exists a provision to load resources from a resource map located in ROM instead, and this is how the native SCSI Manager and other modular components are loaded despite the rest of ROM running in emulation. GetMemFragment is therefore the function used by the ROM; the SCSI Manager isn’t a shared library (at least not by the CFM’s definition), and we obviously can’t call GetDiskFragment until we have the ability to read from disk. During startup, the ROM asks the Resource Manager for a handle to the ‘nitt’ 43 resource in ROM, and then that resource is passed to GetMemFragment to prepare the fragment contained in that resource.

Comparing the ‘nitt’ 43 resources of the GM Flash and 1.0 ROMs reveals that, aside from timestamps in the fragments’ respective headers, the two resources differ by only nine bytes in four places. Curiously, three of those nine bytes are replaced by the value 3 in the 1.0 ROM, where they originally have the value 7 in the GM Flash version. The SCSI ID 7 is the upper bound of what IDs may be assigned to devices (7 is always reserved for the host in Apple’s implementation), whereas ID 3 is that of the internal CD-ROM drive. I suspected this might have something to do with why only the CD-ROM drive is recognized by the 1.0 ROM, but without knowing what the other changed bytes correspond to, I couldn’t know for sure. I’d have to run the two versions through a disassembler.

Three of the four significant differences. Left: GM Flash. Right: 1.0

Finding a sufficient PowerPC disassembler was somewhat of an adventure in itself, but I wound up circling right back to something that I had long since installed on my G3 Power Mac: Apple’s own Macintosh Programmer’s Workshop, or “MPW.” I didn’t know this, but MPW comes with a tool called DumpPEF specifically designed to tear apart fragment containers for analysis. The best part: it includes a very good PowerPC disassembler. All I had to do in MPW Shell was pass along the contents of ‘nitt’ 43 as a data-fork-only file and redirect DumpPEF’s output to a text file, like so:

DumpPEF -do All -ldr All -pi u -dialect PPC601 -fmt on -v "Arthur:Development:Projects:Pippin Kickstart:1.1:nitt43" > "Arthur:Development:Projects:Pippin Kickstart:1.1:nitt43dump.txt"
My boot drive is named Blackwood, after the protagonist in the Journeyman Project games, and my working/data drive is named Arthur, after his wise-cracking sidekick.

I know 68K assembly much better than I do PowerPC, but teaching myself just enough PowerPC to understand what goes on in the SCSI Manager was not as bad as I thought it might be. Matching up the offsets where bytes differ to their corresponding locations in the disassembly, I quickly confirmed my suspicions. If Apple’s SCSI Manager 4.3 Reference guide (and the short amount of time it took Apple to build a native version) is any indication, the SCSI Manager itself was originally written in C. According to Apple’s own documentation, then, one of the common structures used by the SCSI Manager is called a “DeviceIdent,” containing among other things the “targetID” of a particular SCSI device.

If we look at the last two differences as one code “site,” making the total amount of differences map to changes at three code sites, then in the GM Flash ROM, the SCSI Manager appears to be doing the equivalent to this C code at all three sites:

if (devIdent.targetID > 7)

where target ID 7 is the upper bound of what IDs are legally allowed as mentioned earlier. Translated into English, when a request for a SCSI action comes in, the SCSI Manager looks to see if the intended device’s ID is out of range, and if so it refuses the request.

Contrast that with this C code, equivalent to what the 1.0 ROM does at each code site:

if (devIdent.targetID != 3)

Notice the difference? “If a device’s ID is not 3, refuse the request.” This is clearly why no other SCSI devices are recognized by the 1.0 ROM; the low-level code responsible for routing SCSI requests flat out refuses to do so unless it’s to or from a device with ID 3. I had figured out where the problem is, and fortunately, Apple already showed me how to fix it by way of how the GM Flash ROM behaves. 🙂 The next logical step then was to develop a patch.

Implementing The Solution

The most obvious way to patch the SCSI Manager would be to burn a patched 1.0 ROM. In theory it’d be easy—the ‘nitt’ 43 resource is the same size in both the GM Flash ROM and the 1.0 ROM. From a content perspective it’d literally just be a copy/paste job, but I’m primarily a software guy, and I’d rather lose just my time debugging software than lose my time and money feebly trying to make and support a reliable physical tool all while out of my wheelhouse. Acquiring, programming, and installing a custom Pippin ROM board can not only be intimidating to a casual collector/homebrewer (including yours truly), but also significantly more expensive (and legally questionable) than burning Pippin Kickstart to a CD and running it on stock hardware. Besides, if I was to burn a new ROM anyway, why would I stick with 1.0 when I could use the much more fully-featured version 1.3 instead? Pippin Kickstart is a free, open, and purely software-only utility, so I think it’s worth trying to patch in software. The fix should only cost the price of a blank CD-R. 🙂

When GetMemFragment is called to prepare the native SCSI Manager fragment in ROM, no code is copied or moved around in memory. The ‘nitt’ 43 resource stays right where it is and the SCSI Manager is executed directly from its home in ROM. How then does one patch this read-only code in software? Is it even possible?

Writing into read-only memory is out of the question for reasons that should be obvious. What about replacing the SCSI Manager with my own implementation? In order to cleanly install my own replacement, I would have to shut down and clean up the existing ROM-based SCSI Manager so as to make sure no remnants remain. Is this possible? I don’t know. The SCSI Manager is designed to remain permanently resident, so while I know a SCSI Manager system extension exists for older 68K Macs, I don’t know how or when it installs itself and furthermore, I couldn’t find a mechanism by which the PowerPC-based Pippin could accomplish the same task at boot time. So that’s out.

Hang on. If, hypothetically, the CFM loads a fragment from ROM that depends on a fragment that’s loaded from RAM or from disk, how does the ROM fragment know how—and where—to call into those dependencies? What about non-ROM fragments that in turn depend on ROM fragments and other non-ROM fragments alike? It would make sense for the CFM to keep track of these inter- (and, as we’ll see, intra-) fragment locations in a unified way.

Each loaded fragment has at least one associated “data” section allocated in RAM. This section may contain globals or other statically-initialized data referenced by the fragment, but the data section also contains a special area called (by IBM) the “Table of Contents” or “TOC,” though that’s a bit of a misnomer. Apple says that the TOC is more like an address book, acting as a lookup table for functions and data living both within a fragment and outside that fragment. Each fragment has its own TOC, so before a routine in another fragment is called, PowerPC register GPR2—otherwise known as the “RTOC”—is saved, then preloaded with the bottom of the destination fragment’s TOC so that the fragment knows how to find its own globals and data. The calling fragment’s RTOC is restored upon return of the called routine.

A fragment’s code must be position-independent; that is, it should be able to be loaded into and run from any address. Therefore, a fragment’s code section does not usually reference hardcoded memory locations. Instead, it fetches an address it needs from its TOC at runtime by looking up that address in its TOC and reading it from RAM. It does this by using the RTOC register plus a known offset as an index into its TOC. The CFM is responsible for preparing and maintaining these addresses in the TOC at fragment load time, and at any time the loaded fragment or any of its dependencies have to be relocated in memory.

When pointing to data, a TOC entry is just a pointer to that raw data. But when pointing to a routine, because that routine could be exported from a fragment (someone can call us) or imported from another fragment (we’re calling someone else), it needs to know at minimum where its TOC lives in RAM, so a simple raw pointer to the routine is not enough. Enter the “transition vector.” A transition vector is very simple: it contains at least two pointers, the first being the address of the routine within the fragment, and the second being the address of a fragment’s context. In most cases, a fragment’s TOC provides enough context, so the second pointer is used to prepare RTOC immediately prior to entering the routine. A transition vector may optionally contain other fields at the discretion of the compiler and environment used to build the fragment; the only expectation is that it contains at least the first two.

A strange environment

Transition vectors must contain at least two pointers—one to the routine itself and one to its context—but the SCSI Manager’s transition vectors each contain three pointers. The third pointer is unused, but is designated as an “environment” pointer. Early PowerPC Mac development was done on IBM RS/6000 workstations, so this vestigial “environment” pointer may have come from that early toolchain.

If you’ve been paying attention so far, you might be able to figure out where this is going. The SCSI Manager’s transition vectors all point to code in ROM, because ‘nitt’ 43 itself is not loaded into RAM and there’s no problem executing its code directly from its home in ROM. But the transition vectors themselves live in RAM, which means they can be changed. Patching the necessary transition vectors in RAM is tantamount to patching the routines that the SCSI Manager itself exports to be called by the OS. So naturally, the next question is, how do we find those transition vectors?

Calling GetSharedLibrary, GetDiskFragment, or GetMemFragment prepares a fragment (if found) and returns a “connection ID” to that fragment. Each time an interface is established to a particular loaded fragment, it’s called a “connection” to that fragment. Connections are reference counted and when all connections to a particular fragment have been closed, that fragment is unloaded. All three of the “GetFragment” APIs create a new connection and each takes a parameter called findFlags that can equal one of three values:

  • kLoadLib: load a fragment if it’s found and not yet loaded. If it is loaded, create a new connection to the already-loaded fragment.
  • kFindLib: find a loaded fragment. If it is loaded, create a new connection to the already-loaded fragment. If not, return fragLibNotFound.
  • kLoadNewCopy: load a fragment if it’s found and not yet loaded. If it is loaded, create a new connection to the already-loaded fragment but also create a new data section specifically for this connection.

The CFM provides the FindSymbol API for locating a symbol in a fragment by name, given a connection ID. After preparing the SCSI Manager’s native fragment, the ROM calls FindSymbol to find the transition vector for the SCSI Manager’s “InitItt” entry point, then calls it to begin executing the native SCSI Manager’s code.

Hmm. In the SCSI Manager’s case, a connection is created at startup, but it is never closed at any time thereafter, ensuring that the SCSI Manager is never unloaded. Could a new connection be established to the SCSI Manager / ‘nitt’ 43 fragment-as-resource, then a known symbol—perhaps its entry point—be used as a reference to poke around in the rest of the fragment’s data section, including its transition vectors?

This seemed like the most “polite” way of getting at the SCSI Manager’s data section, so I tried this first.

 move.w  #0xFFFF, (RomMapInsert)
 subq.l  #6, %a7        /* make room for GetResource's return handle (4 bytes) */
                        /*  and GetMemFragment's return value (2 bytes) */
 move.l  #nittRsrcType, -(%a7)
 move.w  #43, -(%a7)
 movea.l (%a7)+, %a4

 move.l  (%a4), -(%a7)  /* Ptr                  memAddr */
 subq.l  #4, %a7        /* make room for SizeRsrc's value in length */
                        /*  (4 bytes) */
 move.l  %a4, -(%a7)
 clr.l  -(%a7)          /* Str63                fragName */
 pea    1               /* kLoadLib /*
 pea    0x1A(%a7)       /* ConnectionID*        connID */
 clr.l  -(%a7)          /* Ptr*                 mainAddr */
 clr.l  -(%a7)          /* Str255               errName */
 move.w #3, -(%a7)      /* GetMemFragment */
 _CodeFragmentDispatch  /* we'll assume it succeeds... */
-2817, better known by its other name fragLibConnErr

Would that it were so simple. The use of the findFlags parameter is documented for GetSharedLibrary and GetDiskFragment, but the documentation for GetMemFragment just refers to the documentation for GetDiskFragment for how findFlags is used. Despite Apple’s redirection, passing kLoadLib to GetMemFragment will not create a new connection to an already-loaded fragment at an address previously provided to the CFM. There would be no going in through the front door. Damn. I’d have to sneak in through another way.

The classic Mac OS memory map is split into several areas. There are low-level system globals near the bottom of the address space, a system heap for use by the OS above that, and the remaining RAM is comprised of one or more fixed-size application heaps (and stacks) belonging to running applications. Originally, the system heap was fixed in size, with the remainder of usable RAM reserved for the application heap and stack. Double-clicking an application from the Finder would close down the Finder and the newly-launched application would take its place in the application heap. The reverse would occur when the application closed down, relaunching the Finder in its stead. As this original design was built around running one application at a time, later some technical gymnastics were achieved to add multitasking to the system while maintaining backward and some level of future compatibility with Mac apps. The original Memory Manager APIs and low-memory globals were therefore left mostly unchanged, including a well-known global containing the base address of the system heap.

As is mentioned earlier, the SCSI Manager is designed to remain permanently resident, so it makes sense that its fragment’s data section lives in the system heap. Opening and closing applications has no effect on the existence of the SCSI Manager. Indeed, launching applications often involves the SCSI Manager to fetch those very applications from disk; the SCSI Manager is a core component of loading code on the Pippin. Furthermore, since the Pippin needs to know at all times how to load additional code and data from disk, those exported transition vectors need to be at fixed locations in memory in a nonrelocatable block of RAM. Of the many low-level Memory Manager structures documented early on by Apple, the system heap is one of them, so we can find the SCSI Manager’s data section in the system heap by doing a brute-force linear search.

Reading the SysZone system global gives us the address of the beginning of the system heap. The system heap “zone” begins with a zone header block for various bookkeeping tasks like keeping track of its size, flags, which blocks are free, and other internal uses. Immediately following the zone header are the contents of the heap itself. Likewise, each block in a classic Mac OS heap starts with a block header, describing among other things its size in memory.

Immediately following the block header are the block’s contents. By adding each block’s size to its respective header’s address, we can step through each block of the system heap, inspecting each block’s contents along the way.

 movea.l SysHeap, %a0
 lea     heapData(%a0), %a1  /* A1 -> allocated block in system heap */
 cmp.l   bkLim(%a0), %a1 /* bkLim(A0) -> system heap trailer block */
 beq.w   SkipPatching    /* if this block is the trailer, we've searched */
                         /*  the entire system heap and couldn't find the */
                         /*  SCSI Manager's pidata section */

 movea.l %a1, %a4
 move.l  blkSize(%a1), %d0   /* D0 == physical size of this block */
 add.l   %d0, %a1            /* A1 -> the next block in case we skip */

As well as being a handy PowerPC code fragment disassembler, another nifty facility DumpPEF provides is the ability to examine how a fragment’s data section is initialized. The data section is of a fixed size and, as discussed previously, the SCSI Manager’s data section starts with a Table of Contents, followed by a list of transition vectors. These transition vectors are the bytes we’re looking to patch. But in the SCSI Manager’s case, after the transition vectors comes a series of text string constants. These strings are always in the same location relative to the beginning of the data section and as read-only constants, they always have the same predictable values. Therefore I reasoned that in addition to verifying that a block within the system heap is of the same expected size as the SCSI Manager’s data section, checksumming these strings of text within that block should provide a suitable heuristic for identifying a particular block as belonging to the SCSI Manager.

 /* Verify this really is the SCSI Manager's block in the system heap. */
 /* We do that by checksumming the area in the middle of this block where */
 /*  we know the SCSI Manager looks for some read-only strings. Use an */
 /*  algorithm similar to that used to checksum the Toolbox, only we'll */
 /*  walk our pointer backwards so we can use A3 as-is if/when it comes */
 /*  time to check our TVectors. */
 add.l   -(%a2), %d0
 cmpa.l  %a2, %a3
 ble.s   ChecksumLoop
 cmp.l   #scsiStrsCksum, %d0
 bne.s   NextSysBlock

Once we’ve found our block, we know where the transition vectors live within it, so it’s time to patch them, right? Well, first we need to create the patch itself. At the time Pippin Kickstart runs, there is no application heap yet. Application heaps aren’t set up until the Process Manager starts, which doesn’t happen until after the familiar “Welcome to MacintoshPippin” extension parade has completed and our first application is ready to launch. Therefore, the system heap is our active and only heap. What’s more, as of System 7, so long as there is RAM available the system heap can grow dynamically to accommodate allocation requests, with the Process Manager shifting its base accordingly.

Our patch needs to stick around as long as the SCSI Manager exists, so naturally we need to give it a home somewhere where the OS won’t stomp over it later. Since the SCSI Manager’s data section lives in the system heap, and since Pippin Kickstart itself works from the system heap, it follows that we should be able to safely create a small block of nonrelocatable space in the system heap for our patch to live. Our patch really only needs to replace nine bytes in four locations, but we can’t just create a nine-byte block, stick our bytes there, and call it a day. The nine patched bytes belong to different functions in the SCSI Manager—functions that can and are referenced internally. Specifically, these functions are invoked internal to the SCSI Manager not by referencing transition vectors, but by good old-fashioned relative branching. It makes sense; the SCSI Manager targets the PowerPC and its functions run natively on the PowerPC, so why bother with transition vectors when you know you’re calling other PowerPC functions that are part of the same code fragment? It’s certainly convenient for the Pippin, but makes things slightly more annoying when creating this patch.

We have to ensure that no code that either leads to or leads from our patched locations can lead back to the unpatched versions in ROM. Therefore we have to account for relative branching by including all of that extra code in our patch, even though we don’t change any of it! I wrote a small C++ program to calculate exactly how much code I’d have to copy by essentially “emulating” PowerPC branch instructions, keeping track of the lowest and highest reachable addresses and using the ‘blr‘ instruction as a heuristic for the ends of subroutines. Passing my program a line-by-line disassembly of the SCSI Manager’s code section cut from DumpPEF’s output, I started the “emulation” at each of the three code sites and noted which one could be reached by the largest range. It turns out that all three sites lie within a mere 35K of code that only calls into itself; the rest of the SCSI Manager’s code section appears to be helper functions or routines unrelated to the SCSI Manager’s “core.”

The earliest address of our 35K block is offset 0xB854 into the SCSI Manager’s code section. The transition vector in the SCSI Manager’s data section with the earliest offset that should call into our patch is the vector that points to offset 0xBAD4. We know this transition vector’s index into the data section’s list, so by reading its target address and subtracting an offset (0xBAD4 – 0xB854), we can get the starting address in RAM of the code block to copy into our patch area. We also know exactly how much code to copy—35536 bytes—so the procedure becomes rather simple: copy our 35K of code into a nonrelocatable block on the system heap, then patch that. We can also easily determine which transition vectors point within that original 35K of code, so we know exactly which transition vectors to patch. It turns out that the vectors, starting with the one pointing to offset 0xBAD4 through the end of the data section’s list, all need to point into our patched code. By calculating the difference between offset 0xB854 into the SCSI Manager’s code section, and where our 35K block is in RAM, we get an offset value that makes it trivial to patch the transition vectors. We simply add that offset to each of those transition vectors so that they then point into our patched code instead of into ROM.

 /* now let's patch up the TVectors to point to our patched code */
 move.b  #tVectorsSize-1, %d0    /* # of TVectors to patch minus one */
 movea.l (%a4), %a2
 suba.l  %a0, %a2
 move.l  %a2, (%a4)+
 addq.l  #8, %a4
 dbra    %d0, TVectorLoop

After all of that, we’re still not quite done. All PowerPC processors have some form of a “data cache.” When you make changes to RAM on a PowerPC architecture, those changes aren’t necessarily written to RAM right away. Instead, the address decoder checks first to see whether where you’re reading/writing has been “cached,” or saved in a smaller but faster block of memory within arm’s length of the processor. Cache is to RAM what RAM itself is to System 7’s virtual memory; it is prioritized as a faster alternative to its counterpart, and when there’s no space left its contents are “flushed” to make room. Consequently, writing to a particular address will often write only to the cache instead, anticipating that its contents will be referenced again shortly thereafter.

To further complicate matters, the cache on the PowerPC 603 chip used in the Pippin is split between instructions (code) and data (not code). We’re patching code in RAM, but the Pippin doesn’t know that; it’s all just bytes of data as far as it’s concerned. We want to make sure that our patched area is flushed to RAM so that when the SCSI Manager comes around to execute it next, those patched bytes are waiting in RAM ready for the instruction decoder to pick them up. It’s reasonable to assume that at the time in the startup process when Pippin Kickstart runs, our block of code in the system heap does not have corresponding entries in the instruction cache, but it’s not necessarily safe to assume that our patched code will be automatically flushed to RAM before Pippin Kickstart exits. We certainly wouldn’t want the SCSI Manager to invoke the old unpatched transition vectors, or worse, execute whatever happened to be in RAM before we put our patched block of code there.

There exists an API called MakeDataExecutable that does exactly what we want here. But in order for this API to work for us, we’d have to make a connection to InterfaceLib, call FindSymbol, call MakeDataExecutable with the proper parameters, then close the connection to InterfaceLib. That’s a lot of work for just one call. Fortunately, since Pippin Kickstart runs in the 68K emulator, there’s a faster and easier way, albeit undocumented.

 movea.l %a1, %a0
 move.l  %d6, %d0
 dc.w    0xFE0C      /* undocumented F-line instruction that evicts our */
                     /*  patched area from the PPC data cache into main */
                     /*  memory so it's visible to the instruction decoder */

Apple’s 68K emulator supports the features of a 68LC040 processor with a 68020 exception stack frame. The 68LC040 is like the more powerful 68040 processor powering the Quadra line, but minus the floating-point operations built into the latter. Floating-point operations on the ‘040 are implemented by way of “F-line instructions;” that is, instruction opcodes that begin with the hex digit F. But just because the 68K emulator doesn’t support floating-point operations doesn’t mean that the emulator doesn’t support F-line instructions. 😉 Elliot Nunn helpfully pointed out that one of the F-line instructions used internally by the 68K emulator has the opcode 0xFE0C and it does just what we want: it flushes the PowerPC’s data cache to RAM. This instruction takes two parameters: a pointer in 68K register A0 to an area in memory, and a size in bytes in register D0. Easy peasy, if a little skeezy.

With that, we’re finally done patching the SCSI Manager so that it behaves identically to the version in the Pippin GM Flash ROM.

Bad F-line instructions

System error type 11 often manifests itself as a “bad F-line instruction” bomb dialog in System 7 and later. In System 6 this dialog instead displays the message “coprocessor not installed,” owing to the fact that F-line instructions can map to floating-point operations on an internal or external FPU. Despite suggesting that these errors stem from a missing FPU, very little software for classic Mac OS makes use of—let alone requires—floating-point hardware. For the broadest compatibility, programs requiring floating-point operations either use their own integer math library or call into Apple’s SANE math library instead, requiring no F-line instructions.

Usually these system errors are the result of a buggy program erroneously jumping into an area of data and interpreting it as code, setting off the bomb when carelessly stumbling upon a pair of bytes starting with the hex digit F. 😉

Adding Some Fun

Pippin Kickstart runs from RAM after being loaded from the boot blocks, which are the first two 512-byte sectors of an HFS-formatted volume. I was able to squeeze versions 1.0 and 1.0.1 each into the first 512 bytes of this area. Keeping Pippin Kickstart’s footprint limited to the boot blocks makes authoring the Pippin Kickstart disc relatively easy; I merely have to replace the boot blocks with my own, and since the Pippin loads them for me, I don’t have to make any other calls to load any additional code from the CD. Calls to the disk driver’s _Read—which Pippin Kickstart makes to check the first block of boot candidates—can only return 512-byte chunks, so 1.0 and 1.0.1 use the latter half of the boot blocks as scratch space during their respective boot candidate search loops. With the aforementioned SCSI Manager patch going into Pippin Kickstart 1.1, I need more code than will fit in those first 512 bytes, but I still want to limit myself to the boot blocks for convenience. Keeping the code tight is a fun engineering challenge, too. 🙂

If I move the boot candidate search loop and its dependencies from the first 512-byte block into the second block, I leave behind enough room in the first block to patch the SCSI Manager. By the time I’m done patching the SCSI Manager, I don’t need anything from the first block anymore, so I can jump into the search loop in the second block and that first block can be used instead as scratch space. Other than the address of my scratch space, I don’t have to change any of my tested and working search loop logic from 1.0 and 1.0.1. Hooray!

But the SCSI Manager patch doesn’t take up a lot of space, certainly not a whole extra 512 bytes. All that extra unused space felt like a waste to me, but there’s nothing more that Pippin Kickstart needs to do to allow a stock 1.0 Pippin to boot from any capable SCSI devices. My code golf skills had gotten the better of me. How could I make meaningful use of those remaining bytes?

Pippin Kickstart has a very spartan interface, though perhaps it’s a little too spartan—folks have mistaken it for BSD and have asked me what “kernel” it boots into. 😆 I take that as a compliment and a testament to how much utility I’ve packed into such a small space, but I do admit that it could look prettier. My good friend Tommy Yune is an accomplished graphic artist who graciously drew up a Pippin Kickstart “logo” (seen above) around the time I was working on the first versions. His graphics appear in the readme files I include on the disc. But other than the text Pippin Kickstart prints to the screen logging its behavior, the most anybody sees is the Pippin logo leftover from when the Pippin gets a fresh start.

Perhaps those extra bytes could translate into some extra polish. 🙂

Normally when the Pippin boots, it first draws the Pippin logo and looks for a bootable CD-ROM. If after a few seconds it can’t find one, it starts looping an animation suggesting that a CD be inserted into the built-in CD-ROM drive.

If the inserted CD is an audio CD, then the Pippin launches into its built-in audio CD player application. If the inserted CD is a data CD, then the screen goes black and the Pippin tries to boot from that disc. Many Pippin titles at this point put up a “StartupScreen” made up of the Bandai Digital Entertainment (the Pippin’s first-party publisher) logo; some unofficial titles like “Tuscon” have a custom StartupScreen file. But if the Pippin cannot boot from a given data CD, then it is ejected and the Pippin reboots, drawing the Pippin logo again and repeating the cycle. Therefore if Pippin Kickstart is inserted during the tray-loading animation, you get the least interesting visual result: the screen goes black and nothing else is shown on the screen other than Pippin Kickstart’s text console.

We’ll fix that in 1.1 by drawing Tommy’s “locked” Pippin logo, then drawing it “unlocked” after we’ve successfully circumvented the Pippin’s security. 🙂

Except for the Pippin logo itself, which we get from ROM, we’ll do all of this using QuickDraw primitives. Drawing the Pippin Kickstart logo programmatically rather than storing it as a bitmap takes up a fraction of the space, which is important given that we’re drawing two versions of it and have less than 512 bytes available to pull it all off. To begin, we create a new “clip region” that excludes the area where the Pippin logo is in the center of the screen. Since this is created in RAM at runtime, we have to clean it up before Pippin Kickstart exits, but it’s needed to later tell QuickDraw that we want to allow drawing anywhere but where the logo is.

 lea     logoRect, %a3

 subq.l  #8, %a7
 _NewRgn                        /* create clipRgn */
 move.l	 (%a7), %d5             /* save clipRgn */
 move.l	 %a3, -(%a7)
 _RectRgn                       /* clipRgn == logoRect */

 _NewRgn                        /* create tempRgn */
 move.l	 (%a7), %d6             /* put tempRgn in D6 because D7 is our ROM index */
 move.l	 %d6, (tempRgn - logoRect)(%a3) /* save tempRgn in RAM since D6 */
                                        /*  is used by the search loop */
 _GetClip                               /* tempRgn == original clip region */

 movem.l %d5-%d6/%a3, -(%a7)
 move.l	 %d5, -(%a7)
 _DiffRgn                       /* clipRgn == original clip region - logoRect */

We then set the background color to black (at this point it’s not—black is the foreground color and that’s what QuickDraw uses to initially paint the screen at startup) and erase the area inside the clip region we just created. This has the positive effect of erasing around the Pippin logo if it has already been drawn. We later draw the logo ourselves just in case, but either way this approach avoids some flicker when Pippin Kickstart launches.

 pea     blackColor
 move.l	 %d5, -(%a7)
 _EraseRgn                      /* erase around the logo */

Next we set the foreground color to white and draw a 7-pixel border around the logo area. Since we have to set the foreground color to white for the text anyway, we set it here so we don’t have to worry about it again.

 pea     whiteColor
 _ForeColor                     /* draw white-on-black */
 move.l  #((outlineThickness << 16) + outlineThickness), -(%a7)
 _FrameRect                     /* draw logo outline */

The Pippin logo is stored in ROM as 'PICT' resource -20137. It's trivial to find in ROM where the code is to draw the Pippin logo—search for a call to _GetPicture (0xA9BC) and an instruction that passes -20137 (0xB157) to it. The logo-drawing routine is located in the same place in all three known retail ROMs: offset 0xE3C from the beginning of ROM. We call this routine to fill the outline with the Pippin logo if it hasn't yet been drawn. If it has, then drawing over the Pippin logo has no perceptible side effects.

 jsr     logoRoutineOffset(%a4)  /* draw the Pippin logo from ROM */

We then have to draw the locked Pippin logo's "shackle." I created a routine for this purpose called, appropriately, DrawShackle and placed it in the second boot block so that we can call it again to draw the "unlocked" logo when it's time to exit the search loop. DrawShackle sets QuickDraw's clip region and then draws a framed rounded rectangle clipped to that region. The net effect is a shackle that appears inside the "locked" Pippin logo.

/* input: A3 -> shackle rect - 8 */
/* trashes: D0-D2, A0-A1 are scratch regs used by QuickDraw */
 move.l  %d5, -(%a7)
 _SetClip               /* clip shackle to logo */

 move.l  #((shackleThickness << 16) + shackleThickness), -(%a7)

 addq.l  #8, %a3        /* a3 -> shackle rect */
 move.l  %a3, -(%a7)
 move.l  #((shackleRadius << 16) + shackleRadius), -(%a7)
Green: drawable region, Red: clipped region

Notice how DrawShackle grabs the clip region handle from register D5. Luckily, none of the external routines called by Pippin Kickstart trash this register, leaving it available for temporary storage. The same is true of register D7, used by Pippin Kickstart as an index corresponding to the Pippin's ROM version so that we can call ROM routines from their proper locations. It is not true however of register D6, which is used by the ROM routines called by the search loop.

Now that the "locked" Pippin logo is up on the screen, we ready a clip region in register D5 that will be used to replace the "locked" shackle with one that dangles off to the side. We use the same clip region both to erase the existing shackle and to clip the "unlocked" shackle during the next and final call to DrawShackle. The _SectRgn API lets me calculate this region easily, finding the intersection of the existing clip region (set during the first call to DrawShackle that allows drawing anywhere except the logo area) and a predefined rectangle enclosing both the intended area of the "unlocked" shackle and the area of the existing "locked" shackle. Even though my predefined rectangle overlaps the forbidden logo area, this isn't a problem because _SectRgn finds the intersection of both drawable regions; that is, it calculates the region common to both. In the final clip region, only the shackle areas outside the logo will be affected.

 addq.l  #8, %a3                /* A3 -> clipRect */
 move.l	 %d5, -(%a7)
 move.l	 %a3, -(%a7)
 _RectRgn                       /* clipRgn == clipRect */

 movea.l GrafGlobals(%a5), %a0  /* A0 -> qdGlobals */
 movea.l thePort(%a0), %a0      /* A0 -> qdGlobals.thePort */
 move.l	 clipRgn(%a0), -(%a7)	/* push existing clip region */
 move.l	 %d5, -(%a7)            /* find intersection with clipRect */
 move.l	 %d5, -(%a7)            /* make it our next clip region */

The active clip region at this point is still what DrawShackle uses to draw the "locked" Pippin logo, so everything outside the Pippin logo is still fair game as far as drawing goes. This includes text, so naturally we still get the familiar text console as Pippin Kickstart goes through its expected motions.

When it comes time for Pippin Kickstart to exit and boot from the candidate it finds, the string "Booting..." is printed and the existing shackle is erased, using the clip region that we prepared earlier. Dangling the shackle to the left side of the Pippin logo would overlap our lovely text console, so we call DrawShackle to dangle an "unlocked" shackle off to the right instead.

 /* Erase the "locked" Pippin. */
 move.l	 %d5, -(%a7)

 /* Draw an "unlocked" Pippin. */
 lea     unlockedRect-8, %a3    /* because A4 is our link pointer */
 bsr.s   DrawShackle
Green: drawable region, Red: clipped region

This "unlocked" shackle is missing something, or rather it needs to be missing something. 🙂 In Tommy's logo, the shackle has a "notch" cut out of it, as would a shackle on a real padlock. Cutting a rectangular notch out of our shackle is simple enough; just erase a tiny rectangle where the notch should be.

 pea     notchRect

There's no "notch" primitive in QuickDraw (nor is there a corresponding _DrawNotch API), so drawing the slanted part of the notch requires a little bit of outside-the-box thinking. One option is to set the pen size to the notch width and draw a line between two points. That would work, but there's an even more efficient way, at least in terms of instructions used.

Among its supported primitives, QuickDraw can draw ovals, circles, and rounded rectangles. What do all these shapes have in common? They all involve drawing one or more arcs of a particular width, height, and arc length. Since ovals, circles, and rounded rectangles are themselves at least partially made up of arcs, QuickDraw also exposes the ability to draw just an arc through its _PaintArc API. If we draw an arc at least half the height of the notch we erase with a length extending to the edge of the notch, we get the slanted part we need. There is a tiny bit of overdraw into the shackle area above the notch, but since both the shackle and the arc are drawn with the same white foreground color, it doesn't matter. In the end, using an arc instead of a line segment gets the job done in about half the required instructions, with even less overdraw than drawing a line segment would produce.

 pea     arcRect
 clr.w   -(%a7)
 move.w  #-45, -(%a7)

Finally, we clean up after ourselves and exit. The clip regions we create are allocated dynamically in RAM, so to be a good citizen we dispose of those, but not before restoring the original clip region from prior to launching Pippin Kickstart. The next thing QuickDraw puts on the screen could be an alert, a StartupScreen, or something else entirely; it's up to whomever we hand off the boot process. The least we can do before we say goodbye is return the Pippin to a state reasonably close to how we found it.

 /* Clean up. */
 move.l	 %d5, -(%a7)    /* push clipRgn */
 move.l	 -(%a3), -(%a7) /* push tempRgn */
 move.l	 (%a3), -(%a7)  /* push tempRgn */

After adding these graphics, I'm back to having zero bytes left in both boot blocks. Waste not, want not. 🙂


Apple Computer was willing to license their Macintosh technology to third parties by the mid-90s, but not at the expense of their own first-party products. While the initial line-up of Power Macs and clones were a success, Apple was still a computer company; it was right there in their name. Licensed Mac derivatives like the Bandai Pippin could not be allowed to cut into Apple's bottom line, so Apple took measures both technical and tactical to help protect against the Pippin cannibalizing Mac sales. The term "Mac" or "Macintosh" was never to be used publicly to describe the Pippin or its software; the Pippin runs "Pippin OS" and is based on "advanced technology by Apple Computer." Pippins with the first revision of the retail ROM have special code that explicitly blocks the use of storage devices other than those built into the system and those officially available at launch. But on top of that, as extra insurance against Pippins being used as "cheap Macs," Apple added a signing check to the startup process that verifies that a particular boot CD has been authorized for use on the Pippin.

Bandai: "It's not a Mac. We swear."

There was nothing particularly novel about this approach in 1996, and in fact it's still in use today by almost all video game console platforms. Ever since the Nintendo Entertainment System's release in 1985, most consoles have some kind of protection against running unlicensed software. Atari's 7800 ProSystem from 1986 was the first video game console (that I know of) to use a boot-time signature check (with similarities to the RSA algorithm the Pippin would use ten years later). Like the Pippin and all major disc-based consoles that came after it, all 7800 titles had to be digitally signed for release. Given the amount of computing power required in those days to crack a digital cryptographic signature, and the amount of computing power typically available to the general public, these strategies were mostly effective for their time to prevent unlicensed software from affecting a platform's brand and public image during its supported lifetime, for better or worse.

Reverse-engineering and circumventing the Pippin's boot security wasn't easy, but with the exception of deducing Apple's private RSA key, Pippin Kickstart could have been developed using tools and documentation available in the late 90s and early 2000s. I am often nostalgic for the Mac games of my youth and feel that in modern times they deserve to be played on a "real" video game system on a big-screen TV. Thus the idea appealed to me of hacking an "Apple" video game system to let me do just that. Given the Pippin's place in history and how little attention it has received compared to homebrew efforts for systems from Atari, Nintendo, Sega, Sony, and Microsoft, I was surprised at the positive reception and interest Pippin Kickstart got when I first released it in June 2019. That folks are beginning to dip their toes into producing homebrew for this relatively obscure platform goes above and beyond my expectations; I'm absolutely delighted to have enabled this and I hope it continues.

It has been 25 years since the introduction of the Bandai Pippin. In that time, the Internet has exploded with a force that hardly anybody could have predicted back then, and Apple itself has gone from a relatively small player in the computer industry to one of the largest consumer electronics companies in the world. (Bandai is still doing OK, too.) The Internet has brought together fans of gaming from all over the world and from all kinds of backgrounds, fostering communities that celebrate video games and their technology from the mainstream to the obscure. The classic systems of yesteryear may have been forgotten by major retail outlets, but that doesn't mean they have been forgotten by fans and enthusiasts, no matter how obscure or commercially unsuccessful. Nostalgia is a powerful drug, and thanks to it I think there will always be an audience for new developments targeting these vintage consoles, by amateurs and professionals alike. These days, "retro" is cool, and I'm happy to contribute to the zeitgeist in my own small way.

If I've done anything to help rekindle some gaming nostalgia among fellow retro gaming fans, then it's all worth the while. 🙂