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;
    MoveTo(20, 20);
    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);

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).

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 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.

1 Comment

Even if I'm not that knowledgeable in programming, I always enjoy reading your updates! Thank you for keeping the classic Macintosh alive.

Since you mentioned Dolphin: I hope someday it becomes possible to use the Wii as a Macintosh. I know there've been documented (and somewhat successful) attempts at it using Mac-on-Linux, but if the gates of using a cheap Wii as a native Mac were to be open... the Wii U even more, with better CPU/RAM and HDMI output. Not sure if it is technically possible, but a man can dream.

Post a Comment