The plan article records the milestones; this one records the failures between them. Everything below comes from docs/LOG.md in the repository, the measurement log that every session appends to. None of these is a war story for its own sake — each one changed a rule, a build flag, or a line of the design, and the point of writing them down is that the next extension for this platform does not have to rediscover them.

Two guest faults, one rule

Basilisk II does not report a bad memory access to the guest; it quits. Twice in the first sessions the emulator vanished with catch_exception_raise → sigsegv_dump_state → QuitEmulator, and each time the cost was a reboot and a check that the disk image had survived.

The first fault was the spike tool’s first QuickDraw call. An MPW tool has no QuickDraw globals unless it declares QDGlobals qd and calls InitGraf; the linker had said so plainly (Undefined entry "qd"), and the workaround — a private gray pattern — silenced the message instead of the cause. GetPort, SetPort and FrameRect then read through A5 into an unmapped address. This is not a concern for the extension itself, whose jGNE filter runs with the front application’s globals, but the diagnosis took a reboot to arrive at.

The second was the window overview’s screen capture: a temporary port built with NewPixMap and SetPortPix and closed with ClosePort took the guest down. The form that works edits the port’s own portPixMap and closes with CloseCPort. A related spike-only mistake — disposing, in one tool run, a window and canvas allocated in a previous run — also faulted, because a tool’s heap dies with the tool.

The rule that came out of it stays in force: every new mechanism goes behind a verb in a spike tool, and the non-drawing verb runs first. move none, which only moves port origins and regions and touches no pixel, ran before any of the repaint variants.

The Layer Manager virtualises the Window Manager

Crutch’s 2021 betas reported that PaintBehind and CalcVisBehind ignore background layers unless the current layer is switched with undocumented calls, and the plan budgeted three sessions for a ladder of repaint recipes. The measurement was shorter than the plan. Pointing the WindowList low-memory global at another layer’s list around PaintBehind, from ToolServer’s own layer, had no visible effect at all: the Layer Manager keeps a Window Manager state per layer, so the call painted inside ToolServer’s invisible layer whatever $9D6 said. Worse, the daemon’s console stopped repainting afterwards because its visRgn had been moved, until a restore layer put it back.

The plain, public MoveWindow, one call per window, moved three windows of three processes to their edges in one tick with the desktop repainted at once, and restored them pixel-identical in zero ticks. Crutch’s per-layer optimisation had been for a Mac II; on anything faster the documented call is the recipe. The same lesson repeated in Phase 4: instead of repainting the desktop across layers after the overview, the overview is a window of the front application, and DisposeWindow makes the Window Manager repaint every layer underneath.

The extension on Mac OS 9.0.4

The 68kMLA thread’s worst report was a Pismo that locked up under one of Crutch’s betas on Mac OS 9, and the plan therefore kept the shipped extension inert above 8.5 until a probe had walked the Layer Manager there. The probe found the same hierarchy as System 7.6.1 — the $DEAD marker, parent and sub-window links at the same offsets, the Finder desktop as window kind 20. With the gate lifted, park and restore worked at the first attempt:

Mac OS 9.0.4 in SheepShaver after F11: the daemon’s console parked at the left edge with 3 px showing, the desktop icons untouched

The overview showed the one thing the probe had flagged as a difference. On Mac OS 9 the system layer holds visible windows of its own, and the Control Strip’s tab — window kind 5, 24 by 18 pixels — is one of them. The tiling code treated it like any other window and scaled it up to half the screen:

Mac OS 9.0.4 after F9: the console as a titled tile, and the Control Strip’s tab scaled up to half the screen

For comparison, the same overview on System 7.6.1, where the system layer has nothing visible in it:

The overview on System 7.6.1: three windows of three applications as tiles on a gray canvas

The fix is a window-kind filter for the system layer’s furniture, already in the source. No crash and no Finder lock-up on 9.0.4: the design with MoveWindow and a real window for the overview, and no private Layer Manager calls, does not reproduce the Pismo failure.

Getting the extension onto that guest at all was the larger part of the session. The daemon installed there was an old build without file verbs, so the current AppleBridge Kit went in as a disk image mounted by SheepShaver. SheepShaver itself had been running under sudo with ether etherhelper and an unreachable address, and was moved to slirp with a rewritten start script. And the daemon’s screenshot verb, which photographs the guest’s own framebuffer on System 7, answers “Screen too large to capture” on a 1920-pixel, 32-bit screen — the Mac OS 9 pictures are captures of the SheepShaver window from the host side, by CGWindow ID so that they work across Spaces.

Three ways a code resource goes wrong at build time

A boot INIT and a control panel are code resources: no A5 world, no data segment, no runtime library. MPW SC and Link have opinions about that which surface in unexpected places.

Error 31 is a feature. SC -model near puts every string literal into a Data GlobalDataSeg module, and the resource link then fails with Global Data present but there is no Data Initialization. That is a reliable detector for accidental global data; the cure is SC -b, which places literals into the code, PC-relative. Adopted for the INIT and the cdev alike.

Offset 0 is whatever the compiler emits first. The first build of the control panel restarted System 7 the moment it was opened. DumpObj -mods — read off the daemon’s console, because the Apple Event reply was long enough to drop the bridge connection — listed CDEVMAIN as the seventh module: the static functions of a header included above it had been emitted first, and the Control Panel jumped into PrefsDefaults. Forward declarations before CDevMain, the header included after it.

MPW’s int is 32 bits. Even short % 10 calls the LMODT runtime routine, and (n + cols - 1) / cols with three short operands pulled LDIVT into a code resource that must not link a runtime. The 68000 build target found the last of these; every long multiply and divide now goes through two shift-and-add helpers, decimal digits in the control panel are produced by subtracting powers of ten, and the zoom fractions are 1/1024 rather than a division.

Things that only looked broken

The blank menu bar. After the first live F11 the menu bar was empty, and the animation’s clip region was blamed and fixed. The baseline screenshot taken later showed the bar blank before any F11: the front application at the time, the bridge’s config app, draws no menu bar. The clip fix was still correct and stayed, and so did a DrawMenuBar after each transition — but the defect it was written for never existed.

“Only SimpleText came back.” After a restore-all, one window was visible and three were not. The layer walk showed all four at their original rectangles; the others were behind SimpleText’s 620-pixel window. Positions are verified with the probe now, not by eye.

The stub that could never be clicked. The first click-on-stub test fired restore-all instead of the per-window hook. The edge-click rule restores everything for a click within 5 px of a screen edge — and a stub is, by construction, within 3 px of the edge, so the rule pre-empted the hook every time. Restore-all now requires FindWindow == inDesk; a click on a stub goes to the stub’s owner, whose SelectWindow the hook then sees.

System 7.6.1 with every window parked: the 3-px stubs at the left and right screen edges are the only thing left to click, and each sits inside the 5-px edge band of the restore-all rule

Driving a cooperative machine from outside

Everything is tested through AppleBridge: keystrokes and clicks injected by the daemon, screenshots read back. On a cooperatively scheduled system this has limits, and each was met once.

  • The control panel’s “Set Key…” loop holds the CPU for five seconds waiting for a keystroke. A key injected by the daemon — a separate process — never arrives in time. That test was done at the real keyboard, and passed; the same goes for push buttons, which did not take synthetic clicks where checkboxes did.
  • The overview’s modal loop must yield, or the whole machine stalls with it, the daemon’s heartbeat included. With WaitNextEvent sleeping one tick per iteration the daemon answered with eleven seconds of latency and the bridge’s timeouts fired; at ten ticks it photographed the open overview and delivered a click into it.
  • A Link under ToolServer takes long enough that the daemon drops the connection (“peer closed mid-response”) and redials. A Catenate queued behind it is what gets orphaned; the build script runs, waits, and then reads the error file.
  • A .a file duplicated from the shared folder arrives typeless and Asm refuses it as “Not a text file (OS Error -31001)”; SetFile -t TEXT -c 'MPS ' first.

What remains

The SE/30 with its German System 7.5 is the one compatibility target still unmeasured — the thread’s report there was an instant bus error, and it will either reproduce or not. The 1-bit rendering of the overview has not been seen on real hardware, and the zoom-rect animation itself has never been photographed: twelve frames in a quarter of a second are too fast for a screenshot, and a host-side video capture is the way to look at it.