The Abysmal State of Macintosh Emulation thumbnail

The Abysmal State of Macintosh Emulation

The Abysmal State of Macintosh Emulation

The original Macintosh platform, released in 1984 and discontinued in 2001, was nothing short of iconic. It pioneered many conventions of the graphical user interface, it introduced the mouse to the mainstream, and the operating system was a marvel of its time.

Unfortunately, classic Macintosh emulation is pretty pitiful. Let's go through all the Macintosh emulators I'm aware of. It should also be noted that I haven't talked with any of the developers of these emulators, and I mean no disrespect when writing any of these criticisms. Writing an emulator is a laborious, thankless job, and I'm not writing this to be mean. I'm writing this because the state of Macintosh emulation needs serious improvement, preferably before every working classic Mac dies out.

SheepShaver and Basilisk II

Basilisk II attempting to render my website

SheepShaver and Basilisk II are two very related Macintosh emulators. They share the same developers, the same configuration program, and even the same source code repository. The difference is that SheepShaver targets newer PowerPC-based systems, while Basilisk II targets Motorola 68000 System 7-era systems.

They're fine. They work, I guess. I haven't used SheepShaver much, but Basilisk II has some very nice features like TCP/IP support, and the ability to browse your local computer. Those are very nice. I used Basilisk II a lot when writing my AOL article series, as for some reason only the Mac version of AOL gave me things to explore.

Basilisk II on Windows at least comes with HFVExplorer, a nice-ish disk editor. It lets you browse Macintosh disk images, manage files and resources, and copy things in and out using a variety of conversion formats. (Macintosh files are strange, because they have a data fork and a resource fork, which is unlike almost every operating system today.) It's clunky, weird, and was last updated in 1999, but I appreciate it. It also comes with some Windows 95 drivers for the CD drive, a Windows NT 4 and 2000 compatible network driver (that you don't even need), along with some readmes from the year 2000. How... nice? I guess?

It's far from perfect, though. The Windows version refuses to start with no error message unless you've installed both SDL 1.2 and GTK 2, both very painfully obsolete libraries. Software compatibility is far from perfect, although it's often "good enough" for most use. System crashes tend to take down the entire emulator. But fine, this is a work in progress... right?

Let me... let me just try to explain how new versions of Basilisk II and SheepShaver are released.

Basilisk II official download page. It's just a forum thread, with builds graciously contributed by random forum users.

The official place to download Basilisk II/SheepShaver is a random forum thread on the Emaculation message board. Builds are added to the OP whenever some forum user just decides to recompile the software. Literally years can pass between releases, and there's no synchronization between builds for Windows, Linux, or macOS. The most popular build of Basilisk II dates back to 2010, and this is still the only version listed for Linux platforms. The newest build of SheepShaver is from 2015, explicitly for testing. There are numerous known issues listed in the post. None have been fixed. The latest stable version is from 2013.

For those in the audience who aren't software developers, this is what a normal release page looks like. (The project in question is Twin Peaks, a browser for the Gemini protocol that I've been working on-and-off on.)

Twin Peaks download page. Clean, simple, organized

There's a version number, a screenshot, descriptive text of what changed, and at the bottom, links for every platform. This specifically is a page automatically generated by GitHub, the most popular website to host source code. (I feel obligated now to mention that GitHub has a $200,000 contract with the Immigrations and Customs Enforcement branch of the United States. GitHub isn't good, it's just popular.)

If you wanted to, you can even set up GitHub to automatically compile and publish a release at the push of a single button, or even just when you upload your code to the website. That's called Continuous Integration.

The amusing thing about this all is that Basilisk II and SheepShaver are developed using GitHub. Right here! It's actively somewhat actively worked on, too. They're already using Continuous Integration to check the correctness of the code, at least on Linux. It would take almost no effort for them to use this to compile and publish up-to-date builds, or to at least use the GitHub releases page to host the official builds instead of a random forum thread on an unrelated website.

Honestly, if they just made those simple tweaks to how they build and distribute the emulator, I'd have a lot less complaints about Basilisk II and SheepShaver. The code is perfectly readable, everything is nice and separated, it's fine. It could definitely use some more developers, some unit tests, better ways of managing disk images, but it's fine.

It only gets worse from here.

PearPC

PearPC 0.1, showing an early Mac OS X desktop

PearPC is another Power Macintosh-era emulator. However, instead of emulating Mac OS 8 and 9 like SheepShaver, this targets OS X 10.1 through 10.4, and various versions of Linux and BSD. It's strange that 10.5, the last PowerPC version of Mac OS X, doesn't work, but sure. That's fine.

Well, actually, there might be a reason for that. The most recent releases were in 2011 and in 2005. Thankfully, these are downloadable from the project website, with source code. It does not support sound. CPU emulation is pretty slow, between 15x to 500x slower than the host computer. Most implemented hardware are just stubs at this point, just enough to get the system bootable.

This emulator is very incomplete. It hasn't been in active development since 2005, and development only started around 2004. The absolute latest commit on its GitHub page is from 2015. It's really nothing more than a historical curiosity right now. And yet, somehow, it's still constantly placed in lists of Macintosh emulators as if it's still relevant.

PCE

PCE runnning a Mac Plus

This one is interesting. It's a multi-system emulator, but it emulates some early Macintoshes, up to the Macintosh SE and the Macintosh Classic. Most of the hardware is supported.

PCE also has a JavaScript port, which is used by the Internet Archive for their Macintosh emulator. It works very well in that role, and from the few titles I've tried, they all work fine. That said, no official build has came out since 2017, and the newest version you can download and use out of the box is from 2013.

At least for the Windows build, the user experience isn't great. On the one hand, it bundles in a ROM and a disk image of a pre-installed System 7.0.1, so that saves you some trouble. On the other hand, launching the emulator opened up a terminal window, then the emulator, which just swallowed my mouse and keyboard inputs. It took me some difficulty to figure out how to turn the thing off, and I'm still not really sure how to give it a disk image.

It's aggravating that all the the emulators I consider "fine" are infrequently or never updated. But at the same time, at least on the classic Mac side, the only changes that really need to be made to PCE seem to be either really obscure edge-cases, additional hardware support, or user experience improvements.

QEMU

QEMU, showing the Mac OS 9 easter egg

QEMU is a very popular multi-system emulator that emulates pretty much everything. I think it's often used for cross-platform ARM development, for instance. It has experimental support for Mac OS X and Mac OS 9. It is rather user-hostile unless you're really into reading man pages and fiddling with command-line parameters. Sound support is a work-in-progress. But it is a good, functional emulator. Definitely use this over PearPC. SheepShaver might still be better for now though. Here's a blog post on someone emulating Mac OS 9 in QEMU. It's also where I took the image from, since unlike the others there were no images on QEMU's website I could steal.

Mini vMac

Mini vMac emulating a Macintosh II

This is the one I have the most beef with. It's a small, simple emulator for every Macintosh system from the pre-release Twiggy model all the way up to the Macintosh II, with active work being done on newer models. I'll start by listing what I like about it.

So why am I complaining? It's a good emulator. It does its job perfectly fine. It emulates a Mac Plus. And maybe a Mac II. Isn't that all you need?

This is where I start peeling back the layers. Let's start with something simple: changing the settings. Let's say I'm emulating a Mac II, which has an external monitor, and I want to set the color depth to 256 or 16 colors. Or I want to set my display resolution to 640x480, or 800x600. This should be possible, but it's not. Here's some other things you can't change:

These are all pretty reasonable things to what to change, I'd think. The defaults are fine enough, but the fact that I'm forced to stick with them is aggravating.

Except you actually can change most of these things! You just need to use the Variations Service, which allows you to recompile Mini vMac with any of those options set. Because you can only set them if you recompile the software. This already is very strange. But wait, there's more!

Mini vMac, with a box saying "Demo" moving randomly around the screen

By default, these variations come with this completely obnoxious "Demo" pop-up appearing on random locations on the screen once a second. To get a not-demo variant of Mini vMac so you can do something as simple as emulate a Mac II at 800x600, you need pay (no, sorry, donate) ten dollars or more to the "Gryphel Project" for a Sponsor Code. Seriously.

Look, I'm not against donations. But paywalling basic configuarion options just feels cheap. However, I should be fair, the developer does publish a list of what every donation is spent on, all the way back to 2009. I really appreciate that. So what's on it?

Hosting, $15/mo. Health insurance, 111 days, $1,300

...

What.

Look, I'm not angry at you Paul. You've done a very nice job on your emulator and you absolutely deserve all the donations you've received. I'm angry at the fact that your healthcare is so expensive that $1300 only lasts you 111 days.

Just. uuuuggggghhhh. I've written about this before and I'll no doubt write about this again but a society that forces people to pay this much for the privilege of not dying is completely inhumane. 12 dollars and 54 cents a day. To not die. May I remind the audience that the federal minimum wage in the United States is $7.50 an hour.

Mini vMac's Philosophy

...so. uh. If you don't want to pay for Paul's health insurance, there is another option. You can compile the code yourself. And, well, there's a few issues with that. Let's get into the design philosophy of Mini vMac. Straight from the website's about page:

The Mini vMac emulator collection allows modern computers to run software made for early Macintosh computers, the computers that Apple sold from 1984 to 1996 based upon Motorola's 680x0 microprocessors. The first member of this collection emulates the Macintosh Plus.

Mini vMac began in 2001 as a spin off of the program vMac. It was originally intended to be of limited interest, a simpler version to serve as a programmers introduction to vMac. But vMac hasn’t been updated in many years, so Mini vMac may now be considered its continuation.

The “Mini” in the name now means that each emulator in the collection is as small and simple as possible. The meta program and data that generate the emulators (the Mini vMac build system) are rather bigger. Besides the Macintosh Plus, there are also emulations of the Macintosh 128K, 512K, 512Ke, SE, Classic, and SE FDHD. Work is in progress on Macintosh II emulation. There are also numerous other options.

vMac was the original project. It only targeted the Macintosh Plus. It has not been updated since 1999. I'd download it to check it out, but that's broken. Mini vMac is the actively updated fork. That's fine, that's good.

Let me just shine a light onto the phrase "emulator collection". Like PCE, it's not one binary that emulates every system, it's one binary per system. However, PCE only ever published binaries for separate systems. Mini vMac requires a new "emulator in the collection" for every type of Macintosh.

The rationale behind this was to make every binary as small and simple as possible. And yes, the standard Macintosh Plus emulator for Windows is only 137 kilobytes. The 2010 build of Basilisk II is a whole two megabytes, not counting required libraries or the config program, which brings the total filesize up to about 19 megabytes. It's very impressive that Mini vMac is as small as it is.

There are times where filesize matters. I take great pride in the fact that my homepage is only 35.42 kilobytes. But I haven't sacrificed all that much to get it down to that size.

Mini vMac's choice to prioritize binary filesize above all else was a mistake.

Setting up the project

Let's actually work through the compilation steps. This program was written in the C programming language, so for comparison, here's the normal build steps for most C software.

git clone https://.../ program
cd program
./configure
make

You might get variations on the theme, using CMake or SCons or the like, but generally most projects are incredibly easy to compile on a Linux machine. (Windows is awful dev environment for C programs. Thankfully, WSL exists now.)

Thankfully, the process is documented on the website. So let's follow this, on a Windows machine. I want to compile with GCC using MinGW.

First off, there is no Git repo. No Subversion, Mercurial, or Fossil either. There is, however, a gzipped tarball. That's like the Linux version of a ZIP file. This leads me to the horrifying conclusion that this emulator was developed with absolutely no version control, which is a terrible, terrible idea. As much as I ragged on the other emulators, they at least had Git repos.

But fine. Download that, extract with 7-Zip, sure. Three directories: "extras", "setup", and "src". I go to Setup. The documentation wants me to modify "CONFIGUR.i" to set my compiler, so I set it to MinGW. What's interesting is the number of compilers this supports, including some really esoteric ones like "Pelles C Compiler", "Bloodshed Dev-C++", and Visual C++ 6.0. (k i'm roasting myself here)

But let's talk more about the "Gryphel Build System". It's a big C program, that you compile with a simple gcc tool.c -o tool.exe. Those .i files? C source code. No headers, they're just included as-is. That's not how this works. Oh, and all the files have 8.3 filenames, in case you're running that copy of Visual C++ on Windows 3.1 or something. Here's part of a completely random file.

an extremely convoluted function to manually print out a build file

This is a file that outputs a makefile that your compiler takes in to know what source files to compile. The premise of doing this manually when tools like CMake exist is already pretty silly, but normally these sorts of things use template files. The tool should take in a makefile template, replace some specially marked items in {brackets} or whatever works, and output a new file. Easy to modify, easy to extend to add new features, easy to target new compilers.

Being pointlessly convoluted for no good reason is a recurring theme here. Here's a simple question: where are the list of Macintosh models stored? Why, in SPBLDOPT.i. Obviously. In fact, all 3945 lines of that file verify and set various configuration flags in the final output. The entire build system is 16,125 lines.

I don't know how to express how this really doesn't need to be this complicated. The fact that it can output a million different formats and run on a million different compilers is cool I guess, but is it worth how awful it is to extend and improve?

So, you do all that, you run tool.exe, and it produces... a BASH script. To clarify, the goal of the 16,000 line C file that runs on every compiler ever created is to produce a Unix shell script. Even if you're producing a Microsoft Visual C++ project.

I will, however, note that the build script has code to output as either an Macintosh Programmer's Workshop (1980's era Macintosh IDE) script, a bash script, a VBScript, an AppleScript, or an "XP script", whatever that is. It's undocumented, but you can specify a specific output file by defining #gbo_script to one of gbk_script_mpw, gbk_script_applescript, gbk_script_bash, gbk_script_xp, or gbk_script_vbscript in CONFIGUR.i. There's some obviously faulty logic to set this automatically based on your selected compiler somewhere. 16,000 lines of code and it can't match up "Visual Studio" with "VBScript".

What's really strange is that you have to set your compiler in CONFIGUR.i, but you set your target on the tool.exe commandline. So, yes, I could attempt to compile a 64-bit Windows program in MPW. 16,000 lines of code and it doesn't safe-guard against that.

That shell script contains various commands that essentially write out each and every configuration header needed for the source code, along with creating the output directories and the makefiles. There's no logic in here beyond "don't create directory if it already exists", just a bunch of printf statements.

Why... why didn't tool.exe just create the files? What's this layer of indirection about? What is this build system? Why does this exist?!

Let me just take a quick moment to talk about CMake. It's a pretty popular build system. You can set and verify options in it, and those options appear in a very nice graphical menu. You can conditionally include or exclude files depending on those build targets. It targets all sorts of compilers, although admittedly MPW isn't on that list. I could easily replace all 16,000 lines of this with a CMakeFile and it would work better.

Also, it's this build tool where you set all of your options, like the model of Mac to emulate and the screen resolution and the default speed and all of that. The only documentation on how to use it is in the website. Naturally, there is no command-line help. 16,000 lines of code and it doesn't even come with a --help flag.

But I digress

Compiling

Cool. Fine. Whatever. My tool.c turned into a tool.exe which will barf out a bash script that's supposed to create an extremely sloppy and unoptimized Makefile targeting a Macintosh Plus running on 64-bit Windows. The process wasn't that hard, it just... why.

Let's just run it.

$ setup/tool.exe -t wx64 > ./make.sh
$ ./make.sh
bash: ./make2.sh: cannot execute binary file: Exec format error

...16,000 lines of code and it doesn't even run. What.. what even is

$ file make2.sh
make2.sh: Bourne-Again shell script, Little-endian UTF-16 Unicode text executable, with CRLF line terminators

Viz looking annoyed at the viewer

So let me explain. Each line in this file is created using a custom function called WriteDestFileLen, which wraps WriteCStrToDestFile, which wraps WriteCharToDestFile, which wraps WriteCharToOutput (and adds escape sequences as needed), which eventually calls putchar from the C standard library. Yes, there's four layers of indirection around printing a string here.

The problem here is that I'm running Windows. The Windows terminal, and all internal Windows functions, use UTF-16 character encoding. (Microsoft tried to jump on the Unicode bandwagon but did it too early.) Because of that, the file it produced was UTF-16 formatted. I guess Git Bash doesn't like that.

So, you know what I need to do? I need to open this in a text editor and set the encoding to UTF-8.

$ ./make.sh
./make.sh: line 1: $'\357\273\277#!': command not found

UTF-8 with no BOM.

$ ./make.sh
./make.sh: line 460: printf: command not found
./make.sh: line 583: printf: command not found

And remove those garbage characters, however the heck they got there.

$ ./make.sh
$

Cool. Took you long enough.

And now I get my Makefile, and two new folders, bld/ and cfg/. The output goes in bld/, the configuration files are in cfg/.

Let's recap. I compiled a build tool that created the wrong type of script in the wrong character encoding for my platform, and it produces some header files and a Makefile.

Hi. Question. Why not just let me set all of these options in the configuration headers?! I can do an #define target windows-x64 and it can just use include guards to choose what to compile. Or it can use #pragma error statements to warn me that I have an invalid configuration. Then I just tell my compiler to compile every C file and it just produces the correct output.

The answer? It already does that. To an extent; specific Macintosh models are turned into a mess of function pointers that specify the internal hardware layout, but there's no reason that couldn't be factored out into a header file per system. There's no reason for any of this. The Makefile is the only thing that really warrants a build system, but really, that's not hard at all. Shove them all in a folder, make a script in each format at the root to pick the right one and fill in the filenames and make the directories, bam. Done.

It gets worse from here.

Compiling

$ make
gcc "src/MINEM68K.c" -o "bld/MINEM68K.o" -c -Wall -Wmissing-prototypes -Wno-uninitialized -Wundef -Wstrict-prototypes -Icfg/ -Isrc/ -Os
Cannot create temporary file in C:\WINDOWS\: Permission denied
make: *** [Makefile:18: bld/MINEM68K.o] Error 3

Right, okay, let's switch back to Powershell.

PS > make
gcc "src/MINEM68K.c" -o "bld/MINEM68K.o" -c -Wall -Wmissing-prototypes -Wno-uninitialized -Wundef -Wstrict-prototypes -Icfg/ -Isrc/ -Os
gcc "src/OSGLUWIN.c" -o "bld/OSGLUWIN.o" -c -Wall -Wmissing-prototypes -Wno-uninitialized -Wundef -Wstrict-prototypes -Icfg/ -Isrc/ -Os
...
gcc "src/PROGMAIN.c" -o "bld/PROGMAIN.o" -c -Wall -Wmissing-prototypes -Wno-uninitialized -Wundef -Wstrict-prototypes -Icfg/ -Isrc/ -Os
make: *** No rule to make target 'cfg/main.r', needed by 'bld/'.  Stop.
PS >

So let's talk about Windows resource files. They're Microsoft's attempt to kinda do the same thing as the Macintosh resource forks. Basically, it's extra data like program icons, dialog boxes, translation strings, etc. that's independent from the code, and can be modified and translated without touching the binary. It's nice. More people should use them. The only issue is that Windows resource files are a very Windows-specific thing. But I digress.

The thing is that this Makefile is malformed. You see, it's laid out like this:

bld/GLOBGLUE.o : src/GLOBGLUE.c cfg/CNFGGLOB.h
    gcc "src/GLOBGLUE.c" -o "bld/GLOBGLUE.o" $(mk_COptions)
bld/M68KITAB.o : src/M68KITAB.c cfg/CNFGGLOB.h
    gcc "src/M68KITAB.c" -o "bld/M68KITAB.o" $(mk_COptions)
bld/MINEM68K.o : src/MINEM68K.c cfg/CNFGGLOB.h
    gcc "src/MINEM68K.c" -o "bld/MINEM68K.o" $(mk_COptions)
bld/VIAEMDEV.o : src/VIAEMDEV.c cfg/CNFGGLOB.h
    gcc "src/VIAEMDEV.c" -o "bld/VIAEMDEV.o" $(mk_COptions)
...
bld/: cfg/main.r
    windres.exe -i "cfg/main.r" --input-format=rc -o "bld/" -O coff  --include-dir SRC

and that fails, because things after the colons are considered prerequisites, files that need to exist before the compiler command is ran. The issue here is that make has no idea what a .r file is, because it's supposed to be .rc. So rename that .r file to a .rc, modify the Makefile, continue.

PS > make
gcc \
        -o "minivmac.exe" \
        bld/MINEM68K.o bld/OSGLUWIN.o bld/GLOBGLUE.o bld/M68KITAB.o bld/VIAEMDEV.o bld/IWMEMDEV.o bld/SCCEMDEV.o bld/RTCEMDEV.o bld/ROMEMDEV.o bld/SCSIEMDV.o bld/SONYEMDV.o bld/SCRNEMDV.o bld/MOUSEMDV.o bld/KBRDEMDV.o bld/SNDEMDEV.o bld/PROGMAIN.o  "bld/" -mwindows -lwinmm -lole32 -luuid
C:/WinProg/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/7.3.0/../../../../x86_64-w64-mingw32/bin/ld.exe: cannot find bld/: Permission denied
collect2.exe: error: ld returned 1 exit status
make: *** [Makefile:68: minivmac.exe] Error 1
PS >

You can't compile a directory. Remove "bld/ from that final gcc fine in the Makefile. Continue.

It compiles this time. Except the program has no icon, because the resource file was never linked into the final binary. In fact, looking at the windres line, I'm realizing it's trying to set the output filename to "bld/", which is probably where that came from.

In the resource file command, change "bld/" to "bld/main.res". Both instances. Move that target above ObjFiles. Add bld/main.res to ObjFiles. Recompile. Now it has an icon.

Let's recap. The custom 16,000 line build system that produced a script for the wrong platform in the wrong byte encoding for my system produces an invalid Makefile that I had to edit by hand to just get the program to compile. Also, it was incredibly slow, because it builds every single source file into it's own object file. Also, it doesn't easily allow you to set the compiler to, ex: clang.

Let me fix that for you.

A list of source files and a single call to $(CC). Tada. This can still be improved. Looking at this I forgot the prerequisites, and I should have used conditionals instead of a target per platform. But otherwise, it's significantly faster, better, and easier to modify than the other one. Also, cross-platform.

There's a sort of principle in software development, "Keep It Simple, Silly". What are the official builds built on? A $15/mo DigitalOcean droplet running Linux. Just use a Makefile. Drop the obscure compilers, heck, drop Visual Studio. (Or switch to CMake, as I ended up doing in my fork that I'm gonna get to later.)

You don't need a buggy 16,000 line build system that goes through 5 function calls to print one single character. You need a Makefile.

Code Review

(Disclaimer: I do embedded software development for a career. I know C decently well, and I definitely know how to organize and manage large codebases.)

Hah hah, yep. We're this far in and I haven't in talked about how the project's code is laid out.

C>dir /w src
 Volume in drive C is Windows
 Volume Serial Number is 4ADD-5948

 Directory of ./src

[.]             [..]            ACTVCODE.h      ADBEMDEV.c      ADBEMDEV.h
ADBSHARE.h      ALTKEYSM.h      ASCEMDEV.c      ASCEMDEV.h      BPFILTER.h
COMOSGLU.h      CONTROLM.h      DATE2SEC.h      DISAM68K.c      DISAM68K.h
ENDIANAC.h      FPCPEMDV.h      FPMATHEM.h      GLOBGLUE.c      GLOBGLUE.h
HPMCHACK.h      ICONAPPM.r      ICONAPPO.icns   ICONAPPW.ico    ICONDSKM.r
ICONDSKO.icns   ICONDSKW.ico    ICONROMM.r      ICONROMO.icns   ICONROMW.ico
INTLCHAR.h      IWMEMDEV.c      IWMEMDEV.h      KBRDEMDV.c      KBRDEMDV.h
M68KITAB.c      M68KITAB.h      main.rc         MINEM68K.c      MINEM68K.h
MOUSEMDV.c      MOUSEMDV.h      MYOSGLUE.h      OSGLUCCO.m      OSGLUGTK.c
OSGLUMAC.c      OSGLUNDS.c      OSGLUOSX.c      OSGLUSD2.c      OSGLUSDL.c
OSGLUWIN.c      OSGLUXWN.c      PBUFSTDC.h      PMUEMDEV.c      PMUEMDEV.h
PROGMAIN.c      PROGMAIN.h      ROMEMDEV.c      ROMEMDEV.h      RTCEMDEV.c
RTCEMDEV.h      SCCEMDEV.c      SCCEMDEV.h      SCRNEMDV.c      SCRNEMDV.h
SCRNHACK.h      SCRNMAPR.h      SCRNTRNS.h      SCSIEMDV.c      SCSIEMDV.h
SGLUALSA.h      SGLUDDSP.h      SNDEMDEV.c      SNDEMDEV.h      SONYEMDV.c
SONYEMDV.h      STRCNCAT.h      STRCNCZE.h      STRCNDUT.h      STRCNENG.h
STRCNFRE.h      STRCNGER.h      STRCNITA.h      STRCNPOL.h      STRCNPTB.h
STRCNSPA.h      STRCNSRL.h      SYSDEPNS.h      VIA2EMDV.c      VIA2EMDV.h
VIAEMDEV.c      VIAEMDEV.h      VIDEMDEV.c      VIDEMDEV.h
              92 File(s)      2,005,943 bytes
               2 Dir(s)   6,653,526,016 bytes free

C>

MS-DOS is the only appropriate way to show that directory listing, because it's the only system where it makes sense. Aside from the .icns macOS icons, which just makes the insistence on 8.3 baffling. No subdirectories either.

If I were to organize these, it would be like this:

.
├───HW
│   ├───ADB
│   ├───DISK
│   ├───KBRD
│   ├───M68K
│   ├───MOUSE
│   ├───POWERMAN
│   ├───RTC
│   ├───SCC
│   ├───SCREEN
│   ├───SCSI
│   ├───SOUND
│   ├───VIA
│   └───VIDCARD
├───LANG
├───PATCHES
├───UI
│   ├───MACOSX
│   ├───NDS
│   ├───OLDMAC
│   ├───UNIX
│   ├───WIN32
│   └───XPLAT
└───UTIL
Click for reorganized view with filenames
.
│   ERRCODES.h
│   GLOBGLUE.c
│   GLOBGLUE.h
│   PROGMAIN.c
│   PROGMAIN.h
│   SYSDEPNS.h
├───HW
│   ├───ADB
│   │       ADBEMDEV.c
│   │       ADBEMDEV.h
│   │       ADBSHARE.h
│   ├───DISK
│   │       IWMEMDEV.c
│   │       IWMEMDEV.h
│   │       SONYEMDV.c
│   │       SONYEMDV.h
│   ├───KBRD
│   │       KBRDEMDV.c
│   │       KBRDEMDV.h
│   │       KEYCODES.h
│   ├───M68K
│   │       DISAM68K.c
│   │       DISAM68K.h
│   │       FPCPEMDV.h
│   │       FPMATHEM.h
│   │       M68KITAB.c
│   │       M68KITAB.h
│   │       MINEM68K.c
│   │       MINEM68K.h
│   ├───MOUSE
│   │       MOUSEMDV.c
│   │       MOUSEMDV.h
│   ├───POWERMAN
│   │       PMUEMDEV.c
│   │       PMUEMDEV.h
│   ├───RTC
│   │       RTCEMDEV.c
│   │       RTCEMDEV.h
│   ├───SCC
│   │       SCCEMDEV.c
│   │       SCCEMDEV.h
│   ├───SCREEN
│   │       SCRNEMDV.c
│   │       SCRNEMDV.h
│   │       SCRNMAPR.h
│   │       SCRNTRNS.h
│   ├───SCSI
│   │       SCSIEMDV.c
│   │       SCSIEMDV.h
│   ├───SOUND
│   │       ASCEMDEV.c
│   │       ASCEMDEV.h
│   │       SGLUALSA.h
│   │       SGLUDDSP.h
│   │       SNDEMDEV.c
│   │       SNDEMDEV.h
│   ├───VIA
│   │       VIA2EMDV.c
│   │       VIA2EMDV.h
│   │       VIAEMDEV.c
│   │       VIAEMDEV.h
│   └───VIDCARD
│           VIDEMDEV.c
│           VIDEMDEV.h
├───LANG
│       INTLCHAR.h
│       STRCNCAT.h
│       STRCNCZE.h
│       STRCNDUT.h
│       STRCNENG.h
│       STRCNFRE.h
│       STRCNGER.h
│       STRCNITA.h
│       STRCNPOL.h
│       STRCNPTB.h
│       STRCNSPA.h
│       STRCNSRL.h
├───PATCHES
│   │   ROMEMDEV.c
│   │   ROMEMDEV.h
│   │   SCRNHACK.h
│   │   SONY.bin
│   |   HPMCHACK.h
│
├───UI
│   │   COMOSGLU.c
│   │   COMOSGLU.h
│   │   CONTROLM.c
│   │   CONTROLM.h
│   │   MYOSGLUE.h
│   ├───MACOSX
│   │       ICONAPPO.icns
│   │       ICONDSKO.icns
│   │       ICONROMO.icns
│   │       OSGLUCCO.m
│   │       OSGLUOSX.c
│   ├───NDS
│   │       OSGLUNDS.c
│   ├───OLDMAC
│   │       ICONAPPM.r
│   │       ICONDSKM.r
│   │       ICONROMM.r
│   │       main.r
│   │       OSGLUMAC.c
│   ├───UNIX
│   │       OSGLUXWN.c
│   ├───WIN32
│   │       DBGLOG.c
│   │       ICONAPPW.ico
│   │       ICONDSKW.ico
│   │       ICONROMW.ico
│   │       INTLKBRD.c
│   │       KEYBOARD.c
│   │       main.rc
│   │       OSGLUWIN.c
│   │       OSGLUWIN.h
│   │       SOUND.c
│   │       SOUND.h
│   │       TIMEDATE.c
│   └───XPLAT
│           OSGLUGTK.c
│           OSGLUSD2.c
│           OSGLUSDL.c
└───UTIL
        BPFILTER.h
        DATE2SEC.c
        DATE2SEC.h
        ENDIANAC.h
        PBUFSTDC.h

Just by putting things into folders, we instantly make the codebase significantly less daunting. We have a folder for each hardware component, a folder for language strings, a folder for ROM patches, a folder for the various user interfaces, and a utilities folder. And then all we need at root is PROGMAIN.c, which already is a simple loop that simply calls functions in all the other modules.

Let's look at that main loop file, then. I dunno, the vsync function.

LOCALPROC SixtiethSecondNotify(void)
{
#if dbglog_HAVE && 0
    dbglog_WriteNote("begin new Sixtieth");
#endif
    Mouse_Update();
    InterruptReset_Update();
#if EmClassicKbrd
    KeyBoard_Update();
#endif
#if EmADB
    ADB_Update();
#endif

    Sixtieth_PulseNtfy(); /* Vertical Blanking Interrupt */
    Sony_Update();

#if EmLocalTalk
    LocalTalkTick();
#endif
#if EmRTC
    RTC_Interrupt();
#endif
#if EmVidCard
    Vid_Update();
#endif

    SubTickTaskStart();
}

Thanks, I hate it.

Three things are glaring at me just from this one function alone.

First dbglog shouldn't need an ifdef wrapper. That wrapper should either be in dbglog itself, or preferably dbglog should be a macro that can be set to whatever is deemed appropriate. It's also worth noting there's different dbglog functions for various datatypes, which in theory could be very powerful, but in reality are just sent to stdout using...

LOCALPROC dbglog_write(char *p, uimr L)
{
    uimr r;
    uimr bufposmod;
    uimr curbufdiv;
    uimr newbufpos = dbglog_bufpos + L;
    uimr newbufdiv = FloorDivPow2(newbufpos, dbglog_buflnsz);

label_retry:
    curbufdiv = FloorDivPow2(dbglog_bufpos, dbglog_buflnsz);
    bufposmod = ModPow2(dbglog_bufpos, dbglog_buflnsz);
    if (newbufdiv != curbufdiv) {
        r = dbglog_bufsz - bufposmod;
        MyMoveBytes((anyp)p, (anyp)(dbglog_bufp + bufposmod), r);
        dbglog_write0(dbglog_bufp, dbglog_bufsz);
        L -= r;
        p += r;
        dbglog_bufpos += r;
        goto label_retry;
    }
    MyMoveBytes((anyp)p, (anyp)dbglog_bufp + bufposmod, L);
    dbglog_bufpos = newbufpos;
}

yeah, you tell me.

Second, what's a uimr? What's a LOCALPROC? To answer those questions, open a copy of Macintosh Inside Volume II and flip to some random page.

Pascal source code listing from Macintosh Inside

Pascal was the programming language that the original Macintosh primarily used, until around the mid-90s when everyone moved over to MetroWerks CodeWarrior, which was a C compiler.

Here's a screenshot of MPW, the compiler at the time:

MPW about screen

On the one hand, this is quite possibly the coolest About screen I've ever seen. The pieces assemble into a floppy disk that's spray-painted blue. On the other hand, this is a compiler from 1993. I've actually tried using it to write my own Macintosh software and it's honestly incomprehensible. May I remind you that to this day Mini vMac explicitly supports this IDE?

I guess whoever programmed this decided they loved Pascal so much they wanted to keep using Pascal names for everything, but they didn't love it so much to actually write the emulator in Pascal. I'd do a git blame but this came from a source tarball.

All of these alternate names are stored in SYSDEPNS.h. Here's the relevant lines for LOCALPROC.

#define MayNotInline __attribute__((noinline))
#define LOCALFUNC static MayNotInline
#define LOCALPROC LOCALFUNC void

Ignoring how pointless of a macro MayNotInline is, a LOCALPROC is just a static void. What's wrong with static void? Every C programmer understands that. It's a function that returns nothing and is only accessible from the current file. Which, yes, you could call that a "local procedure", but why? This is a C program, not a Pascal program.

But the third issue in that function. Notice how every line like RTC_Interrupt() is wrapped in #ifdef Em_RTC lines? That's bad.

Let's look at another file. OSGLUWIN.c. Operating System GLUe for WINdows. Stuff that creates the windows, handles the mouse and keyboard events, etc. It's 6,000 lines for some reason. It also inexplicably has Windows CE support, despite the fact that I'm fairly certain Mini vMac was never released for that platform. Also, nobody uses Windows CE anymore.

I'm going to ignore that for now. Let's scroll about half-way down to line 3138 and just stare in awe at how awful this is.

#if MayNotFullScreen
    CurWinIndx = WinIndx;
#endif

    MainWnd = NewMainWindow;
    MainWndDC = NewMainWndDC;
    gTrueBackgroundFlag = falseblnr;
#if VarFullScreen
    UseFullScreen = WantFullScreen;
#endif
#if EnableMagnify
    UseMagnify = WantMagnify;
#endif

#if VarFullScreen
    if (UseFullScreen)
#endif
#if MayFullScreen
    {
        ViewHSize = ScreenX;
        ViewVSize = ScreenY;
#if EnableMagnify
        if (UseMagnify) {
            ViewHSize /= MyWindowScale;
            ViewVSize /= MyWindowScale;
        }
#endif

This is one section of a 361 line function named ReCreateMainWindow. It would be a completely fine and normal function if it weren't absoultely littered with #if statements.

You see, let's go back to how Mini vMac stated that it was a compiler collection. A set of small, simple compilers that emulated one specific machine. Those #if lines? That's what makes it a collection.

There are 3,776 instances of #if in the entire codebase, exluding the build system. Almost four thousand places where code is excluded on a per-line level just to bring the file size down a little bit more.

What's your internet speed? 10 mbps? Almost certainly more than that, but that's the low end today. To download one whole megabyte at that speed it takes... 0.8 seconds. It took you more time to read that sentence than a one megabyte file would have taken to download.

The .ZIP file for Mini vMac on Windows can be downloaded in 11 seconds over a 56k dial-up modem. Is that worth making your code completely unreadable and inflexible? On dial-up, a megabyte would only take 2 minutes 26 seconds to download. That's honestly fine, in my opinion.

One more example to pick on, just to prove that this is a systemic problem. SCRNMAPR.h. This file is supposed to represent the "Screen Mapper", whatever that is. Here's the main function, starting at line 78.

/* now define the procedure */

LOCALPROC ScrnMapr_DoMap(si4b top, si4b left,
    si4b bottom, si4b right)
{
    int i;
    int j;
#if (ScrnMapr_TranN > 4) || (ScrnMapr_Scale > 2)
    int k;
#endif
    ui5r t0;
    ScrnMapr_TranT *pMap;
#if ScrnMapr_Scale > 1
    ScrnMapr_TranT *p3;
#endif

    ui4r leftB = left >> (3 - ScrnMapr_SrcDepth);
    ui4r rightB = (right + (1 << (3 - ScrnMapr_SrcDepth)) - 1)
        >> (3 - ScrnMapr_SrcDepth);
    ui4r jn = rightB - leftB;
    ui4r SrcSkip = ScrnMapr_ScrnWB - jn;
    ui3b *pSrc = ((ui3b *)ScrnMapr_Src)
        + leftB + ScrnMapr_ScrnWB * (ui5r)top;
    ScrnMapr_TranT *pDst = ((ScrnMapr_TranT *)ScrnMapr_Dst)
        + ((leftB + ScrnMapr_ScrnWB * ScrnMapr_Scale * (ui5r)top)
            * ScrnMapr_TranN);
    ui5r DstSkip = SrcSkip * ScrnMapr_TranN;

    for (i = bottom - top; --i >= 0; ) {
#if ScrnMapr_Scale > 1
        p3 = pDst;
#endif

        for (j = jn; --j >= 0; ) {
            t0 = *pSrc++;
            pMap =
                &((ScrnMapr_TranT *)ScrnMapr_Map)[t0 * ScrnMapr_TranN];

#if ScrnMapr_TranN > 4
            for (k = ScrnMapr_TranN; --k >= 0; ) {
                *pDst++ = *pMap++;
            }
#else

#if ScrnMapr_TranN >= 2
            *pDst++ = *pMap++;
#endif
#if ScrnMapr_TranN >= 3
            *pDst++ = *pMap++;
#endif
#if ScrnMapr_TranN >= 4
            *pDst++ = *pMap++;
#endif
            *pDst++ = *pMap;

#endif /* ! ScrnMapr_TranN > 4 */

        }
        pSrc += SrcSkip;
        pDst += DstSkip;

#if ScrnMapr_Scale > 1
#if ScrnMapr_Scale > 2
        for (k = ScrnMapr_Scale - 1; --k >= 0; )
#endif
        {
            pMap = p3;
            for (j = ScrnMapr_TranN * jn; --j >= 0; ) {
                *pDst++ = *pMap++;
            }
            pDst += DstSkip;
        }
#endif /* ScrnMapr_Scale > 1 */
    }
}

Let's just go through why I strongly dislike this function very quickly.

  1. LOCALPROC, again
  2. Use of nonsensical type names like si4b and ui5r. Those map to signed short and unsigned int, by the way. There's this header called in the C standard library, stdint.h, that defines lovely type names like int16_t and uint32_t, that every C programmer understands. Use those, please.
  3. Pointless #if blocks that, in this instance, prevent variables from being defined or to manually unroll loops. Having 8 more bytes allocated on the stack really isn't going to harm anything. I promise. And your compiler does the loop unrolling for you. Has since the 90s.
  4. Convoluted macro chains. ScrnMapr_Scale is a alias of MyWindowScale which is a config option set during in the build system. ScrnMapr_TranT is an alias of ui4b which is an alias of unsigned short. ScrnMapr_N is a complicated math function dependent on quite a few configuration variables.
  5. No comments. "now define the procedure" doesn't count. What is a screen mapper? What is the goal of this function?
  6. Incomprehensible variable names. Some of them are extremely long, like MyWindowScale. (btw, "My" was only in the programming textbooks so they could show how to reimplement basic functions without name collisions. You're not supposed to name variables with "My".) And others are really short, like jn or t0, which could mean literally anything.
  7. Excessive use of global variables. Note that this persumably does a "screen mapping", yet it returns void and all the arguments are just integers. You'd really have to look at it to know, but I think it's modifying ScrnMapr_Map. ...wait, no, it's ScrnMapr_Dest. Obviously.
  8. There's code in a header file. People do this in C++, but you never do this in C because it will lead to compile errors if you ever try to include the header from more than one source file.

...those last two are strange. Okay. Wait. Wait. Top of the file.

/*
    SCReeN MAPpeR
*/

/* required arguments for this template */

#ifndef ScrnMapr_DoMap /* procedure to be created by this template */
#error "ScrnMapr_DoMap not defined"
#endif
#ifndef ScrnMapr_Src
#error "ScrnMapr_Src not defined"
#endif
...

This is a template?! Where is it used... oh. oh no.

#if EnableMagnify && ! UseColorImage

#define ScrnMapr_DoMap UpdateScaledBWCopy
#define ScrnMapr_Src GetCurDrawBuff()
#define ScrnMapr_Dst ScalingBuff
#define ScrnMapr_SrcDepth 0
#define ScrnMapr_DstDepth 0
#define ScrnMapr_Map ScalingTabl
#define ScrnMapr_Scale MyWindowScale

#include "SCRNMAPR.h"

#endif

#if (0 != vMacScreenDepth) && (vMacScreenDepth < 4)

#define ScrnMapr_DoMap UpdateMappedColorCopy
#define ScrnMapr_Src GetCurDrawBuff()
#define ScrnMapr_Dst ScalingBuff
#define ScrnMapr_SrcDepth vMacScreenDepth
#define ScrnMapr_DstDepth 5
#define ScrnMapr_Map ScalingTabl

#include "SCRNMAPR.h"

#endif

...

ScrnMapr_DoMap is never called directly. Every platform that uses this (basically everything except Windows) does something like #define ScrnMapr_DoMap UpdateScaledBWCopy, then includes this file. It only includes it once though, depending on build configuration settings like whether magnification mode is enabled, or the bit depth of the screen. Various function parameters, like ScrnMapr_Src and ScrnMapr_Dst are #define'd before including this header.

I'm not sure whether to be impressed or horrified by this. This is a complete misuse of a header file. Header files are supposed to contain function/variable declarations, struct/enum definitions, and possibly macros. They aren't supposed to be used as make-shift code generators. I genuinely have never seen "code templates" in C before, and I'm astonished that they're a thing.

You know how I would implement this?

/* SCRNMAPR.h */
#include <stdint.h>
typedef struct {
    uint16_t top;
    uint16_t left;
    uint16_t right;
    uint16_t bottom;
} rect_t;

// Copy a rectangular bitmap region, scaling and converting color depth as needed
void ScrnMapr_DoMap(
    rect_t bounds,
    const uint8_t *src, uint8_t *dst, uint8_t src_depth, uint8_t dst_depth,
    const uint8_t *map, uint8_t scale
);
/* OSGLUxxx.c */
#define UpdateScaledBWCopy(top, left, right, bottom) ScrnMapr_DoMap( \
    {top, left, right, bottom}, \
    GetCurDrawBuf(), ScalingBuff, 0, 0, \
    ScalingTabl, MyWindowScale \
)
#define UpdateMappedColorCopy(top, left, right, bottom) ScrnMapr_DoMap( \
    {top, left, right, bottom}, \
    GetCurDrawBuf(), ScalingBuff, vMacScreenDepth, 5, \
    ScalingTabl, 1 \
)
...

See, that's easy to understand I think. ScrnMapr_DoMap is now an obvious, commented function that does a defined task with clear inputs and outputs. We then use some wrapper macros to make common operations easier. And we define all of them, because these macros don't compile to anything unless we use them. Also, we create a rectangle typedef, because rectangles are a common object that we want to move around as a whole unit. That's how you're supposed to use the language. Not... not that.

The entire codebase is like this. I do not understand how it hasn't just collapsed under its own weight yet. I'd give more examples but I've gone on for long enough. You get the picture.

Conclusion

tl;dr: I award you no points..

It's this or Basilisk II. Or maybe something like CLK, which I never mentioned because it's new and immature, but it is promising. Mini vMac is very good on the exterior. It's the most user-friendly Macintosh emulator. It's genuinely my preferred emulator nowadays. Nothing wrong with Basilisk II, but it's not the most stable or usable emulator IMO, and unlike Mini vMac it's not updated very frequently.

I really really want to like Mini vMac. I've actually started my own fork, µvMac, that's supposed to clean up the codebase of Mini vMac, update it to modern coding standards, and give the project new life. I've been working on-and-off on it (mostly off; I'm realizing that I can't really program as a hobby if I'm already programming for 40 hours a week at work, and I've spread myself too thin with projects honestly), and already I feel like it's easier to build and hack on.

But let's step back a bit. I sincerely apologize if this seems inappropriately roasty and argumentative. I'm just amazed that a platform as important as the Macintosh doesn't have a decent emulator. This isn't just bad for the average retro game player; it's bad for preservation. We need to get this right, and we need to ensure that the codebase is readable above all else. Otherwise, when the last Mac dies, we might lose out on a large part of computing history.

I wish the best for the Mini vMac and the other emulators, I really do.Seriously, I know that emulator development is hard, thankless work. Throw some change at Paul C. Pratt, he genuinely deserves it. I hope that this article inspires others to help out with these projects and make them even better.

Thanks for reading. Have a nice day.