Code Review — Findings and Citations

Companion to the porting plan · 27 July 2026

Executor, read twice

The evidence behind A GNUstep Front End for Executor. The plan says what to do; this says what was found and where. Everything here is source reading with file and line citations. Where something could not be verified, it says so.

Method and honesty

Two trees were read in full: Cliff Matthews' 2008 MIT source release, which contains the 1993–97 NeXTSTEP front end, and autc04's modern C++17 fork, which is the build target. Five parallel reviews covered the old front end's architecture, the modern front-end contract, the input and output subsystems, the toolchain and GNUstep API surface, and the bundled HFS_XFer utility.

Nothing here was compiled or executed

No GNUstep toolchain was present on the review machine. Every GNUstep claim is static reading of libs-gui and libs-back at HEAD on 27 July 2026, plus project documentation. Claims about Executor itself are read from the two source trees. Section 12 lists what could not be settled either way.

Contents

  1. 01The front-end contract
  2. 02Threading and synchronisation
  3. 03Pixel formats
  4. 04Toolchain and runtime
  5. 05The 1993 front end
  6. 06Input, sound, printing, scrap
  7. 07GNUstep API gaps
  8. 08Interface files
  9. 09Rootless, in depth
  10. 10HFS_XFer
  11. 11Salvage register
  12. 12Unknowns
01 — The front-end contract

Exactly one abstract base class, Executor::VideoDriver vdriver.h:103-174. No required free functions. The global Executor::vdriver vdriver.h:176 is defined by the core at vdriver.cpp:10, not by the front end.

Pure virtual

SignatureLineConstraint
void runEventLoop():111Called on the main thread from main(). Must block until endEventLoop().
void endEventLoop():112Called from the emulator thread main.cpp:503. Must be async and thread-safe.
bool setMode(int w, int h, int bpp, bool grayscale):117Emulator thread. Allocates framebuffer_, returns true.
void requestUpdate() (protected):168Invoked with mutex_ already held. Must not block.

Also required

  • A constructor (IEventListener*, int& argc, char* argv[])argc by non-const reference. Required by main.cpp:416.
  • A default_vdriver.h supplying using DefaultVDriver = …, found via target_include_directories(… PUBLIC .). Included by main.cpp:62.

main.cpp is compiled as plain C++

So default_vdriver.h and everything it includes must be free of Objective-C. Qt solves this with bare forward declarations qt/qt.h:5-9. The GNUstep equivalent is #ifdef __OBJC__ @class Foo; #else typedef struct objc_object Foo; #endif.

Optional — all have working defaults

MethodDeclCallerQt overrides?
updateScreen(t,l,b,r):114dirtyrect.cpp:216no
isAcceptableMode(…):115qGDevice.cpp:396no (SDL2 does)
setColors(int, const vdriver_color_t*):116qGDevice.cpp:234no
putScrap / getScrap / weOwnScrap:119-121scrap.cpp:150, :260no — Qt has no clipboard
setTitle(const std::string&):123prefs.cpp:89no
setCursor / setCursorVisible:125-128qCursor.cppyes qt.cpp:301, :326
setRootlessRegion(RgnHandle):131windRootless.cpp:42base + commitRootlessRegion
beepAtUser():134osutil.cpp:849no
noteUpdatesDone / updateMode / handleMenuBarDrag:136-139toolevent.cpp, menu.cpp:1068no

The true minimum is proven by headless.h — sixteen lines implementing setMode and three empty methods.

The callback interface

IEventListener vdriver.h:62-78, reachable as the protected member callbacks_ vdriver.h:152. The concrete instance is EventSink vdriver.h:80-101. All methods are safe to call from the GUI thread.

mouseButtonEvent(bool down, int h, int v)   // convenience, :65
mouseButtonEvent(bool down)                // :71
mouseMoved(int h, int v)                   // :72
keyboardEvent(bool down, unsigned char mkvkey)  // :73
suspendEvent()                             // :74  focus lost
resumeEvent(bool updateClipboard)          // :75  focus gained
requestQuit()                              // :76
wake()                                     // :77  only Wayland uses it
02 — Threading and synchronisation

main.cpp:424-509 is unambiguous about ownership:

auto executorThread = std::thread([&] {
    ROMlib_InitGDevices(...);   // :441 → vdriver->setMode
    executor_main();            // :496
    vdriver->endEventLoop();    // :503
});
vdriver->runEventLoop();        // :506  MAIN THREAD, blocks all session
executorThread.join();          // :507

The toolkit's loop owns the main thread and calls into Executor through callbacks_. Executor never calls the front end to pump events — there is no pumpEvents(). This matches [NSApp run] directly.

GUI thread to emulator

Front-end calls are queued and delivered via a synthetic interrupt eventsink.cpp:90-95:

void EventSink::runOnEmulatorThread(std::function<void()> f) {
    std::lock_guard lk(mutex_);
    todo_.push_back(f);
    eventInterrupt.trigger();
}

where eventInterrupt eventsink.cpp:15-19 is serviced on the emulator thread and drains the queue in pumpEvents() :97-106. The front end never touches Toolbox state.

Emulator thread to GUI — your problem, three existing idioms

Front endMechanism
QtQMetaObject::invokeMethod queued, plus a hand-rolled mutex/condvar when it must block qt.cpp:179, 193-235, 298
SDL2onMainThread() pushes to todos_, wakes with a custom event, blocks on done_ sdl2.cpp:45-52, 219-226
Waylandeventfd + poll wayland.cpp:60, 186-200
GNUstep-performSelectorOnMainThread:withObject:waitUntilDone:

The deadlock, with its three call sites

requestUpdate() is invoked with mutex_ held at every site:

  • vdriver.cpp:143 lock → :152 in setColors
  • vdriver.cpp:158 lock → :171 in setRootlessRegion
  • vdriver.cpp:176 lock → :178 in updateScreen

And the draw path takes the same mutex qt.cpp:242. Therefore waitUntilDone:NO is mandatory. Conversely setMode, setCursor and setCursorVisible are called without the mutex and may block — Qt's setMode does.

Two more ordering rules

Release the mutex before updateBuffer(). Qt unlocks at qt.cpp:263 before calling it at :268, because it fans out to a thread pool of hardware_concurrency()-2 workers vdriver.cpp:20-86, :35 and runs long.

There is a startup race. The emulator thread can reach setMode main.cpp:441 before the main thread reaches runEventLoop :506. Qt survives because queued invocations wait in the queue. performSelectorOnMainThread:waitUntilDone:YES also survives. Do not design setMode around a run loop that is already spinning.

Under GNUstep, also enable multithreading early — the runtime must know it is multithreaded before performSelectorOnMainThread: behaves — and wrap every secondary-thread entry point touching Objective-C in an autorelease pool. Executor already had to do this on macOS macosx.mm:83.

03 — Pixel formats

What the guest writes

Executor::Framebuffer vdriver.h:43-60, constructed at vdriver.cpp:12-17:

rowBytes = ((width * bpp + 31) & ~31) / 8;   // rows padded to 4-byte multiples
DepthLayout
1, 2, 4, 8Palette-indexed, MSB-first within each byteIndexedPixelGetter vdriver.cpp:242-269 computes shift = 8 - (x*depth%8) - depth, decrementing.
16Mac RGB555, big-endian, read through GUEST<uint16_t> vdriver.cpp:364, unpacked with 5→8 bit expansion :366-375.
32Mac xRGB8888, big-endian, GUEST<uint32_t> vdriver.cpp:381, then | 0xFF000000.

Accepted modes are powers of two up to 32 vdriver.cpp:98-109; minimum size 512×342 vdriver.h:31-32. The framebuffer is big-endian Mac format. It must never be handed to AppKit directly.

What updateBuffer() produces

vdriver.cpp:271-405. Output is packed 32-bit 0xAARRGGBB in host byte order, no padding, stride bufferWidth * 4 :332, :341. On a little-endian host the memory order is B, G, R, A. Alpha is 0xFF except where a pixel lies outside the rootless region and equals opaque white, which is written as fully transparent :332. Work is chunked at 100 rows when a rect exceeds 160 rows :303-309.

Qt performs no conversion of its own. QImage::Format_RGB32 is bit-identical, so it passes qimage->bits() straight in qt.cpp:215, :268.

The AppKit equivalent:

NSBitmapImageRep *rep = [[NSBitmapImageRep alloc]
    initWithBitmapDataPlanes:&buf
                  pixelsWide:w pixelsHigh:h
               bitsPerSample:8 samplesPerPixel:4
                    hasAlpha:YES isPlanar:NO
              colorSpaceName:NSDeviceRGBColorSpace
                bitmapFormat:(NSAlphaFirstBitmapFormat
                            | NSBitmapFormatThirtyTwoBitLittleEndian)
                 bytesPerRow:w*4 bitsPerPixel:32];

That highlighted constant arrived in gnustep-gui 0.25. Verify before building around it.

Palette

setColors vdriver.cpp:141-153 receives 16-bits-per-channel vdriver_color_t vdriver.h:38-41 from gd_update_colors() qGDevice.cpp:216-234 and stores host-native 0xAARRGGBB into colors_[256]. It then dirties the whole screen and calls requestUpdate() :151-152 — correct, since a CLUT change alters every pixel's meaning. If you do not override it, palette handling is free. Qt does not override it.

Dirty rects

DirtyRects dirtyrect.h:10-31. Maximum five :13, auto-merged into unions when they overlap dirtyrect.cpp:150-196. Half-open, top-left origin. getAndClear() at :199-204.

Framebuffer::rgbSpec vdriver.h:56 is consulted only by qPixMapConv.cpp:265-268. Qt, SDL2, Wayland and headless all leave it null, which selects the correct Mac defaults. Leave it null.

04 — Toolchain and runtime

How front ends are selected

  • src/CMakeLists.txt:1set(FRONT_ENDS qt x sdl sdl2 wayland CACHE STRING …). A cache list; override with -DFRONT_ENDS="qt;gnustep".
  • :102-107 — every directory is unconditionally added; each decides for itself whether to create its target, guarded on its dependency being found qt/CMakeLists.txt:4.
  • :605-610 — requested list intersected with what actually built.
  • :612-637 — one executable per front end, executor-<name>, all from the same main.cpp; front-end-headless links into every one.
  • :641-647 — the first surviving entry wins the plain executor name.

Naming contract: library target front-end-<name>, directory src/config/front-ends/<name>/, and PUBLIC . on the include directories. Adding one is three edits plus a new CMakeLists.txt.

Runtime and compiler

ItemValueEvidence
Runtimelibobjc2 v2.3Released 2025-09-16; master active. MIT.
ABI flaggnustep-2.2gnustep-make 2.9.3 sets this as the ng default. ABI-identical to 2.0; 2.2 unlocks compiler fast paths gated in clang. Needs clang 18+.
Compilerclang onlyGCC has no -fobjc-runtime= option at all. gnustep-make's configure forces CC=clang: "The ng runtime library setting requires clang rather than gcc."
Library combong-gnu-gnulibobjc2 + gnustep-base + gnustep-gui.
Linkerlldgnustep-make warns GNU ld "might not produce working Objective-C binaries using the gnustep-2.0 ABI." The v2 ABI needs section-boundary symbols; symptom is cannot locate symbol __start___objc_selectors. gold is deprecated upstream.
C++ stdliblibstdc++Both work — libobjc2 CI covers libc++ and libstdc++ across LLVM 13–18. Keep one across the whole link; do not pass -stdlib=libc++.

Flags emitted by the ng combo, derived from gnustep-make's library-combo.make and common.make:

-fobjc-runtime=gnustep-2.2 -fblocks -DGNUSTEP_RUNTIME=1 -D_NONFRAGILE_ABI=1
-fexceptions -fobjc-exceptions -D_NATIVE_OBJC_EXCEPTIONS
link: -fexceptions -lobjc -lgnustep-base -lgnustep-gui

Do not call enable_language(OBJCXX)

CMake's CMakeDetermineOBJCXXCompiler sets clang++ ahead of the compiler list, so the effective Linux order is clang++, c++, g++ — while CXX detection has no clang preference and resolves c++g++. On a box with both, you get clang++ for the one .mm and g++ for ~400 .cpp files, splitting the C++ ABI across the vdriver.h boundary, which carries shared_ptr, function and string.

Additionally, CMake ships only Apple-*-OBJCXX.cmake platform modules — no Linux ones — so OBJCXX targets get no -rdynamic handling. And OBJCXX needs CMake 3.16 while this project's floor is 3.12. Let .mm ride CMAKE_CXX_COMPILER and set the language per source file.

The personality-function trap

Clang's default Objective-C runtime on Linux is GNUstep with an empty version tuple, and bare -fobjc-runtime=gnustep means 1.6. Without an explicit version, clang emits __gnu_objc_personality_v0 instead of __gnustep_objcxx_personality_v0 — and catching a std::exception then segfaults. Filed as LLVM issue 33904, closed invalid, the answer being "specify the runtime." CMake will compile .mm with no Objective-C flags at all and hand you crashes that read as heap corruption.

Two corrections to widely-repeated advice: libobjcxx no longer exists — removed in commit fefb333b0 (2017-12-25), folded into libobjc.so, though libobjc2's INSTALL file still describes it. And exception interop is regression-tested in both directions — Test/CXXException.m and ObjCXXEHInterop.mm, at -O0 and -O3, both ABIs, both stdlibs.

Objective-C++ mixing is fully supported: gnustep-make has a dedicated .mm rule, and libobjc2 has ENABLE_OBJCXX on by default. libobjc2 itself needs C++20 to build, but it is a separate shared object, so a C++17 consumer is fine. ARC works on Linux and is per-file, so ARC and manual-retain translation units mix freely — though this codebase is manual throughout.

Precedent

GNUstep's own android-examples compiles a .mm from CMake via gnustep-config with no enable_language. libobjc2 is itself a CMake project with Ubuntu CI. No Gershwin-specific build convention was located.

05 — The 1993 front end

8,453 lines excluding HFS_XFer. Every file is dual-compiled #ifndef OPENSTEP. The NEXTSTEP branch is finished, shipped code; the OpenStep branch is an abandoned 1997 port.

The framebuffer bridge

Two buffers. The guest one MacViewClass.m:103-105, allocated :470-473; the host one :89-95, page-aligned :465, in 16-bit RGB 4:4:4 declared once at :408-414.

The aliasing trick — the image rep is built around the client-owned buffer, so plain C stores are visible to it with zero copy MacViewClass.m:483-499. There is no shared memory, no framebuffer device, and no Display PostScript pixmap.

The blit :612-712 does not use setNeedsDisplay:. It grabs the drawing context from arbitrary emulator code, sets a multi-rect clip, and redraws the whole bitmap :696-709:

[self_view lockFocus];
NSRectClipList (nxr, num_rects);
[current_screen_bitmap draw];
[self_view unlockFocus];

Two supporting details that matter: [self allocateGState] :168, :243 — "since we will be repeatedly focused on" — without which per-frame lockFocus is ruinous; and the Y-flip per rect :693. drawRect: :340-393 exists only for genuine expose events and does the same twenty-line redraw.

Event loop

AppKit owns it. The emulator is a coroutine on a hand-built second stack, on the same thread. contextswitch() NEXTmain.m:110-168 is inline assembly — m68k at :127-138, i386 at :139-164 — pushing callee-saved registers, 108 bytes of FPU state and segment registers, then swapping stack pointers and returning into the other side.

The pump is - step MacViewClass.m:1059-1112: run guest code continuously until the host has an event waiting, then yield. Driven by a timer — on OpenStep, an NSTimer at .00000001 seconds :135-149, i.e. as fast as the run loop permits.

Re-entrancy is ugly. drawRect: can itself context-switch during printing :376-382. Clipboard services drive the guest synchronously by posting a synthetic Mac event and burning exactly three context switches :2626-2647, :2667-2679 — three, because that empirically worked.

blockinterrupts.m is declared dead by its own author

Line 76 is #error "This stuff has succumbed to bitrot; see new virtual interrupt stuff", and it is not in either makefile's source list. For the record, the disabled block at :16-72 implemented Mac sound completion routines by suspending the emulator thread, reading its registers, rewriting the program counter, resuming it, then restoring all three register banks. It carries a /* TODO set up stack with args… */ at :52.

Window model

One window for the entire Mac screen. grep -rni rootless over the whole 2008 tree returns zero hits. The class comments apologise MacViewClass.m:39-45:

/* NOTE: This isn't a very good NEXTSTEP object, because much of its
 * data is stored in static variables, instead of in instance
 * variables.  That means we can only have one instantiation. */

Backed by static MacViewClass *self_view; :77, which every C entry point reaches through. vdriver_init() :431-463 resizes the host window to the whole Mac screen. The nib confirms it: one WindowTemplate containing one CustomView.

Colour and depth

Guest depths 1, 2, 4, 8 only — no direct colour ever :784. Two host classes, detected once: colour, or 2-bit grayscale :170-171, :246-247, which forces vdriver_fixed_clut_p — the Mac is told it has a CLUT it cannot change :173-181. Conversion tables are built lazily :635-660. The entire "conversion" for the native NeXT display is *d++ = ~*s++ :553-608 — polarity inversion, unrolled four times.

The OpenStep branch was written but seemingly never compiled

  • MacViewClass.m:2606-2607validRequestorForSendType:(NSString), taking NSString by value. Cannot compile.
  • :2616-readSelectionFromPasteboard: declared void where the protocol wants BOOL.
  • :459, :2383, MacAppClass.m:202 — tracking rects unfinished, three separate #warnings.
  • :313-314 — "no longer disabling dead keys".
  • NEXT.c:659, :698 — keyboard translation gutted.
  • :2760 — print scaling hardcoded to 1.0.

Expect a first-compile bug tail beyond the catalogued API gaps.

06 — Input, sound, printing, scrap

Keyboard

The front end's entire job is: produce a Mac virtual key code and call callbacks_->keyboardEvent(down, mkv). KCHR lookup, KeyTranslate, dead keys, autokey and the key map all live in the core eventsink.cpp:42-68.

The mapping table is reusable verbatim

GNUstep's X11 backend sets keyCode from the raw X keycode — keyCode = ((XKeyEvent *)xEvent)->keycode; in XGServerEvent.m. The Wayland backend uses code = key + 8, which is the same numbering. So:

-[NSEvent keyCode]  →  x_keycode_to_mac_virt[]  →  MKV_*  →  keyboardEvent()

x/x_keycodes.cpp:11-148, 136 entries. Qt already compiles this file into its own target qt/CMakeLists.txt:12 and falls back to it when its symbolic lookup misses qt.cpp:114-119.

Ignore -[NSEvent characters] entirely. The classic code used charcodes because ROMlib wanted them; the modern core runs the guest's own KCHR, so host-translated characters would double-translate.

Modifiers are keys, not flags. ROMlib_GetModifiers() osevent.cpp:223-239 derives everything by testing the key map — ROMlib_GetKey(MKV_CLOVER) and friends. There is no flags path. The front end must deliver explicit down/up events for MKV_LEFTSHIFT 0x38, MKV_CLOVER 0x37, MKV_LEFTOPTION 0x3a, MKV_LEFTCNTL 0x3b, MKV_CAPS 0x39 and the right-hand variants 0x3c–0x3e. Constants at rsys/keyboard.h:94-211.

Tables available across the tree, should you need another space:

TableIndexed bySize
x/x_keycodes.cpp:11-148raw X11 keycode136
x/x_keysym.cppX11 keysym2 pages
sdl2/keycode_map.cpp:15-133SDL2 keycode~110
qt/qtkeycodes.cpp:7-122Qt::Key~100
win32/vk_to_mkv.hWin32 VK265
osevent/ibm_keycodes.cppPC scancode set 1

Wayland is not a viable target

keyboard_handle_modifiers accumulates flags into the config struct but generates no NSFlagsChanged events at all — command, shift and option would be dead. It also forces code = 0 for Enter and Delete. X11/cairo is the target.

Mouse

Trivially thin in the modern tree qt.cpp:84-103: forward view coordinates to mouseButtonEvent / mouseMoved. No Y-flip, no clamping — the core expects top-left origin. Override -isFlipped to return YES and all of the 1993 code's vdriver_height - y arithmetic NEXT.c:576, :597 disappears.

Required: [window setAcceptsMouseMovedEvents:YES], -acceptsFirstMouse:YES, and folding right-button into left for one-button Mac semantics NEXT.c:635-636, :644-645. GNUstep's X11 backend generates NSMouseMoved unconditionally when no button is down and has full scroll-wheel support; classic Mac has no wheel, and IEventListener has no scroll callback, so drop those.

Sound

Interface is SoundDriver sound/sounddriver.h:20-34 — a pull/hunger model where HungerStart() returns a buffer plus a time window, the core fills it, the driver plays it. Default is SoundFake sounddriver.cpp:18-24.

Executor is currently silent everywhere

The only real driver in the tree is config/front-ends/sdl/sdlsound.cpp, 171 lines, belonging to the legacy SDL1 front end that builds only when SDL1, X11 and Xext are all found. Grepping qt/ sdl2/ x/ wayland/ headless/ for sound or audio returns zero hits. The 1993 NeXTSTEP implementation was never written either — both functions in NEXTsound.m:35-41 are empty bodies.

NSSound cannot serve this. GNUstep's implementation loads only complete files (-initWithContentsOfFile:, -initWithData:) and streams them through GSSoundSourceGSSoundSink plugin bundles with a fixed 4096-byte buffer. There is no application-supplied PCM callback and no streaming API — no equivalent of SDL_AudioCallback or HungerStart. Only two sinks exist, libao and OSS. The code carries a FIXME admitting it grabs "the first available sink/device for now," and gnustep-dev described it in 2012 as "a bit of a mess" they'd like to revisit but which "doesn't seem likely in the foreseeable future." The architecture is unchanged on master.

SDL2 audio

Port the existing 171-line SDL1 driver. Mechanical: SDL_OpenAudioSDL_OpenAudioDevice. No new project dependency.

SoundKit

NEXTSPACE's Frameworks/SoundKit, PulseAudio-backed with a NeXT-style API. SNDPlayStream exposes -playBuffer:size:tag: plus write and empty callbacks — the hunger model, already built. GPL v2+.

Streaming NSSound

Would need a new GSSoundSink with an app-supplied buffer source. libs-gui's recent NSSound.m activity is a Feb 2026 thread-leak fix and a May 2025 video-playback commit; no open PRs mention sound.

NSBeep is fine for beepAtUser() — compare the X front end using XBell x.cpp:792-797.

Printing

Do nothing. print/prPrinting.cpp:452-516 already generates PostScript and popens it to a program read from the INI file's [Printer] section :461-467, falling back to a spool file. print/PSprint.cpp is 1,200+ lines of QuickDraw-to-PostScript, entirely independent of any front end. Printing is not part of the VideoDriver interface at all.

NEXTprint.m's ROMlib_availableFonts exists only in the NEXTSTEP branch and its only modern call site is guarded by a macOS macro PSprint.cpp:1169. On Linux these are dead declarations. MacPrintClass.h declares a class with no .m anywhere in the tree and zero references.

Correction: GNUstep still has working PS/DPS operators

PSOperators.h provides static inline C functions calling matching DPS* on the default context; DPSOperators.h expands each into a real C function-pointer dispatch through ctxt->methods->… — the same path AppKit uses internally. Colour, gstate, matrix, path, text, the NeXT compositing extensions, and a real variadic DPSPrintf are all present. PSWait() is a documented no-op.

Total DPS surface in the old front end is about ten call sites. Four DPSPrintf and four PS* calls compile unchanged; only DPSGetCurrentContext()GSCurrentContext() and [[NSDPSContext currentContext] flush][[NSGraphicsContext currentContext] flushGraphics] need substituting, plus two header renames. psfns.psw contains one line: % no longer needed, so pswrap effort is zero.

And the NSView printing callbacks — -rectForPage:, -beginPageSetupRect:placement:, -endPrologue, -beginTrailer, -endPage, -dataWithEPSInsideRect: — are all implemented in NSView.m, none stubbed. Only Fax is a stub.

Clipboard

This is where the 1990s code exceeds the modern tree. PutScrapX MacViewClass.m:1874-2038 handles five Mac types — TEXT, EPS , RTF , TIFF, PICT — each cached with its own generation counter so a multi-flavour copy declares all flavours atomically :1906-1970. Text and RTF get CR↔LF conversion and charset transcoding :1915-1916, :1942-1943. RTF additionally gets a synthesised \fonttbl spliced in by insertfonttbl() NEXT.c:720-760, walking the guest's own FONT/FOND resources.

GetScrapX :2042-2193 reads only if [pasteboard changeCount] > ROMlib_ourchangecount :2105-2106 — the guard that stops it clobbering the guest's own scrap.

Front endClipboard support
1995 NeXTSTEPFive flavours, charset conversion, RTF font tables
xTEXT only x.cpp:799-881, raw XA_PRIMARY/XA_STRING, spin-wait polling for SelectionNotify with 10 retries :853-857. No CLIPBOARD atom.
qt, sdl2, wayland, headlessNothing. SDL2's is commented out sdl2.cpp:200-206.

NSPasteboard maps onto the old code almost line for line. Two risks: it is a client of the external gpbs daemon, auto-launched via NSTask NSPasteboard.m:1985-2036 but a packaging concern; and cross-application interop is documented as weak — gnustep-dev in 2011: "Copy/paste from X11 apps to GNUstep doesn't work well (only plain text was working)," with the root cause being exactly the type-list update that getScrap's probe depends on. Keep the raw X11 path as a fallback.

Two bugs to fix in passing

Qt hardcodes resumeEvent(true) qt.cpp:147; SDL2 hardcodes false sdl2.cpp:201. Both are wrong. The 1995 code computed it from the pasteboard change count MacAppClass.m:209-215, which is what decides whether the guest needlessly reconverts the clipboard on every focus change.

07 — GNUstep API gaps

Bitmap drawing — the performance landmines

NSDrawBitmap() is real Functions.m:346 but allocates and releases a temporary NSBitmapImageRep on every call, then enters the same path as -[NSBitmapImageRep draw]. Strictly worse. Keep the aliasing-rep approach.

A non-canonical format costs 307,200 iterations per frame

GSContext.m:906-943 checks -isCompatibleBitmap: and, for anything that is not canonical 8-bits-per-channel interleaved, calls -_convertToFormatBitsPerSample:… NSBitmapImageRep.m:2427-2560 — a doubly-nested loop calling cached IMPs of getPixel:atX:y: and setPixel:atX:y: once per pixel. At 640×480 that is 307,200 iterations every frame, silently. The 1993 code's bitsPerSample:4 bitsPerPixel:16 MacViewClass.m:490-499 lands squarely in it.

And the 2-bit grayscale path draws nothing at all. Cairo bails unless the space is Device or Calibrated RGB, logging "Image format not support in cairo backend" and returning CairoGState.m:1042-1048. A silent no-op.

Both fixes are cheap: Executor's make_rgb_spec() is fully parameterised, so moving to 8:8:8:8 / 32bpp / device RGB is a constants change plus row-stride arithmetic MacViewClass.m:408-414, :423-425. Delete the grayscale path entirely.

Indexed/CLUT bitmaps are genuinely absent — NSCustomColorSpace and NSNamedColorSpace are declared NSGraphics.h:56, 60 with zero branches in NSBitmapImageRep.m. Irrelevant here, because Executor converts indexed to RGB before AppKit ever sees it.

CoreGraphics is not available. -[NSImage CGImageForProposedRect:context:hints:] is declared in a category NSImage.h:489-494 with no implementation anywhere — calling it raises "does not recognize selector."

Drawing performance

Invalidation is real dirty-rect accumulation: NSView maintains _invalidRect and propagates up the superview chain. All three backing-store types are genuinely distinguished by X11 — buffered and retained get an offscreen pixmap XGServerWindow.m:~1732.

No XShm anywhere in libs-back

A repo-wide search for XShm or MIT-SHM returns zero results. The default surface uses plain cairo_xlib_surface_create onto a server-side pixmap, so every framebuffer blit round-trips through the X protocol. There is also no surface cache for direct rep draws — each -draw does malloc → cairo_image_surface_create_for_data → paint → destroy → free CairoGState.m:1066-1210. The one upload-once-blit-many path is NSCachedImageRep, useless here since the bitmap changes every frame.

The only quantified data point is from 2015 — Savannah patch #8781, "Faster bitmap drawing on Cairo": 512×512 × 500 iterations, 0.880s → 0.810s, implying roughly 600 draws per second on that hardware. That suggests the raw cairo primitive is not the bottleneck for one 640×480 surface at 60fps. The risk is the surrounding pipeline. No 2020s-era benchmark exists.

Escape hatch. -[NSWindow windowRef] is implemented NSWindow.m:5955 and on X11 returns a gswindow_device_t * exposing Display *display; Window ident; Drawable buffer; x11/XGServerWindow.h:83-104, from which XShmPutImage is mechanically reachable. Two caveats: it is unverified whether distribution -dev packages install that internal header, and no evidence was found that anyone has ever done this — no blog post, list thread or issue. The one demonstrated GPU path is NSOpenGLView with a real GLX backend.

No prior art of any kind

No emulator was found using GNUstep AppKit for framebuffer display. Previous, Basilisk II and SheepShaver all use SDL, Cocoa, Qt or GTK. Searches for "gnustep emulator" surface only terminal emulators. Absence of evidence rather than evidence of absence — but you would be first.

Window level and style — the constraint on rootless

APIguiReality
Borderless / Titled / Closable / Miniaturizable / ResizableyesThe only masks with effect GSStandardWindowDecorationView.m:139-167. X11's _checkStyle: uses style & 15 XGServerWindow.m:794 — low four bits only.
Textured / HUD / FullSizeContentView / NonactivatingPanelparsedRead from XIB only GSXib5KeyedUnarchiver.m:1124-1132; no other consumer.
-setStyleMask:absentZero hits in libs-gui. No public API to change style after creation. The backend machinery exists XGServerWindow.m:2374 but nothing ever calls it.
-setLevel: and all ten constantsyesReal EWMH mapping XGServerWindow.m:3419-3577 — MainMenu→DOCK, Floating→UTILITY. But _NET_WM_STATE_ABOVE is never set, so always-on-top relies on the WM inferring it from UTILITY, which KWin and Mutter generally do not.
-setOpaque:stubLiterally { /* FIXME */ _f.is_opaque = isOpaque; } NSWindow.m:1426-1429. No backend API behind it.
Per-pixel alpha / clearColor backgroundnoWindow creation always uses the shared screen visual XGServerWindow.m:1863-1935. No per-window 32-bit ARGB visual selection.
-setAlphaValue:yesVia _NET_WM_WINDOW_OPACITY; needs a compositor; whole-window only.
Shaped windowsno public APIXShapeCombineMask exists XWindowBuffer.m:587 but is private, called only from drag and slide views. Hard-edged only.
-setHasShadow:inertSets a non-standard atom read by no known compositor. Carries // FIXME: What size?
-setIgnoresMouseEvents:yesGenuine click-through via XFixesSetWindowShapeRegion XGServerWindow.m:5199-5232, gated on HAVE_XFIXES.
-addChildWindow:ordered:partial_children is maintained NSWindow.m:5507-5540 but read only by -childWindows. Children do not follow the parent.
Tracking rects, cursor rects, custom cursorsyesReal X11 delivery, builds a genuine XcursorImage XGServerWindow.m:4340.

Window-manager dependency is generic rather than WindowMaker-specific — _checkWindowManager XGServerWindow.m:1186-1234 probes three different supporting-WM properties. But per-WM workarounds persist, e.g. :2421: "Without this, iceWM does not let you move the window!"

Wayland: do not plan on it. stylewindow:, setalpha:, setShadow:, restrictWindow:toImage: and shaped windows are all empty stubs WaylandServer.m:602-605, :917-924, :319-322. Levels are consulted once at surface creation via zwlr_layer_shell — wlroots only, unsupported by Mutter and KWin. Only click-through is real.

Backends

libs-back/Source/: art, cairo, fontconfig, gsc, headless, opal, wayland, win32, winlib, x11, xdps. Server ∈ {x11, win32, wayland}; graphics ∈ {cairo (default), art, xlib, opal, headless}. art is not removed — still the fallback when cairo or freetype are missing, still CI-tested, and still being fixed: issue #176, "read back flipped views in the art backend," merged 2026-07-26/27. NSDPSContext exists only under xdps, which requires the dead X11 DPS extension and is unbuildable today — so MacViewClass.m:920 will not compile as written.

libs-opal has OPColorSpaceIndexed.m but it was last touched in 2010, and Opal's backend was non-functional for years until libs-back PR #77 landed 2026-04-30.

NX* to NS* C functions

Mostly clean: NXBeep, NXRunAlertPanel, NXRectFill, NXRectClip, NXRectClipList, NXHighlightRect, NXEraseRect, NXIntersectionRect and NXCopyBits all have direct equivalents. Gone: NXSetRect (use NSMakeRect), and NXConvertRGBToColor/ColorToRGB, since the packed NXColor integer died at the OpenStep transition.

One behavioural gap worth knowing: NXPing has no equivalent. flushGraphics pushes buffered operations but does not block for a server round-trip, and PSWait() is an explicit no-op. Anything depending on NXPing's synchronous semantics is an architectural rewrite, not a substitution.

08 — Interface files

Gorm cannot open these. Not "with effort" — at all.

All fourteen nib entries are NeXT typedstream version 4, the pre-keyed-archiving format, verified with file(1). English.lproj/Executor.nib/data.nib is typedstream, big endian, v4, system 930; the OpenStep variant's objects.nib is typedstream, little endian, v4, system 1000. The data.classes files are readable ASCII property lists; the archives are not.

The GNUstep wiki is explicit: NIB compatibility covers 10.2 and later; older typed-stream nibs must be converted first. Newer nibs contain keyedobjects.nib; ours contain data.nib and objects.nib. Runtime loading is equally blocked — GSModelLoaderFactory.m dispatches by signature to Gorm, keyed-nib, XIB and gmodel loaders, and there is no typedstream path anywhere. So [NSBundle loadNibFile:] MacAppClass.m:264 fails at runtime, and [NXApp loadNibSection:] Executor_main.m:65 does not compile.

ToolStatusVerdict
nib2gmodeldeadLast commit 2008-04-22. Self-documents as "does NOT compile on Linux, FreeBSD, or any other OS that doesn't have Apple or NeXT libraries installed."
nib2xibaliveCommits through 2025-06-29 and does target typedstream — but "runs only on OPENSTEP 4.2 currently" and is self-labelled experimental.
Gormhealthy1.5.0 released 2025-02-15, commits through 2026-07-18. The gate is the format, not the tool.

The only import path is: stand up OPENSTEP 4.2 on real hardware or the Previous emulator, run experimental nib2xib, then Gorm. Multi-day archaeology, approximate fidelity, untested against big-endian archives.

How little is actually in them

Extracted via strings plus the readable data.classes. The historical high-water mark (26 KB) held a main menu and eight windows: the game window, splash screen, preferences, a "Death Certificate" crash panel, a bug-report mailer, two registration windows and a serial-number window. The shipped nib is 7 KB and the preferences and splash strings are already absent from it.

The code has rotted away from the nibs badly:

  • MacViewClass declares no instance variables at all@interface MacViewClass:NSView { } MacViewClass.h:326, empty braces. All seven outlets its metadata declares are stale.
  • Four of seventeen declared actions are implementedpause: :2526, abort: :2573, printGame: MacAppClass.m:243, showInfo: :257. The other thirteen are dead.
  • SoundGenerator declares eight actions and ten outlets and has no source file anywhere in the tree.
  • Executor.project.nib is untouched Project Builder boilerplate — its strings are "My Window" and "UNTITLED". One data.nib is a 76-byte broken build artifact with a 0-byte data.classes.

There is zero menu construction in code — no NSMenu, addItem: or setMainMenu: anywhere. Structure is 100% nib-driven, and MacAppClass.m:118-150 does nothing but copy roughly 25 outlets into global_* C variables so the plain-C core can reach them.

Live UI surface today: one menu, one window plus framebuffer view, one info panel, four actions.

Recommendation: build it in code

Import is impossible at reasonable cost, and there is almost nothing to import. The extra panels are ARDI-era licensing UI whose backing code is gone — conversion would faithfully reproduce the rot. The nibs are more valuable read via strings as a design reference, which is already done above. And nibs would not help with the hard part regardless: the framebuffer view's draw path and event routing are not expressible in one.

Concretely: a -buildMainMenu of roughly 60 lines, programmatic window and view creation in -applicationDidFinishLaunching:, and deletion of the entire outlet layer — the ~25 global_* copies become direct members. Keep the old English.lproj in-tree, read-only, as reference.

One further argument: .gorm is a bundle of archived objects — undiffable, unmergeable, unreviewable. This project is currently living the thirty-year cost of that choice, since nobody can read these nibs today without strings.

09 — Rootless, in depth

The repository's own documentation is wrong

README.md:18 advertises "Rootless — emulated windows are part of your desktop." docs/subsystems/window-dialog-menu.md:27-30 states that when the rootless flag is true, "windows are not drawn onto the emulator framebuffer" and are instead "delegated to the host compositor." docs/subsystems/video-driver.md:49-51 repeats it. Both statements are contradicted by the code.

windRootless.cpp:14-46 unions every visible window's structure region, plus the menu bar, plus any open menus, into one region and makes one call:

RgnHandle rgn = NewRgn();
SetRectRgn(rgn, 0,0, qdGlobals().screenBits.bounds.right, LM(MBarHeight));
for(WindowPeek wp = LM(WindowList); wp; wp = WINDOW_NEXT_WINDOW(wp))
    if(WINDOW_VISIBLE(wp))
        UnionRgn(rgn, WINDOW_STRUCT_REGION(wp), rgn);
vdriver->setRootlessRegion(rgn);

Qt consumes it as a shape mask on a single fullscreen window qt.cpp:244-256window->setMask(qtRgn) on the one window created at :216 and maximised at :217. Wayland does the same via set_input_region wayland.cpp:452. Windows are still drawn into the framebuffer; updateBuffer reads it in both modes and the region only selects which spans are copied versus left transparent vdriver.cpp:332.

The four load-bearing levels

1 · Every port shares one bitmap

qGrafport.cpp:91 — every port opened, including each window's:

PORT_BITS(p) = qdGlobals().screenBits;

and screenBits.baseAddr is the main GDevice's PixMap base :33, which is the vdriver framebuffer qGDevice.cpp:77. Windows draw at absolute screen coordinates, clipped by visRgn. No independent backing surface. This is faithful — the real Macintosh worked this way.

2 · The framebuffer is in the guest's address space

qGDevice.cpp:76 calls SetupVideoMemoryMapping(...); mman.cpp:383-390 installs it in the guest offset table; qGrafport.cpp:59, 72 publish LM(ScrnBase) and LM(ScreenRow). Guest 68k code can and does write directly to screen memory.

Not theoretical — autorefresh.cpp:12-19 exists solely to cope with it:

"This file provides a mechanism to detect when applications are bypassing QuickDraw and writing directly to screen memory. It works by partitioning the screen into NUM_AUTOREFRESH_STRIPS horizontal strips… Each strip is periodically checksummed."

Per-window surfaces are fundamentally incompatible with this. An app poking ScrnBase writes to screen coordinates, not to any window's surface.

3 · Window chrome can be guest code

Frames come from the Window Definition Procedure, invoked as WINDCALL(w, wDraw, 0) windDisplay.cpp:127. Built-in WDEFs blit embedded bitmaps windDocdef.cpp:21-25. But ROMlib_windcall windMisc.cpp:511-533 does LoadResource(defproc)the WDEF is a resource, which can come from the application's own resource fork and execute as guest 68k code. No host titlebar can reproduce arbitrary custom chrome.

4 · The guest manages its own windows

C_FindWindow windMouse.cpp:27-60 walks Executor's LM(WindowList) in Z-order and hit-tests in global coordinates. DragTheRgn windMisc.cpp:120-215 implements dragging by XOR-ing a grey outline into the shared framebuffer and polling to mouse-up. Occlusion is computed by Executor via CalcVis/CalcVisBehind/PaintBehind windMisc.cpp:369-434, called from about ten sites. Hand stacking to the host WM and its z-order can disagree with visRgn — which is what QuickDraw clips against.

ARDI already answered this

They wrote the NeXTSTEP front end with full AppKit available and complete control of their own source, and used one window. Every other outlet in MacAppClass.h:16-43 is Executor's own chrome, not a Mac window. docs/outdated/wishlist:20 — "Give some thought to rootless windows during the code restructuring" — is as far as it got.

What per-window would actually require

  • Redirect every window's PORT_BITS to a per-window offscreen buffer — the GWorld machinery exists qGWorld.cpp.
  • Rewrite CalcVis/PaintBehind to stop computing occlusion.
  • Suppress or reimplement the WDEF, synthesising Mac part-codes from host events.
  • Intercept DragWindow/GrowWindow/ZoomWindow, defer to the host WM, and reflect results back into WindowList.
  • Accept that any app writing to ScrnBase breaks — precisely the population autorefresh.cpp exists to serve.
  • Plus the five missing GNUstep window features from §7.

Multi-month change to the most compatibility-sensitive subsystem, with a guaranteed tail of app-specific breakage. Not a front-end feature, and emphatically not free.

10 — HFS_XFer

9,194 lines that look like exactly what a Mac-oriented desktop needs, and are not.

HFS_XFer v2.2, © 1991–92 ARDI. It is not a NeXTSTEP application. It is a classic Mac Toolbox program written in C against ROMlib and linked as a NeXTSTEP binary. The only Objective-C is HFS_XFer_main.m — fourteen lines of boilerplate instantiating Executor's own NSApplication subclass. The UI is MenuMgr, DialogMgr, StandardFile and TextEdit.

Operations, from file_funcs[] HFS_XFer.c:20: Copy Disk, Move Files, Copy Files, Rename, Delete, New Folder, Quit. The transfer is HFS to HFS, using Mac File Manager calls on both sides — no host-filesystem path, no AppleDouble, MacBinary or BinHex conversion. Host access happens only because ROMlib maps a Unix directory into a fake Mac volume. The Info panel concedes: "No System 7 floppies / No fancy file conversions."

The HFS engine

HFS_XFerLinesMainlineLines
btree.c1,990src/hfsBtree.c2,515
file.c1,193src/hfsFile.c1,655
volume.c685src/hfsVolume.c1,066
helper.c332src/hfsHelper.c809
xbar.c267src/hfsXbar.c1,032

All of it is dead code, and a superseded fork besides

Every one of those .c files is wrapped, first line to last, in #if defined(OUTDATEDCODE). xbar.c, the dispatcher, has all twenty branches under the same guard and degrades to unconditional forwarding to the real File Manager. The directory is referenced nowhere in the build system.

The maintained descendants are in mainline and are in the build. Decisively: grep -c 'CW(\|CL(' returns 0 across HFS_XFer's btree.c, volume.c, file.c and hier.c, against 168 in mainline hfsBtree.c and 68 in hfsVolume.c. ARDI did the endian work later, in the other copy. This one works only on a big-endian machine.

Limits, even setting endianness aside: 512-byte physical blocks hardcoded; allocation block numbers are unsigned short throughout, giving the classic 65,535-block ceiling; byte offsets are signed long, so 2 GB on 32-bit. And in practice far tighter — helper.c backs the entire volume with static char buf[NBIGBLOCKS * BIGBLOCK], 144 × 20 KB = 2.88 MB, with TransPhysBlk doing memcpy into it. It only ever worked on floppies.

Defects found in passing

  1. Inverted bounds checkhelper.c:238 reads if (firstbigblock >= 0 || lastbigblock < NBIGBLOCKS). That must be &&. As written, an out-of-range block passes the guard and the following memcpy runs past the static array.
  2. Uninitialised readstransferer.c:397-400 reads hpb and vrn on a path where neither is populated.
  3. Unbounded sprintfHFS_XFer.util.c:135, 168 formats from argv into fixed 50- and 100-byte buffers.

The %-prefixed files

Not backups and not editor artifacts — AppleDouble sidecars, the convention A/UX and ARDI's tools used to store Mac forks on a Unix filesystem. file(1) confirms every one. Most are the 198-byte no-resource-fork case carrying only type and creator. Two carry real payload: %HFS_XFer.%B9 is 8,910 bytes, type PROJ creator KAHL — a THINK C project file, where %B9 is hex-escaped 0xB9, which is π in MacRoman, so the file is HFS_XFer.π. And %HFS_XFer_DA is 23,387 bytes, type DFIL creator DMOV — a Font/DA Mover suitcase holding the compiled Desk Accessory. Their paired data forks are 0 bytes.

The Desk Accessory

A genuine 68k DRVR code resource, not a NeXTSTEP component. Entry point is int main(cntrlParam *pb, DCtlPtr dctlp, int n) dispatching on open/control/status/close HFS_XFer_DA/HFS_XFer.c:670, with SetUpA4.h and RememberA4()/SetUpA4() bracketing every callback — the THINK C idiom for a code resource with no A5 world. Its context in practice is Executor itself: as a DA it appears under the Apple menu of whatever Mac application is running, so files could be moved without quitting. Functionally identical to the main app — xbar2.h is seventeen one-line macros hard-wiring every call to the real trap.

Verdict: ignore it. Don't port it, don't mine it.

Nothing to port — the presentation layer is 100% Mac Toolbox. Nothing to mine — the HFS engine is #if 0'd dead code superseded by an endian-corrected mainline copy. And even the good version loses to hfsutils and hfsfuse, which are endian-clean, 64-bit clean, maintained, handle HFS+ and HFSX, handle volumes over 2 GB and non-512-byte sectors, and expose forks properly.

Estimated cost of the alternatives, for the record: porting the UX to GNUstep is 2–4 weeks and takes roughly zero lines from here — a rewrite, not a port. Mining the HFS code is 4–8 weeks minimum and yields something strictly worse than hfsfuse. Licensing is not the obstacle; there is simply no technical benefit.

Two things worth ten minutes each

transferer.c's copy1file/CopyFork, about 120 lines, is a compact and correct specification of what "copy a Mac file faithfully" means: PBHCreate, copy data fork, copy resource fork via PBHOpenRF, then PBSetCatInfo last to restore Finder info and dates — plus preallocate-then-SetEOF, skip invisibles, and refuse to copy a directory into its own descendant.

HFS_XFer.util.c is a media-recognition hook: probe block 2 for the 'BD' HFS signature, else look for an Apple partition map, then hand the device to the running GUI over a Unix socket. The Linux equivalent is roughly forty lines of udev rule plus unar and hfsfuse. Read it for the design.

11 — Salvage register

Read closely

RegionWhy
The core bridge MacViewClass.m:396-537, :612-712, :340-393Roughly 250 lines defining the whole architecture: dual framebuffer, aliasing rep, lockFocus outside drawRect:, multi-rect clip, Y-flip, allocateGState. Port the shape, retype every line.
- step :1059-1112Run guest until the host has an event pending, then yield. Survives any redesign.
vdriver_flush_display :906-923Read the comment. It documents a real failure mode where skipping the flush queues messages until the machine pages itself to death.
host_flush_shadow_screen :2233-2259The polled-refresh design for apps that bypass QuickDraw. Still needed.
setcursorX :994-1035The XOR-cursor dithering hack — inverted regions rendered as a per-row alternating checkerboard — and disableCursorRects. You will hit both.
Scrap handling :1875-2193 + NEXT.c:720-760Multi-flavour ordering, staleness counters, CR↔LF, and the synthetic RTF font table. Hard-won; better than anything in the modern tree.
Focus delegates MacAppClass.m:164-236Host key-window focus to Mac suspend/resume, with the change-count gate deciding clipboard conversion. The right model.
performKeyEquivalent: :2486-2505Swallow ⌘-equivalents while a guest app runs. Small, essential, easy to forget.
NEXT.c:661-672Two paragraphs proposing the correct keyboard architecture — import host keymaps at startup with optional guest KMAP/KCHR override. Worth more than the code around it.

Archaeology

RegionVerdict
OldMacViewClass.m, 2,907 linesSuperseded 1995 snapshot, not in the build. Its one unique idea — writing straight into NeXT video RAM via a setuid-root kernel module — is dead behind if (0 && …) at :1041, :2166, and the callee has no implementation anywhere. Delete.
blockinterrupts.m#error'd by its author at line 76. Not in the build.
contextswitch NEXTmain.m:110-168, :288-344Inline-asm coroutines with hand-built stacks. Modern Executor uses a real thread.
ROMlib_load_ardi_mods NEXTmain.m:183-259Loads a setuid-root Mach kernel module and probes for it by deliberately triggering SIGILL :256.
Printing MacViewClass.m:2689-2892Raw PostScript into a DPS context, rectForPage: spinning contextswitch with a 10,000-iteration timeout existing for one Excel bug :1038-1057, and endPageSetup emitting a deliberately false %%BeginDocument: to work around Word 5 :2806-2810.
~80% of MacAppClass.mRegistration keys, serial numbers, licence enforcement, ARDI's phone number in five separate string constants.
MacViewClass.h:19-315~300 lines of Mac Roman ↔ NeXT encoding defines. The problem is still real for the clipboard; this table is not the answer.
MacWinClass, MacPrintClass.h28 lines total, plus a header for a class with no implementation. Under GNUstep you likely need no NSWindow subclass at all.
Mach-O linker magic nextstep.make.*:6-19-sectcreate, -segaddr, __ICON, lowseg. No ELF equivalent — rewrite. Icons become bundle resources; the fixed-address segment becomes the mmap(MAP_FIXED) scheme the x11 and sdl front ends already use.
NEXTkeyboard.mNeXT event-status-driver ADB probing. Not #ifdef-guarded — always compiled. Delete.
12 — Unknowns

Stated plainly, because a review that hides its gaps is worse than one that names them.

Four libraries were not read, and should be

Scope here was Executor's two trees plus libs-gui and libs-back. These were not opened, and two of them bear directly on the most expensive conclusions in the plan:

  • libs-opal — Core Graphics. Could offer a faster path to the screen than NSBitmapImageRep, which is the single largest unquantified risk (§7). Known caveats: OPColorSpaceIndexed.m last touched 2010; the Opal backend was non-functional until libs-back PR #77 landed 2026-04-30.
  • libs-quartzcore — CoreAnimation. A layer-based approach is a third option that neither Qt nor Wayland considers, and it bears on the rootless ceiling described in §9. It would still face the guest-writes-to-screen-memory constraint, but the question should not be closed without looking.
  • NSSound in libs-gui — read as it stands today (§6), where it cannot serve the hunger model. Work is reportedly in progress; if a streaming sink lands, that verdict changes and the AppKit-native path becomes the preferred one.
  • libs-corebase — CoreFoundation. Bears on how much of the bridge must be Objective-C++ at all, and therefore on exposure to the runtime and exception traps in §4.

The Gershwin desktop sources are likewise unread, and answer a different question: what the desktop already provides, so the front end integrates rather than duplicates.

  • Nothing was compiled or executed. No GNUstep toolchain, no clang, no Gorm on the review machine.
  • The literal output of gnustep-config --objc-flags and --gui-libs on a current install. All flags given are derived from gnustep-make's own makefiles, not observed.
  • Whether distribution -dev packages install libs-back/Headers/x11/XGServerWindow.h, which gates the raw-Xlib blit escape hatch.
  • Any GNUstep drawing benchmark newer than 2015.
  • Whether recent binutils fixed the GNU ld / v2-ABI problem, or whether lld remains strictly necessary. gnustep-make's own check admits it has no accurate test and only warns.
  • Current first-hand bug reports for window levels under KWin, Mutter or i3 — the _NET_WM_STATE_ABOVE gap is inferred from source only.
  • Whether [NSGraphicsContext graphicsPort] is populated with a live cairo context on the on-screen path.
  • Gershwin's own build conventions — the project was not located from the review environment.
  • CMake's issue tracker for prior OBJCXX-on-Linux reports; automated fetching is blocked, so "not checked" rather than "nothing there."