How?

“If you wish to run an After Dark module, you must first invent the Macintosh”

— Carl Sagan, probably

This page describes how Afterglow invents just enough of a Macintosh to run After Dark modules.

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:

TypeIDNameBytes
TEXT1000Extended credits160
sVal1000Buildings:2
sVal1001Building Height:2
sUnt1001Height Units67
xVal1003Flasher?2
STR 128Credit Line70
ADgm128Main3,860
snd 128Wes’ Meteor3,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.

$000000 exception vectors + low-memory globals Ticks $016A · RandSeed $0156 · ScrnBase $0824 the OS state 1989 code reads directly $000400 the sentinel: BRA.S * (the runner's stop sign) $000900 A5 world (the module's global variables) $080000 stack (grows downward) where each message call is assembled $100000 module code: the ADgm bytes, copied here the program counter lives in this block $200000 heap: 12 MB bump allocator master-pointer pool for Handles, then data blocks; NewPtr / NewHandle carve from here $E00000 top of heap (not to scale)
The emulated address space. Everything the module can touch is a plain C array on this end, and a plain slab of WebAssembly heap in your browser.

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;
MessageCodeMeaning
Initialize0allocate storage, read the control values, set up
Blank2paint the screen black (the “saver” part)
DrawFrame3draw one animation step, called in a loop, forever
Close1clean 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:

higher addresses ↑ result space (2 bytes, OSErr) caller reads this after → storage Handle (VAR parameter) blankRgn (region handle) message = 3 (DrawFrame) → GMParamBlock (your control values live here) return address = $000400 (the sentinel) SP → lower addresses ↓
One DrawFrame call, as built by the runner. Then the CPU is pointed at the module's entry and told to go.

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:

68k executes $A891 (_LineTo) CPU: unimplemented instruction → callback trap dispatcher decode the word: bit 11 set? bit 11 = 0 → OS trap arguments in registers NewPtr: D0=size → A0=ptr bit 11 = 1 → Toolbox trap Pascal: arguments on the stack LineTo: pop h,v from stack handler runs in C quickdraw.c rasterizes; PC resumes after the trap
One bit in the trap word picks the calling convention; the rest selects one of ~150 reimplemented Toolbox routines.

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:

apply_pixel(x, y) one call per dot drawn framebuffer: 640×480 × 32-bit ARGB what you see; blitted to the canvas each frame index shadow: 8-bit palette index remembers which palette slot each pixel used emulated bitmap: 8-bit, at ScrnBase inside 68k memory; what the module reads back three answers to “what color is pixel (x,y)?”, one per audience
The triple buffer. Each layer serves a different reader: your GPU, the palette animator, and the module itself.

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:

All 256 colors a module gets. Index 0 is white, 255 is black; everything Starry Skyline draws is some row of this grid. Starlight is the gray ramp; the comet is pure red from the red ramp.

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.

68k stack
pen
red · sparse
fetch fault dispatch rasterize

One segment of the comet, instruction by instruction. The 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:

sparse
$22008800
medium
$DDFF77FF
gray
$AA55AA55
solid
$FFFFFFFF
eraser
$00000000

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

1989 Mac: drawing takes ~0.25 s; CRT rescans every 16.7 ms → comet visible ✓ naive emulator: drawing takes microseconds; one sample per frame draw+erase already over… …before the sample ✗ Afterglow: publish the framebuffer mid-frame, between drawing calls → comet visible ✓
Red bar: the comet's draw-and-erase lifetime. Blue ticks: moments the display actually samples the bitmap.

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

Turn on Sound in the panel and wait for the next comet; it arrives with “Wes' Meteor,” the digitized whoosh from the resource table above, decoded from the original 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.