Companion to the porting plan · 27 July 2026
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.
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.
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.
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.
| Signature | Line | Constraint |
|---|---|---|
void runEventLoop() | :111 | Called on the main thread from main(). Must block until endEventLoop(). |
void endEventLoop() | :112 | Called from the emulator thread main.cpp:503. Must be async and thread-safe. |
bool setMode(int w, int h, int bpp, bool grayscale) | :117 | Emulator thread. Allocates framebuffer_, returns true. |
void requestUpdate() (protected) | :168 | Invoked with mutex_ already held. Must not block. |
(IEventListener*, int& argc, char* argv[]) — argc by
non-const reference. Required by main.cpp:416.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.
| Method | Decl | Caller | Qt overrides? |
|---|---|---|---|
updateScreen(t,l,b,r) | :114 | dirtyrect.cpp:216 | no |
isAcceptableMode(…) | :115 | qGDevice.cpp:396 | no (SDL2 does) |
setColors(int, const vdriver_color_t*) | :116 | qGDevice.cpp:234 | no |
putScrap / getScrap / weOwnScrap | :119-121 | scrap.cpp:150, :260 | no — Qt has no clipboard |
setTitle(const std::string&) | :123 | prefs.cpp:89 | no |
setCursor / setCursorVisible | :125-128 | qCursor.cpp | yes qt.cpp:301, :326 |
setRootlessRegion(RgnHandle) | :131 | windRootless.cpp:42 | base + commitRootlessRegion |
beepAtUser() | :134 | osutil.cpp:849 | no |
noteUpdatesDone / updateMode / handleMenuBarDrag | :136-139 | toolevent.cpp, menu.cpp:1068 | no |
The true minimum is proven by headless.h — sixteen lines implementing setMode and
three empty methods.
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
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.
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.
| Front end | Mechanism |
|---|---|
| Qt | QMetaObject::invokeMethod queued, plus a hand-rolled mutex/condvar when it must block qt.cpp:179, 193-235, 298 |
| SDL2 | onMainThread() pushes to todos_, wakes with a custom event, blocks on done_ sdl2.cpp:45-52, 219-226 |
| Wayland | eventfd + poll wayland.cpp:60, 186-200 |
| GNUstep | -performSelectorOnMainThread:withObject:waitUntilDone: |
requestUpdate() is invoked with mutex_ held at every site:
setColorssetRootlessRegionupdateScreenAnd 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.
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.
Executor::Framebuffer vdriver.h:43-60, constructed at
vdriver.cpp:12-17:
rowBytes = ((width * bpp + 31) & ~31) / 8; // rows padded to 4-byte multiples
| Depth | Layout |
|---|---|
| 1, 2, 4, 8 | Palette-indexed, MSB-first within each byte — IndexedPixelGetter vdriver.cpp:242-269 computes shift = 8 - (x*depth%8) - depth, decrementing. |
| 16 | Mac RGB555, big-endian, read through GUEST<uint16_t> vdriver.cpp:364, unpacked with 5→8 bit expansion :366-375. |
| 32 | Mac 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.
updateBuffer() producesvdriver.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.
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.
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.
set(FRONT_ENDS qt x sdl sdl2 wayland CACHE STRING …). A cache list; override with -DFRONT_ENDS="qt;gnustep".executor-<name>, all from the same main.cpp; front-end-headless links into every one.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.
| Item | Value | Evidence |
|---|---|---|
| Runtime | libobjc2 v2.3 | Released 2025-09-16; master active. MIT. |
| ABI flag | gnustep-2.2 | gnustep-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+. |
| Compiler | clang only | GCC 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 combo | ng-gnu-gnu | libobjc2 + gnustep-base + gnustep-gui. |
| Linker | lld | gnustep-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++ stdlib | libstdc++ | 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
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.
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.
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.
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.
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.
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 authorLine 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.
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.
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.
validRequestorForSendType:(NSString), taking NSString by value. Cannot compile.-readSelectionFromPasteboard: declared void where the protocol wants BOOL.#warnings.Expect a first-compile bug tail beyond the catalogued API gaps.
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.
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:
| Table | Indexed by | Size |
|---|---|---|
x/x_keycodes.cpp:11-148 | raw X11 keycode | 136 |
x/x_keysym.cpp | X11 keysym | 2 pages |
sdl2/keycode_map.cpp:15-133 | SDL2 keycode | ~110 |
qt/qtkeycodes.cpp:7-122 | Qt::Key | ~100 |
win32/vk_to_mkv.h | Win32 VK | 265 |
osevent/ibm_keycodes.cpp | PC scancode set 1 | — |
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.
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.
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.
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
GSSoundSource→GSSoundSink 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.
Port the existing 171-line SDL1 driver. Mechanical:
SDL_OpenAudio→SDL_OpenAudioDevice. No new project dependency.
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+.
NSSoundWould 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.
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.
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.
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 end | Clipboard support |
|---|---|
| 1995 NeXTSTEP | Five flavours, charset conversion, RTF font tables |
x | TEXT 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, headless | Nothing. 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.
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.
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.
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."
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.
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 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.
| API | gui | Reality |
|---|---|---|
| Borderless / Titled / Closable / Miniaturizable / Resizable | yes | The only masks with effect GSStandardWindowDecorationView.m:139-167. X11's _checkStyle: uses style & 15 XGServerWindow.m:794 — low four bits only. |
| Textured / HUD / FullSizeContentView / NonactivatingPanel | parsed | Read from XIB only GSXib5KeyedUnarchiver.m:1124-1132; no other consumer. |
-setStyleMask: | absent | Zero 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 constants | yes | Real 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: | stub | Literally { /* FIXME */ _f.is_opaque = isOpaque; } NSWindow.m:1426-1429. No backend API behind it. |
Per-pixel alpha / clearColor background | no | Window creation always uses the shared screen visual XGServerWindow.m:1863-1935. No per-window 32-bit ARGB visual selection. |
-setAlphaValue: | yes | Via _NET_WM_WINDOW_OPACITY; needs a compositor; whole-window only. |
| Shaped windows | no public API | XShapeCombineMask exists XWindowBuffer.m:587 but is private, called only from drag and slide views. Hard-edged only. |
-setHasShadow: | inert | Sets a non-standard atom read by no known compositor. Carries // FIXME: What size? |
-setIgnoresMouseEvents: | yes | Genuine 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 cursors | yes | Real 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.
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.
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.
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.
| Tool | Status | Verdict |
|---|---|---|
nib2gmodel | dead | Last 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." |
nib2xib | alive | Commits through 2025-06-29 and does target typedstream — but "runs only on OPENSTEP 4.2 currently" and is self-labelled experimental. |
| Gorm | healthy | 1.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.
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.pause:
: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.
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.
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-256 —
window->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.
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.
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.
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.
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.
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.
PORT_BITS to a per-window offscreen buffer — the GWorld machinery
exists qGWorld.cpp.CalcVis/PaintBehind to stop computing occlusion.DragWindow/GrowWindow/ZoomWindow, defer to the host WM, and
reflect results back into WindowList.ScrnBase breaks — precisely the population
autorefresh.cpp exists to serve.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.
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."
| HFS_XFer | Lines | Mainline | Lines |
|---|---|---|---|
btree.c | 1,990 | src/hfsBtree.c | 2,515 |
file.c | 1,193 | src/hfsFile.c | 1,655 |
volume.c | 685 | src/hfsVolume.c | 1,066 |
helper.c | 332 | src/hfsHelper.c | 809 |
xbar.c | 267 | src/hfsXbar.c | 1,032 |
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.
helper.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.transferer.c:397-400 reads hpb and
vrn on a path where neither is populated.sprintf — HFS_XFer.util.c:135, 168 formats from
argv into fixed 50- and 100-byte buffers.%-prefixed filesNot 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.
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.
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.
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.
| Region | Why |
|---|---|
| The core bridge MacViewClass.m:396-537, :612-712, :340-393 | Roughly 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-1112 | Run guest until the host has an event pending, then yield. Survives any redesign. |
vdriver_flush_display :906-923 | Read 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-2259 | The polled-refresh design for apps that bypass QuickDraw. Still needed. |
setcursorX :994-1035 | The 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-760 | Multi-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-236 | Host key-window focus to Mac suspend/resume, with the change-count gate deciding clipboard conversion. The right model. |
performKeyEquivalent: :2486-2505 | Swallow ⌘-equivalents while a guest app runs. Small, essential, easy to forget. |
| NEXT.c:661-672 | Two paragraphs proposing the correct keyboard architecture — import host keymaps at startup with optional guest KMAP/KCHR override. Worth more than the code around it. |
| Region | Verdict |
|---|---|
OldMacViewClass.m, 2,907 lines | Superseded 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-344 | Inline-asm coroutines with hand-built stacks. Modern Executor uses a real thread. |
ROMlib_load_ardi_mods NEXTmain.m:183-259 | Loads a setuid-root Mach kernel module and probes for it by deliberately triggering SIGILL :256. |
| Printing MacViewClass.m:2689-2892 | Raw 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.m | Registration 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.h | 28 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.m | NeXT event-status-driver ADB probing. Not #ifdef-guarded — always compiled. Delete. |
Stated plainly, because a review that hides its gaps is worse than one that names them.
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.
gnustep-config --objc-flags and --gui-libs on a current
install. All flags given are derived from gnustep-make's own makefiles, not observed.-dev packages install libs-back/Headers/x11/XGServerWindow.h,
which gates the raw-Xlib blit escape hatch._NET_WM_STATE_ABOVE gap is inferred from source only.[NSGraphicsContext graphicsPort] is populated with a live cairo context on the on-screen
path.