AppKit scaffold — findings
What it takes to go from "process starts" to "a CPU-rendered gradient is on screen, surviving a resize storm, exiting cleanly" on macOS/AppKit — the minimal window example evolved into the instrumented scaffold the F01 first-pixel and F02 resize demos build on. The program is ./examples/scaffold/app.d (with instrument.d, the stderr event logger every other demo copies).
Last reviewed: June 10, 2026
All run findings below are A[ssh]: built and executed on mac-bsn (aarch64-darwin, macOS 26.3.1, LDC 1.41.0 / DMD v2.111.0) over SSH with the console session locked — the WindowServer accepts and registers windows in this state, but does not composite them (see the sidecar evidence).
| Measurement | Value |
|---|---|
| Concepts to first pixel | 11 distinct platform object/handle types (see table) |
LOC (app.d, excl. instrument.d) | 391 |
init_start → first_pixel_presented | 123.2 ms A[ssh] (cold AppKit + WindowServer connection) |
| First geometry callback ("first configure") | first setFrameSize:, synchronous inside setContentView: |
| Resize → redraw latency in the storm | ~0.7 ms to setFrameSize:, ~4 ms to drawRect: return |
| Exit | clean 0 via [NSApp stop:nil] + synthetic event post |
What the scaffold adds over the example
./example/app.d proves the irreducible window-open sequence (sharedApplication → initWithContentRect:… → makeKeyAndOrderFront:) and then deliberately returns without running the loop. The scaffold, ./examples/scaffold/app.d, turns that into a real (still nib-less, code-only) app:
- a custom
NSViewsubclass defined in D (GradientView) whosedrawRect:blits a CPU-rendered 32-bit gradient viaCGDataProviderCreateWithData→CGImageCreate→CGContextDrawImageinto the view's currentCGContext; [NSApp run]as the loop, with a ~16 ms repeatingNSTimerdrivingsetNeedsDisplay:(and the bounded-run schedule);- a programmatic
setFrame:display:resize storm (six content-size changes, per F02) with per-resize buffer reallocation; - clean termination both ways:
WSI_AUTO_EXIT=1→stop:plus a syntheticNSEventTypeApplicationDefinedpost (without whichrunkeeps blocking —stop:is only checked after an event dispatch); interactive close →windowWillClose:→terminate:; - full F01-style instrumentation (
<monotonic_us> <DEMO> <EVENT_KIND> k=v…on stderr, viainstrument.d).
The example is 117 LOC; the scaffold is 391 — the delta (~270 LOC) is the price of a real draw path, a subclass, a loop, and instrumentation on AppKit.
Concepts to first pixel
Distinct platform object/handle types touched before first_pixel_presented (the F01 metric): 11.
| # | Handle type | Touched by |
|---|---|---|
| 1 | NSApplication | sharedApplication, setActivationPolicy: (Regular) |
| 2 | NSString | setTitle: ("wsi-scaffold") |
| 3 | NSWindow | initWithContentRect:styleMask:backing:defer: |
| 4 | NSView (subclass) | GradientView — initWithFrame:, setContentView: |
| 5 | SEL | sel_registerName("tick:") for the timer target |
| 6 | NSTimer | scheduledTimerWithTimeInterval:… (~16 ms, repeating) |
| 7 | NSGraphicsContext | currentContext inside drawRect: |
| 8 | CGContextRef | the view's backing Quartz context |
| 9 | CGColorSpaceRef | CGColorSpaceCreateDeviceRGB |
| 10 | CGDataProviderRef | wraps the malloc'd pixel buffer |
| 11 | CGImageRef | CGImageCreate → CGContextDrawImage |
(Counting the autorelease-pool token and the CGDirectDisplayID returned by the CGMainDisplayID() headless guard would make 13; they are bookkeeping, not part of the draw path.)
Init step sequence and timing A[ssh]
The WSI_AUTO_EXIT=1 run, verbatim (timestamps are µs since init_start):
0 APPKIT init_start auto_exit=1 hold=0
136 APPKIT step name=CGMainDisplayID
18961 APPKIT step name=NSApplication_sharedApplication
41767 APPKIT step name=setActivationPolicy policy=regular
43281 APPKIT step name=NSWindow_initWithContentRect size=480x320
79874 APPKIT window_created scale=2.0
79897 APPKIT step name=GradientView_initWithFrame
79917 APPKIT step name=setContentView
79983 APPKIT first_configure size=480x320 scale=2.0
80097 APPKIT step name=makeKeyAndOrderFront
95012 APPKIT step name=NSTimer_scheduledTimerWithTimeInterval interval_ms=16
95034 APPKIT step name=NSApp_runStep deltas: CGMainDisplayID ≈ 18.8 ms (the first call into CoreGraphics — it bootstraps the WindowServer connection, so the "headless guard" is itself the first round-trip); sharedApplication ≈ 22.8 ms; setActivationPolicy: ≈ 1.5 ms; initWithContentRect: ≈ 36.6 ms (window device creation — NSBackingStoreBuffered, defer:false); creating + installing the D-side view ≈ 0.2 ms; makeKeyAndOrderFront: ≈ 14.9 ms. Everything is synchronous — there is no Wayland-style configure/ack negotiation; the window exists, sized, the moment the initializer returns (backingScaleFactor already reads 2.0 there).
"First configure". AppKit has no configure event; the first geometry callback this scaffold could find is the content view's first setFrameSize:, which fires synchronously inside setContentView: (66 µs after the step, when the window sizes the view to fit its content area). That is what the scaffold logs as first_configure; every later setFrameSize: is logged as resize.
First draw A[ssh]
95034 APPKIT step name=NSApp_run
121183 APPKIT buffer_alloc size=960x640 bytes=2457600
123212 APPKIT first_pixel_presented t=1
123224 APPKIT frame_callback t=1makeKeyAndOrderFront:does not draw. NodrawRect:arrives until[NSApp run]starts pumping; the first one lands ~26 ms into the loop's first drawing pass. A program that never runs the loop (like the original example) never draws.- The buffer is allocated at 960×640 pixels for a 480×320-point view —
backingScaleFactoris 2.0, and the scaffold renders at backing resolution and lets Quartz's CTM map the points rect 1:1 onto pixels. first_pixel_presentedis the return of the firstdrawRect:(cumulative 123.2 ms frominit_start; render + blit of the 2.4 MB buffer is ~2 ms). Per F01's caveat: this is the strongest confirmation AppKit gives a software renderer — CoreAnimation may still defer actual compositing to the end-of-cycleCATransactioncommit, and with the screen locked nothing is composited at all.- Steady state: 120 timer ticks produced 106
frame_callbacks ≈ 15–20 ms apart — AppKit coalescessetNeedsDisplay:dirty marks into the run loop's drawing pass, so ticks ≠ draws.
Resize storm A[ssh]
Six programmatic setFrame:display:YES changes (frame computed with frameRectForContentRect:), one every 3 ticks. One cycle, verbatim:
607374 APPKIT step name=setFrame_display size=640x400
608069 APPKIT resize size=640x400 scale=2.0
608623 APPKIT buffer_alloc size=1280x800 bytes=4096000
612108 APPKIT frame_callback t=28The sequence is strictly synchronous and app-paced: setFrame:display:YES → setFrameSize: on the content view (~0.7 ms) → buffer realloc → drawRect: returning ~4 ms after the request — ahead of the next timer tick, i.e. the display:YES flag forces the redraw rather than waiting for the loop's normal pass. The window always has exactly the size the app asked for (the app picks the size; nothing is negotiated or denied), every resize logs scale=2.0, and all six reallocations (4 MB worst case at 1600×1040) completed with no artifacts, no mismatches, and a clean exit:
2051196 APPKIT step name=NSApp_stop tick=120
2051630 APPKIT loop_exit frames=106 ticks=120 resizes=6NOTE
A programmatic setFrame: does not enter AppKit's live-resize mode — viewWillStartLiveResize/…End never fire (they wrap interactive border-drags only, a Tier-C test per F02). The buffer strategy here is per-resize free+malloc; pooling is unnecessary at these rates.
The D-side NSView subclass: extern (Objective-C) worked, no fallback needed
The plan was to try a pure-D Objective-C subclass and fall back to the runtime C API (objc_allocateClassPair + class_addMethod) if the compiler couldn't emit one. The fallback was not needed: LDC compiles GradientView — a non-externextern (Objective-C) class GradientView : NSView — into real Objective-C class metadata that the runtime registers at load, and AppKit happily dispatches drawRect:, setFrameSize:, tick:, and windowWillClose: (delegate method — selector lookup, no protocol declaration needed) to the D method bodies. Defining Objective-C classes landed in DMD 2.085 and reached LDC in 1.40.0; the LDC 1.40.0 release notes state:
Objective-C: The compiler now properly supports Objective-C classes and protocols, as well as swift stub classes (via the
@swiftUDA). (#4777)
The recipe (see app.d), for the F-demo agents:
- Bodied methods are the subclass implementation; bodyless
@selectormethods bind to the inherited implementation.GradientViewdeclares bodylessstatic GradientView alloc()andGradientView initWithFrame(NSRect)(typed covariantly — declared on the leaf to avoid the covariant-return clash on a base declaration, same idiom as the example) and bodieddrawRect:/setFrameSize:/tick:/windowWillClose:. override+supercalls work.setFrameSize:is declared on the externNSViewdeclaration and overridden withoverride … { super.setFrameSize(s); … }— the compiler emits theobjc_msgSendSuperpath. Forgetting thesupercall leaves the view permanently unsized.- Methods AppKit only ever calls by selector (
drawRect:,tick:,windowWillClose:) need no base declaration at all — defining a method with the right@selectoris enough; Objective-C dispatch is by selector, so it shadowsNSView's implementation without D-leveloverride. - Do not redeclare
sel_registerNameasextern (C) void*— objective-d'sobjc.rtalready declares it returning itsSELstruct, and LDC rejects the duplicate with a mangled-name/IR-type mismatch error. Importobjc.rt : SELand useSEL.register("tick:"). - D-side state lives in
__gsharedmodule globals; instance variables in the Objective-C class were not needed for a single-window demo (and not exercised here).
WindowServer sidecar evidence A[ssh]
While a WSI_HOLD=1 run (no storm, ~10 s) held the window open, a Swift sidecar on the same machine queried CGWindowListCopyWindowInfo with .optionAll. Verbatim hit:
["kCGWindowSharingState": 0, "kCGWindowOwnerName": scaffold, "kCGWindowOwnerPID": 90674,
"kCGWindowLayer": 0, "kCGWindowStoreType": 1, "kCGWindowAlpha": 1,
"kCGWindowBounds": { Height = 315; Width = 433; X = 194; Y = 639; },
"kCGWindowMemoryUsage": 2368, "kCGWindowNumber": 27963]kCGWindowOwnerName = scaffoldwith the matching PID and a realkCGWindowNumberproves WindowServer registration from an SSH-launched, non-bundled binary.- The
kCGWindowIsOnscreenkey is absent (the sidecar had to use.optionAll, not.optionOnScreenOnly): the window is registered but not composited because the console session is locked. This is the expectedA[ssh]shape — on-screen verification of actual pixels is Tier C (logged-in console). kCGWindowBoundsis in the top-left-origin global display space (unlike AppKit's y-up screen coordinates) and reads 433×315 at (194, 639) — a uniform ~0.9 down-scale of the requested 480×348 frame (480×320 content + 28 pt titlebar). The window application-side geometry is exactly as requested (first_configure size=480x320 scale=2.0); the WindowServer-side bounds appear re-mapped to the lock-screen display configuration, so treat sidecar bounds as registration evidence only, not geometry ground truth, while the screen is locked.
Surprises
- The headless guard is the first round-trip.
CGMainDisplayID()took ~18.8 ms — it bootstraps the WindowServer connection beforesharedApplicationeven runs. "Check then connect" is really "connect twice". first_configureis synchronous insidesetContentView:(66 µs later) — AppKit sizes the content view viasetFrameSize:while the setter is still on the stack. There is no asynchronous configure phase anywhere on this platform.makeKeyAndOrderFront:paints nothing. The firstdrawRect:only arrives ~26 ms into[NSApp run]— ordering a window front and getting pixels into it are separated by the run loop's first drawing pass.- 120 ticks → 106 draws:
setNeedsDisplay:dirty marks are coalesced per run-loop pass, so anNSTimeris a refresh requester, not a frame clock (the real frame clock isCADisplayLink, out of scope for the scaffold). stop:without an event post hangs.-[NSApplication stop:]only sets a flag that the loop checks after dispatching an event; the scaffold must post a syntheticNSEventTypeApplicationDefinedevent to unblocknextEventMatchingMask:.- The locked-screen WindowServer rescales registered windows (~0.9× here) — sidecar bounds disagree with app-side geometry while no one is logged in.
- The D/Objective-C toolchain surprise was pleasant: a real Objective-C subclass,
override, andsupercalls all worked first try on LDC 1.41 — the only friction was thesel_registerNameredeclaration clash with objective-d's typedSEL.
Sources
- This scaffold —
./examples/scaffold/app.d,./examples/scaffold/instrument.d; the predecessor./example/app.d; the AppKit survey. - Feature specs — F01 first pixel, F02 resize.
- Apple Developer documentation (Wayback-pinned, bot-hostile host):
NSApplication,NSWindow,NSView,drawRect:,setFrameSize:,backingScaleFactor,stop:,NSTimer,CGImage,CGWindowListCopyWindowInfo. - D ↔ Objective-C — the D specification's Interfacing to Objective-C, the DMD 2.085.0 changelog (defining Objective-C classes), the LDC 1.40.0 release notes (quoted above), and the
objective-dpackage (objc.autorelease,objc.rt).