1. Open the resource fork
To get started, we need to crack open a module file.
A classic Mac OS file is really two files. The data fork holds what is normally a file's contents. The resource fork is a small structured database of typed records: Code, icons, sounds, menus, and preferences, etc.
An After Dark module lives entirely in a resource fork. Here's a subset of what the Starry Skyline resource fork contains:
| Type | ID | Name | Bytes |
|---|---|---|---|
TEXT | 1000 | Extended credits | 160 |
sVal | 1000 | Buildings: | 2 |
sVal | 1001 | Building Height: | 2 |
sUnt | 1001 | Height Units | 67 |
xVal | 1003 | Flasher? | 2 |
STR | 128 | Credit Line | 70 |
ADgm | 128 | Main | 3,860 |
snd | 128 | Wes’ Meteor | 3,978 |
The ADgm resource is the program. The sVal
and xVal resources declare the settings UI: a slider
named “Buildings:”, a slider named “Building Height:”
(with an sUnt resource listing its labeled stops), and a checkbox
named “Flasher?”.
2. Build a fake Macintosh
The 3,860 bytes of ADgm code were compiled against a world: a
Macintosh with the Toolbox in ROM, operating system state at fixed low
addresses, a screen bitmap it can scribble on, and a heap to allocate from.
None of that exists here, so the emulator fabricates it inside a flat
16 MB byte array, the 68k's 24-bit address space.
The most period-specific part is the low-memory globals.
Mac software of this era didn't always ask the OS politely for the time; it
read a 32-bit tick counter straight from address $016A,
because that's where the OS kept it. Starry Skyline does exactly this: its
blinking rooftop beacon (the “Flasher?” checkbox) times itself off
the clock, and the Random trap keeps its seed at
$0156. So the emulator maintains those addresses like
a real Mac would: the tick counter advances at 60 Hz, and
$0824 holds a pointer to a real, writable screen
bitmap, because some modules bypass drawing APIs entirely and poke pixels into
it themselves.
The heap is deliberately simple. After Dark modules allocate during startup
and free at shutdown, so a bump allocator suffices: each
NewPtr/NewHandle just advances a pointer, and
nothing is ever freed individually. When the module restarts (you'll trigger
this if you move a slider), the allocator simply resets to a recorded
watermark, every allocation reclaimed in one move. Handles,
the Mac's double-indirected pointers, get a real master-pointer pool so the
classic **h double dereference works unmodified.
3. Have a conversation
An After Dark module isn't a program you launch; it's a plugin with a single entry point. The After Dark engine (and now this emulator) calls one Pascal function, over and over, with a message code:
FUNCTION main(VAR storage: Handle; blankRgn: RgnHandle;
message: Integer; params: GMParamBlockPtr): OSErr;
| Message | Code | Meaning |
|---|---|---|
Initialize | 0 | allocate storage, read the control values, set up |
Blank | 2 | paint the screen black (the “saver” part) |
DrawFrame | 3 | draw one animation step, called in a loop, forever |
Close | 1 | clean up |
Everything you're watching is message 3 on repeat. The
params argument points at a GMParamBlock the emulator
fills in: the four control values (your sliders ride to the module in this
struct), a description of the “monitor” (640×480, 8-bit color), a
brightness byte, and capability flags. The emulator claims to be a colorful,
sound-capable Mac, and the module believes it.
Calling 68k code from C means speaking the Pascal calling convention: the caller reserves space for the result, pushes arguments, pushes a return address, and jumps. For each frame the emulator hand-assembles this stack:
The highlighted slot solves a basic problem: how does an emulator
know when a called function is done? Afterglow pushes a fake return
address: $000400, where it has planted the
instruction BRA.S *, “branch to yourself,” an
intentional one-instruction infinite loop. When the module eventually executes
RTS, the program counter lands on the sentinel, the execution
loop notices PC == $000400, and the call is over. No special halt
opcode, no instrumentation of the module's code, just a return address that
can't go anywhere.
The CPU doing the running is Musashi, an open-source 680x0 interpreter (configured as a 68020-class core, the widest target, so modules built for later Macs run too). It fetches, decodes, and executes each instruction against the 16 MB array from the emulated memory described above: additions, branches, loops, all the ordinary machinery of a program. Which leaves one big question: what happens when the module calls the operating system that isn't there?
4. Catch the Toolbox calls
When Starry Skyline wants a random number, the compiled instruction is not
a jump to some library. It is the two-byte opcode $A861, an
instruction that does not exist on the 68000. Motorola reserved every
opcode starting with the bits 1010 (hex $A) as
permanently unimplemented; executing one makes the CPU raise a “line
1010” exception. Apple turned this into the Mac's system-call mechanism:
the OS installed an exception handler that decoded the offending word and
dispatched to the right ROM routine. Every Toolbox call (Random,
LineTo, NewHandle, all of them) is one of these
A-line traps, encoded in the binary as a single word.
This makes emulation much simpler, because every OS call
announces itself at the instruction level. Afterglow doesn't search for API
calls; the CPU literally faults into them. Musashi hits $A861,
finds no such opcode, and invokes a callback, which lands in the trap
dispatcher, the heart of the whole system:
Follow Random ($A861) all the way through. The module, being
a Pascal caller, has already pushed two bytes of result space. The trap fires;
the dispatcher sees a Toolbox trap; the handler advances the random seed at
$0156, writes a 16-bit result into the reserved
stack slot, and execution resumes at the next instruction. The module never
learns the ROM is missing. A drawing call like LineTo ($A891)
works the same way, except the handler pops a destination point off the stack
and hands it to the rasterizer.
Two other details. Gestalt, the
“what Mac am I running on?” call, returns made-up answers:
System 7.1, an FPU, 8 MB of RAM, color QuickDraw. And modules
feature-detect by comparing a trap's address against the address of the
known-unimplemented trap, so the emulator keeps all unimplemented traps at
one shared address and implemented ones at another, making the standard
detection idiom return the right answer.
Building this dispatcher is most of what “writing the emulator” means day to day: run a module, watch the log for Unhandled Toolbox trap, look the number up in Inside Macintosh, implement it, repeat.
5. Push the pixels
The drawing API the modules call is QuickDraw, the Mac's
original graphics system. Its model: every window of drawable memory is a
GrafPort with a pen: a position, a size, an 8×8-bit repeating
pattern, and a transfer mode saying how new pixels combine
with what's there (srcCopy replaces, srcXor flips,
patOr stamps the pattern on, …). Afterglow reimplements the whole
primitive set in C: lines, rects, ovals, round-rects, arcs, polygons,
regions, text, and the everything-blitter CopyBits. It even
re-reads the pen state from the GrafPort's memory at every call, because real
modules cheat and poke fields like the pen pattern directly into the struct
rather than calling the setter.
The interesting part is where the pixels land. Every dot goes to three places at once:
Why three? The ARGB buffer is for display. The 8-bit bitmap inside emulated
memory exists because the module can legally read the screen back through the
pointer at $0824; some modules (Rose, Fish!) write
or read it directly, so it has to be real and consistent. And the index shadow
enables a classic trick: palette animation. The screen is
8-bit indexed color against a 256-entry palette, and modules like Satori and
Supernova animate by calling SetEntries to rewrite palette slots
without redrawing a single pixel. Because the shadow knows each
pixel's slot, the emulator can sweep once over the screen and recompute every
ARGB value when the palette moves.
The palette itself is the standard Mac 8-bit color table (a 6×6×6 color cube, then dedicated red, green, blue, and gray ramps, then black), reconstructed below by the same formula the emulator uses:
6. Watch an opcode become pixels
The trap dispatcher and QuickDraw are two halves of the same path: an A-line
word faults into the Toolbox, and a drawing call ends up in
apply_pixel. Here they meet, drawing the red comet that streaks
across the sky every few seconds. Below is the kind of fragment its draw loop
runs: move the pen to one point, then streak a line to another. The visualization
follows the instructions through the emulator until the comet lands on the grid.
Four of the instructions are ordinary MOVE.Ws pushing the
streak's endpoints onto the stack — each really is the word
$3F3C followed by its immediate value. The other two are A-line
traps: _MoveTo ($A893) parks the pen at the comet's
tail, and _LineTo ($A891) draws the red trail to its
head, laid down through a sparse red pen pattern. It uses the same trap
machinery described above, with different Toolbox routines.
MOVE.Ws run straight on the CPU; the two A-line words fault into
Afterglow, and _LineTo streaks the grid red, one
apply_pixel at a time.7. Get it onto your screen
Everything so far (CPU, traps, rasterizer, memory) is plain portable C
with no Apple APIs anywhere: file access goes through a virtual file layer,
sound is PCM handed to a callback, and the screen is just that ARGB array. So
the whole engine compiles essentially as-is to
WebAssembly with Emscripten. The 16 MB
emulated Macintosh becomes a slab of WASM heap; the only
concession to the browser is three guards around usleep() calls,
because you must never block a browser's main thread.
A thin JavaScript host does the rest: it fetches the
module file, parses the resource fork, copies each resource
into the WASM heap and registers it with the engine, then drives frames from
requestAnimationFrame. After Dark ran its modules at roughly 30
frames per second, so a wall-clock accumulator fires DrawFrame at
30 Hz regardless of whether your display refreshes at 60 or 120. The
finished framebuffer is blitted straight onto a <canvas>
with no post-processing; the flat, hard-edged 640×480 grid in the panel is
the emulator's actual framebuffer, the exact bytes produced by the rasterizer.
One subtlety: a DrawFrame message is synchronous, meaning the 68k
runs until the frame is done. Some modules sit inside a frame busy-waiting on
the tick counter (one module pauses a third of a second mid-frame, by design),
which would freeze the page. So in live mode each frame gets a
10 ms budget: if it runs over, the engine simply stops
executing and returns, leaving the 68k's registers and stack frozen
in place, and the next animation tick resumes the same frame where it left
off. An interpreted CPU makes this almost free; pausing a program between any
two instructions is the one thing an emulator is naturally great at.
8. The comet problem
Keep an eye on the live skyline in the player. Every so often (it's a roughly
1-in-a-hundred roll of Random() each frame, so on average every
few seconds) a red comet streaks across and vanishes. That comet was the
hardest thing on this page to get right, for an unexpected reason: the
module erases it as it goes, and on original hardware that was fine.
Inside one DrawFrame call, the comet routine runs a 41-pass
loop. Each pass draws four short line segments through progressively denser
pen patterns (sparse dots, then medium, then checkerboard gray, then solid)
and erases one segment at the tail. The patterns, straight from the module's
bytes:
$22008800$DDFF77FF$AA55AA55$FFFFFFFF$00000000By the time the loop and its three final cleanup lines finish, about 205 drawing calls later, the comet is gone from the framebuffer. The frame ends in exactly the state it began. So why was it ever visible? Because a Macintosh of the era ran those 205 calls in about a quarter of a second on its 7.83 MHz 68000, while the CRT redrew the screen from that same bitmap 60 times a second. The monitor kept catching the comet partway through. The animation was visible only because the hardware was slow: a race between the CPU and the display that the module's authors counted on.
An emulator executing at modern speed loses that race in the other direction: all 205 calls complete in microseconds, the display samples the framebuffer once per frame, and the comet has always already cleaned up after itself. Perfectly emulated, perfectly invisible.
The fix is to re-create the race. After pixel-modifying operations the engine can publish intermediate framebuffer states and briefly yield, slowing the comet back down to roughly its original pace, so the display catches it in flight, the way a CRT did. (It also left a testing puzzle: a self-erasing animation never changes the final frame, so regression tests hash those mid-frame states too; otherwise the comet could silently break and no checksum would notice.)
snd resource and played through Web Audio. And try the
Flasher? checkbox: that beacon blinks on the rooftop antenna,
timed off the 60 Hz tick counter at $016A.That's the whole machine: a resource fork cracked open in JavaScript, a 16 MB make-believe Macintosh, a CPU interpreter that faults into a reimplemented Toolbox 150 traps wide, and a rasterizer writing every dot three ways. In the middle of it all, 3,860 bytes from 1989, still drawing the same skyline, with every part of the Macintosh around it replaced by your browser.
← Back to Afterglow. Every supported original 68k module runs through this same machine.