The first article described v0.1 of an Exposé extension for System 7: a boot INIT and jGNE filter that hides every window of every application with the Finder’s zoom-rect animation. That is the core of what PB170 asked for on 68kMLA in 2021 — but only the core. The thread specified more, Crutch’s betas 3–5 added more still, and the testers’ reports named the places where it broke. This article is the plan for the rest, written before the work so that the measurements can be held against it afterwards.
Scope decisions
Four choices were open; they are now made.
| Question | Decision |
|---|---|
| Edge behaviour | Move windows to the edge with 3 px still visible — PB170’s specification and mock-up — rather than hiding them |
| Window overview (the other half of Exposé) | Live tiles: scaled copies of each window’s current pixels plus its title, click to switch. No snapshot cache, no trap patches on HiliteWindow/BringToFront |
| Test matrix | Basilisk II / 7.6.1 (development), SheepShaver / Mac OS 9, a real SE/30 (German System 7.5, over the serial bridge), and a 68000 build variant |
| Control panel | A functional cdev with a plain System 7 layout; PB170’s design adopted later if he shares the resource |
Two rules from v0.1 stay in force. The INIT body remains A5-free and position-independent (-model near, no libraries, all state in the resident block). And every new mechanism is proven first behind a verb in a spike tool, non-drawing verb first — a guest memory fault costs a reboot and risks the disk image, as the two crashes of the first session showed.
Design decisions settled up front
- The control panel talks to the INIT through a Gestalt selector
'EXPO'that returns the resident block’s address. It is a documented API, callable from a cdev with an inline trap, and it answers “is the extension loaded, which version” at the same time. A system-heap scan for the block’s signature was the alternative; AppleBridge’s own code notes such scans are far too weak (BRA.Wis a common opcode). - The block layout is a contract. A
Prefsstructure sits at the start of the globals (corner mask, hide key and modifiers, overview key and modifiers, animation style, edges on/off), followed byshown,busy,mode,abort, then the working state. The cdev writes onlyPrefs; the assembly hooks testshown/busyby offset. - Once windows are edge stubs, an application switch no longer restores all of them. A stub is a real window; clicking it is a
SelectWindow, which restores that one. The trigger and a click on bare desktop near an edge restore everything; Shift + trigger is a hide-only mode that keeps pushing new windows out. - The overview’s screen restore reuses the edge mode’s repaint primitive, which is why the edge mode must be built first.
- Settings never live in a resource. Crutch lost a week to a settings resource that silently lost its system-heap bit. File plus resident block, nothing else.
Phases, ordered by risk retirement
flowchart LR P0["0 - probes
toolchain, publish"] --> P1["1 - edge stubs
repaint primitive"] P1 --> P2["2 - INIT integration
per-window restore"] P2 --> P3["3 - prefs, Gestalt,
control panel"] P1 --> P4["4 - window overview
live tiles"] P2 --> P5["5 - compatibility
OS 9, SE/30, 68000"] P3 --> P6["6 - polish, v1.0"] P4 --> P6 P5 --> P6
Phase 0 — toolchain probes (1 session)
Two compiler questions decide how much of the later code can be written in C. Does MPW SC honour #pragma parameter for register-based OS traps (NewPtrSys, Gestalt, PBReadSync, InsTime) when no library is linked? A probe resource built exactly like the INIT answers that from the linker’s error file; if not, a small header of hand-written three-word inlines replaces them. And which runtime routines does a -mc68000 build of the current code pull in (%LMUL, %LDIV)? — the input to the 68000 variant.
Phase 1 — edge stubs: the repaint primitive (2–3 sessions, highest risk)
Moving a window without MoveWindow is straightforward: MovePortTo, OffsetRgn on its structure and content regions, and a union of old and new structure regions as the dirty area. What is not straightforward is repainting that dirty area across every process’s layer in one pass — Crutch reported PaintBehind and CalcVisBehind ignoring background layers unless the current layer is switched, and the Layer Manager’s switching calls are undocumented. The phase is therefore a ladder of verbs, each photographed:
move— geometry only, no repaint (a crash test)paintlayer— per leaf layer, point theWindowListlow-memory global at that layer’s window list aroundPaintBehind/CalcVisBehind(what the private switch does, without needing its selector)paintroot— onePaintBehindfrom the root layer (does the patchedPaintOnerecurse?)movewindow— plainMoveWindowper window as the baseline (does the artefact even occur on 7.6.1?)finder1/finder2— the two ways to make the Finder repaint the desktop at once: hide → move → oneWaitNextEvent(0)→ show, orValidRecton the moved windowsrestore <n>/restoreall
The exit criterion is one recipe whose restore is pixel-identical to the state before, checked by screenshot difference. The same phase adds the hide-only mode, a constant 0.25 s animation with a trail length that shrinks as the window count grows, skipping fully obscured windows, and the WindowShade case (collapsed windows go fully off-screen instead).
Phase 2 — integration and per-window restore (2 sessions)
The winning recipe replaces ShowHide in the INIT. Per-window restore — double-clicking the Finder icon of a folder whose window is at the edge brings back only that window — needs head patches on SelectWindow, ShowWindow and BringToFront, installed at boot with GetToolTrapAddress/SetToolTrapAddress, the mechanism AppleBridge already uses for its global MenuSelect patch. The hook runs in whichever process called the trap, which is the correct context: that is where the click arrived.
Phase 3 — preferences, Gestalt channel, control panel (2–3 sessions)
A text file Exposé Prefs in the Preferences folder (KEY=value, CR-terminated), read at boot by A5-free C through the parameter-block file calls. The cdev follows AppleBridge’s control panel skeleton — entry at offset 0, state in the cdevValue handle, resource ids from −4064 upward — with four hot-corner checkboxes, two “Set…” keystroke buttons whose capture dialog shows the modifiers live, animation and window-edge switches, and a status line that says whether the extension is loaded. Changes apply immediately: the cdev asks Gestalt for the block and copies the Prefs structure into it.
Phase 4 — window overview with live tiles (3–4 sessions, second-highest risk)
A second trigger tiles every window: the grid is ceil(sqrt n) columns, each tile scaled to fit with the aspect kept. Every window’s currently visible pixels are copied from the screen into a per-tile offscreen before anything is drawn, so later tiles never capture earlier ones; then the desktop is filled gray and the tiles blitted with a frame and title. This has to work at 1 bit (a PowerBook 170) and at 8 bits, so the drawing forces black/white around the blits and avoids any “dimming” arithmetic. The modal loop inside the event filter is the risk: it is bounded by a 30-second deadline and by a Time Manager task that sets an abort flag from interrupt time, and selecting a window of another process is deferred — SetFrontProcess first, SelectWindow on a later event when that process is current.
Phase 5 — compatibility matrix (2–3 sessions)
- 68000: the two long multiplications and divisions are rewritten with shifts and a 32-bit shift-subtract divide; a second build target without
-mc68020; the criterion is a link with no runtime references. Verified on the 68040 guest and the 68030 SE/30; a real 68000 is not available here. - Mac OS 9 in SheepShaver: the layer-walking probe tool runs there before any INIT — the Layer Manager changed in 8.5 and Crutch’s beta locked up a Pismo. The shipped INIT is inert above 8.5 unless a preference overrides the gate.
- SE/30, German System 7.5: deploy over the serial bridge into
Systemerweiterungen, reboot, trigger by injected key, read the screen by region-cropped screenshot. The thread’s SE/30 report was an instant bus error; this is where it either reproduces or does not.
Phase 6 — polish, packaging, publication (1–2 sessions)
Version and icon resources, a startup icon if it can be drawn without a library, the MacBinary and BinHex of the extension and the control panel, a compatibility table in the README, the measurement log, an update to these articles, the v1.0 tag, and a follow-up in the 68kMLA thread.
The unknowns, and where each is measured
| Unknown | Measured in | Fallback |
|---|---|---|
| Cross-layer repaint after moving windows | Phase 1 verb ladder | MoveWindow per window (“beautiful but slow”) |
| A modal loop inside the event filter | Phase 4, first from a tool, then in the INIT | Deadline + interrupt-time abort flag |
| SC and register-based traps without libraries; a runtime-free 68000 build | Phase 0 link probes | Hand-written three-word inlines |
| Mac OS 8.5+/9 Layer Manager | Phase 5 probe before any INIT | System-version gate keeps the INIT inert |
Method
Every phase is built through AppleBridge: sources converted and copied into the guest, the MPW build script run and its error file read afterwards, the resource verified with DeRez, the extension installed into the Extensions folder, the guest rebooted through the daemon, and every state driven by injected keys and clicks and recorded as a screenshot before and after. Each verified step is a commit in github.com/LoetLuemmel/Expose; docs/LOG.md there receives the measurements, and these articles the milestones.
Estimated size: eleven to fourteen sessions end to end. Phase 1 is the one that decides whether the specification’s edge stubs are achievable at Crutch’s speed on this toolchain — which is why it comes first.
Progress log
2026-08-29, same day as the plan — Phases 0, 1 and 2 done. Phase 0: SC honours #pragma parameter for the register-based OS traps without any library (only PBReadSync/PBCloseSync are glue), and a 68000 build needs exactly ULMULT and LDIVT, both from the interpolation routine. Phase 1 closed in one session instead of three: the plain MoveWindow moved three windows of three processes to their edges in one tick, stubs visible, desktop repainted at once, restore pixel-identical — while the per-layer PaintBehind route did nothing from a foreign layer, because the Layer Manager virtualises the Window Manager state per layer. No private call needed. Phase 2: the boot-time head patches on SelectWindow, ShowWindow and BringToFront work — a click on a window’s stub makes its owner call SelectWindow and only that window returns; the Gestalt('EXPO') channel is live and a State tool reads the resident block. One rule fell out of the measurements: restore-all on an edge click must require FindWindow == inDesk, because a stub is by construction within the edge band and would otherwise never reach the hook. v0.2 is in the repository.
Pit
2026-08-29, later — Phase 3 done (v0.3). The extension reads Expose Prefs at boot (one KEY=value per line, in the Preferences folder) and a control panel writes it — hot corners, the trigger key captured from the next keystroke, animation on/off, window edges on/off — and copies the same structure into the resident block through Gestalt('EXPO'), so a change applies at once: ticking “Bottom right” and moving the mouse there parked the windows without a restart, and a captured F10 replaced F11 on the spot.


Three things the build taught. SC -b is what keeps string literals out of the A5 data segment — without it the resource link fails with Error 31, which turns out to be a reliable detector for accidental global data. A static function included from a header before CDevMain is emitted first and pushes the entry off offset 0; the first build of the panel restarted the machine that way. And MPW’s int is 32 bits, so even short % 10 pulls the LMODT runtime into a code resource that must not link one — decimal digits are produced by subtracting powers of ten. On the testing side: checkboxes accept the bridge’s synthetic clicks, push buttons need the real mouse, and a key-capture loop that holds the CPU cannot be fed by an injected keystroke at all on a cooperatively scheduled system — that one test was done by hand, and passed.
Pit
2026-08-29, evening — Phase 4 done (v0.4): the window overview. F9 tiles every window of every application: each window’s currently visible pixels are copied from the screen into one offscreen canvas, scaled with the aspect kept, framed and titled, and shown in a temporary full-screen window that belongs to the front application — disposing that window afterwards makes the Window Manager repaint every layer underneath, which removed the whole “repaint the desktop across layers” problem from the design. A click on a tile selects the window; if it belongs to another process, that process is brought forward and the filter completes the selection on its next event.

Two measurements shaped the loop. The first capture attempt faulted the guest — building a temporary port with NewPixMap/SetPortPix and closing it with ClosePort — and the working form edits the port’s own pixmap and closes it with CloseCPort. And the modal loop must yield: with WaitNextEvent sleeping one tick per iteration the bridge daemon answered with eleven seconds of latency and its screenshots timed out; with ten ticks it photographed the open overview and delivered a synthetic click into the loop. The loop is bounded by a 30-second deadline and by a Time Manager task that raises an abort flag from interrupt time. Remaining for later phases: the overview key in the control panel, and a 1-bit run on real hardware.
Pit
2026-08-29, late — Phase 5a done (v0.5); 5b and 5c prepared. Every long multiply and divide in the extension now goes through two shift-and-add helpers, so the same source links as a 68000 code resource with no runtime references at all — the last one to go was an innocent (n + cols - 1) / cols, because MPW’s int is 32 bits. The 68000 build was then installed on the 68040 guest and passed the full regression (park, restore, overview, cancel), which verifies the arithmetic if not the processor. The extension also gained its system-version gate: on Mac OS 8.5 or later it registers its Gestalt selector and does nothing else, and the control panel reports “inert on this system”, until the Layer Manager walk has been measured there — G=1 in the preferences file overrides the gate for the adventurous.
For that measurement the layer probe now exists as a standalone application that writes its report beside itself (SheepShaver has no ToolServer), verified on 7.6.1. The SheepShaver instance was moved from etherhelper to slirp the way the AppleBridge installer prescribes; the Mac OS 9 run and the SE/30 run are the two open items before v1.0, together with the Phase 6 polish — the overview key in the control panel, a clipped label, version and icon resources.
Repository state: tags v0.1 … v0.5, binaries for 68020+ and 68000, the control panel, the probe application, and a log of every measurement in docs/LOG.md.
Pit
2026-08-29, later still — Phase 5b: Mac OS 9.0.4 passes. The probe application, run from the AppleBridge Kit mounted as a SheepShaver disk, reported the same Layer Manager hierarchy as System 7.6.1 — the $DEAD marker, the parent and sub-window links at the same offsets, the Finder’s desktop as window kind 20 — with one difference: on Mac OS 9 the system layer holds visible windows of its own, the Control Strip’s tab among them. With G=1 in the preferences the gate was lifted, the 68000 build installed into Systemerweiterungen, the machine rebooted through the bridge, and F11 parked and restored, F9 showed the overview and Escape disposed it — no crash and no Finder lock-up, the failure Crutch’s beta met on a Pismo. The Control Strip tab did appear as an oversized tile; a window-kind filter for the system layer’s furniture is already in the source. Getting there took a detour worth recording: the old daemon on that guest had no file verbs, so the kit went in as a mounted disk image, and SheepShaver was moved from etherhelper to slirp with a rewritten start script. One target remains for Phase 5: the SE/30.


The two pictures are host-side captures of the SheepShaver window, because the daemon’s own screenshot verb refuses a 1920-pixel, 32-bit screen; the System 7 pictures in the earlier entries came out of the guest itself.
Pit
