An emulator in one sitting
Run Starry Night 2.0 with Musashi
Start with an empty directory. Finish with the original After Dark 2.0 module executing as 68k machine code and drawing into an HTML canvas.
No after-dark-vm dependency — this is the machinery underneath
What we are building
An After Dark module is not a picture or a movie. It is a Macintosh code
resource with a small message-based ABI. After Dark prepares a parameter
block, calls the module with messages such as Initialize,
Blank, and DrawFrame, and supplies the Macintosh
Toolbox services the code expects.
Our host does the same thing at the narrowest useful scale. Musashi handles CPU instructions; about 670 lines of intentionally plain C provide guest memory, the After Dark call frame, and just enough QuickDraw for this version of Starry Night.
Why C + Emscripten
Musashi is C, and its integration surface is six memory callbacks plus CPU control functions. Keeping the host in C makes registers, guest addresses, big-endian reads, and stack layouts explicit. Emscripten then compiles the complete CPU and host to WebAssembly.
No binding layer around Musashi and no duplicated guest-memory model.
Two EM_JS functions copy RGBA bytes to ImageData and update status text.
The browser already has a canvas; avoiding a windowing library keeps the example direct.
We construct only the low-memory globals and Toolbox behavior this one module needs.
Rust could provide stronger types, but wrapping a C CPU core and browser APIs would add build and FFI surface here. JavaScript could host a Wasm Musashi build, but every trap would cross the Wasm/JS boundary. For this tutorial, C is the shortest path from opcode to pixel.
Lay out the project
You need Git, Make, a C compiler, Python 3, and an activated Emscripten SDK.
mkdir -p starry-musashi/src
cd starry-musashi
SOURCE_BASE=https://morphing.cloud/afterglow/tutorial/sample
curl -fsSLO "$SOURCE_BASE/Makefile"
curl -fsSLO "$SOURCE_BASE/index.html"
curl -fsSLO "$SOURCE_BASE/m68kconf_tutorial.h"
curl -fsSLO "$SOURCE_BASE/musashi-a-line.patch"
curl -fsS "$SOURCE_BASE/src/mac.h" -o src/mac.h
curl -fsS "$SOURCE_BASE/src/mac.c" -o src/mac.c
curl -fsS "$SOURCE_BASE/src/main.c" -o src/main.c
# Copy your projected module resource fork.
cp "/path/to/projected/Starry Night" starry-night-2.rsrc
The companion project does not include Starry Night. Copy your projected
Starry Night 2.0 resource fork to starry-night-2.rsrc. “Projected”
means it contains a standalone ADgm resource with ID 0. Projection
from the After Dark 2.0 control panel is a separate preservation step.
Find ADgm 0
A classic resource fork starts with offsets to its data and map sections.
read_adgm() walks the type list, finds type ADgm, then
copies resource ID 0 into an ordinary host buffer. We load those bytes at
guest address $100000.
for (uint32_t i = 0; i < type_count; i++) {
uint32_t type = types + 2 + i * 8;
if (memcmp(fork + type, "ADgm", 4) != 0) continue;
/* Find reference ID 0, then copy its data block. */
...
memcpy(copy, fork + block + 4, size);
return copy;
}
This loader is deliberately strict. It validates every offset before using it
and reports a clear error if the fork is malformed or has no ADgm 0.
A production importer also understands MacBinary, AppleDouble, HFS images,
archives, and the built-in DRVR form used by the original control panel.
Give Musashi a machine
Musashi does not allocate RAM or know what a Macintosh is. The host owns a 16 MiB byte array and exposes big-endian 8-, 16-, and 32-bit accessors through the callback names Musashi expects.
unsigned int m68k_read_memory_8 (unsigned int a) { return r8(a); }
unsigned int m68k_read_memory_16(unsigned int a) { return r16(a); }
unsigned int m68k_read_memory_32(unsigned int a) { return r32(a); }
void m68k_write_memory_8 (unsigned int a, unsigned int v) { w8(a, v); }
void m68k_write_memory_16(unsigned int a, unsigned int v) { w16(a, v); }
void m68k_write_memory_32(unsigned int a, unsigned int v) { w32(a, v); }
The rest of the synthetic machine is small but significant:
- a stack, code area, bump-allocated heap, and a sentinel return address;
- classic low-memory globals for
Ticks,ScrnBase,thePort,CurrentA5, and the random seed; - a minimal
GrafPort, QuickDraw globals, screenBitMap, and regions; - a stable A5 world and a 44-byte After Dark
GMParamBlock.
m68k_init();
m68k_set_cpu_type(M68K_CPU_TYPE_68EC020);
m68k_pulse_reset();
Call the module as After Dark would
The module entry point uses a Pascal-style stack. Before every message,
begin_message() resets SP, reserves an OSErr result,
pushes the parameters, installs a sentinel return address, sets A5, and starts
PC at the code resource.
push16(0); /* OSErr result slot */
push32(storage_var); /* VAR storage */
push32(blank_rgn);
push16(message);
push32(param_block);
push32(SENTINEL); /* guest RTS stops here */
m68k_set_reg(M68K_REG_A5, A5_WORLD);
m68k_set_reg(M68K_REG_PC, CODE_BASE);
| Message | Value | What the host does |
|---|---|---|
| Initialize | 0 | Let the module allocate and populate its storage handle. |
| Blank | 2 | Prepare the screen and reset animation state. |
| DrawFrame | 3 | Advance the original module once; repeat from the browser loop. |
When the guest executes RTS, PC becomes SENTINEL. That
is our clean return condition. It avoids inventing a second calling mechanism
around Musashi.
Turn A-line opcodes into QuickDraw
Macintosh Toolbox calls compile to words in the $A000–$AFFF range.
Upstream Musashi treats an A-line word as a CPU exception. The included
four-line patch first offers it to Musashi’s existing illegal-instruction
callback; m68kconf_tutorial.h connects that callback to
mac_trap().
+ /* Macintosh Toolbox calls use A-line opcodes. Let the host handle them. */
+ if (m68ki_illg_callback(REG_IR))
+ return;
The dispatcher reads parameters from the guest stack, performs the host-side operation, pops exactly those parameter bytes, and returns 1 to suppress the CPU exception. Unknown traps stop with the opcode and guest PC instead of silently corrupting state.
NewHandleClear, HLock, HUnlock
Random, foreground/background color, clip and pen state
EraseRgn, PaintRect, PaintOval, and Line
MapPt maps module coordinates into the demo rectangle
This is the central lesson: CPU emulation is only one layer. The module becomes visible when a host supplies the OS contracts around it. Starry Night is a good first target because its projected code and narrow QuickDraw vocabulary let us see that boundary without first implementing an entire Macintosh.
Copy RGBA pixels to the canvas
The QuickDraw shim writes directly into a 640 × 480 RGBA array. An Emscripten
EM_JS function views the same bytes through HEAPU8 and
publishes them with putImageData(). There is no renderer framework
between the trap and the canvas.
EM_JS(void, present_canvas, (const uint8_t *pixels), {
const canvas = document.getElementById("screen");
const context = canvas.getContext("2d");
const length = 640 * 480 * 4;
if (!Module.starryImage)
Module.starryImage = context.createImageData(640, 480);
Module.starryImage.data.set(HEAPU8.subarray(pixels, pixels + length));
context.putImageData(Module.starryImage, 0, 0);
});
Drawing traps end the current Musashi timeslice. The browser callback resumes
up to eight such yields per animation refresh, presents if anything changed,
and starts the next DrawFrame after the guest returns. This keeps
Starry Night’s intra-frame erase/draw sequence responsive without blocking the
browser event loop. See Emscripten’s main-loop API and C/JavaScript interop guide for the underlying mechanisms.
Compile and run
The Makefile clones a pinned Musashi
revision, applies the A-line callback patch, generates its opcode table, and
invokes emcc. The resource fork becomes a preloaded file in
Emscripten’s virtual filesystem.
make
make serve
# Then visit http://localhost:8000
You should see a black 640 × 480 display accumulate Starry Night’s original stars. The status line reads “Starry Night 2.0 · original 68k code.”
Headless smoke test
The native target uses the same guest host without the canvas. It is the fastest way to distinguish an emulation error from browser presentation.
make native
./starry-native starry-night-2.rsrc 120
frames=120 lit_pixels=674
Confirm that your file is the same projected Starry Night 2.0 module. A different release or module may use more Toolbox calls. The error gives you the next opcode and PC to implement; that iterative loop is exactly how a broader module host grows.
Complete source
These are the exact files used for the native and WebAssembly validation of this tutorial. Expand them to read inline or download them into the tree above. The resource fork is intentionally absent.
Makefileclone, patch, build, servedownload
Loading…
index.htmlcanvas shelldownload
Loading…
m68kconf_tutorial.hMusashi configurationdownload
Loading…
musashi-a-line.patchroute Mac traps to the hostdownload
Loading…
src/mac.hsmall public APIdownload
Loading…
src/mac.cguest, ABI, Toolbox, rasterizerdownload
Loading…
src/main.ccanvas and native harnessdownload
Loading…
README.mdquick referencedownload
Loading…
What a general module emulator adds
This host is intentionally specific: it explains the mechanism, runs Starry Night, and fails loudly outside that contract. A practical After Dark engine keeps the same architecture while making every layer deeper.
The conceptual loop does not change: execute guest code, intercept the contracts it expects, update host-visible state, and yield often enough to present it. Afterglow’s emulator is this experiment grown to support a much wider software ecosystem.