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.
7.1.2 Disk Tools Finder
|
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.
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.
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.
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:
- I tried to disassemble the
StdTxMeas
implementation – I got lost when going through too many layers of indirection due to the calling convention.
- I looked at its 68K implementation and checked some of the intermediate building blocks – the width table looked correct.
- 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!
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.