// macOS/AppKit F03 demo — modal-loop survival (../../../features/f03-modal-loop.md).
// On macOS the "modal loop" is a *run-loop mode switch*: during interactive
// live-resize / menu tracking AppKit runs the run loop in NSEventTrackingRunLoopMode
// instead of the default mode, so timers scheduled only in the default mode starve
// and any animation they drive freezes. The cure is scheduling the animation timer
// in NSRunLoopCommonModes, which AppKit's tracking/modal modes are members of.
//
// The demo animates a full-window ~2 Hz color cycle and races two identical
// ~100 ms NSTimers:
// timer=default scheduledTimerWithTimeInterval: (default run-loop mode only)
// timer=common timerWithTimeInterval: + addTimer:forMode:NSRunLoopCommonModes
// (this one drives setNeedsDisplay: -> the color cycle)
// Both log `tick timer=… n=… gap_ms=… phase=…` per fire.
//
// Because a programmatic setFrame: never enters live-resize (scaffold finding) and
// an interactive border-drag cannot be performed over SSH, the bounded run proves
// the *mechanism* headless: two one-shot common-modes timers each run the run loop
// nested (runMode:beforeDate:) in a non-default mode for ~2 s — first
// NSEventTrackingRunLoopMode (the very mode AppKit's live-resize/menu tracking
// loops run in), then NSModalPanelRunLoopMode. The default-mode timer starves for
// each whole phase; the common-modes timer keeps ticking — the same starvation an
// interactive drag causes. Per-timer max-gap statistics are reported per phase at
// exit.
//
// The nested phases are deliberately entered from their own one-shot timers, NOT
// from the animation timer's callback: a repeating NSTimer never fires
// re-entrantly while its own callout is on the stack, so a nested loop run from
// inside tickCommon: would starve the common-modes timer too (measured — see the
// findings doc).
//
// Live-resize instrumentation for the interactive Tier-C run:
// viewWillStartLiveResize/viewDidEndLiveResize log `modal_enter`/`modal_exit`
// (kind=live_resize), and every drawRect: logs inLiveResize.
// Findings: ../../f03-modal-loop.md; recipe and build command: ../../scaffold.md.
//
// Modes (environment variables):
// WSI_AUTO_EXIT=1 bounded run: default (~3 s) -> tracking (~2 s, nested
// NSEventTrackingRunLoopMode) -> resumed (~2 s) -> panel
// (~2 s, nested NSModalPanelRunLoopMode) -> resumed (~3 s) ->
// [NSApp stop:nil] (+ synthetic event post) -> exit 0.
// (none) interactive: no modal phase is injected; runs until the
// window closes so a human can border-drag and observe the
// default-mode timer freeze while the color cycle survives
// (Tier C; see the manual-run queue).
//
// Headless-safe: prints `SKIP:` and exits 0 when there is no window server
// (CGMainDisplayID() == 0), per the research-docs guidelines.
module (module) appapp;
import (package) corecore.(module) core.attributeThis module contains UDA's (User Defined Attributes) either used in
the runtime or special UDA's recognized by compiler.
Attribute Name, Linkage, Description
gnuAbiTag, C++,
Declares an ABI tag on a C++ symbol.
mustuse,,
Ensures that values of a struct or union type are not discarded.
optional, Objective-C,
Makes an Objective-C interface method optional.
selector, Objective-C,
Attaches an Objective-C selector to a method.
standalone,,
Marks a shared module constructor as not depending on any
other module constructor being run first.
weak,,
Specifies that a global symbol should be emitted with weak linkage.
Source
core/attribute.d
attribute : selector;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stdioD header file for C99 <stdio.h>
pubs.opengroup.org/onlinepubs/009695399/basedefs/stdio.h.html, stdio.h
Source
core/stdc/stdio.d
stdio : (alias) app.printf = int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stdlibD header file for C99.
pubs.opengroup.org/onlinepubs/009695399/basedefs/stdlib.h.html, stdlib.h
Source
core/stdc/stdlib.d
stdlib : (alias) app.getenv = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv, (alias) app.malloc = void* core.stdc.stdlib.malloc(ulong size) nothrow @nogcmalloc, (alias) app.free = void core.stdc.stdlib.free(void* ptr) nothrow @nogcfree;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(module) core.sys.posix.unistdD header file for POSIX.
unistd : (alias) app.usleep = int core.sys.posix.unistd.usleep(uint) nothrow @nogc @trustedusleep;
import (package) corecore.(module) core.timeModule containing core time functionality, such as Duration (which
represents a duration of time) or MonoTime (which represents a
timestamp of the system's monotonic clock).
Various functions take a string (or strings) to represent a unit of time
(e.g. convert!("days", "hours")(numDays)). The valid strings to use
with such functions are "years", "months", "weeks", "days", "hours",
"minutes", "seconds", "msecs" (milliseconds), "usecs" (microseconds),
"hnsecs" (hecto-nanoseconds - i.e. 100 ns) or some subset thereof. There
are a few functions that also allow "nsecs", but very little actually
has precision greater than hnsecs.
Symbol Description Types Duration Represents a duration of time of weeks or less (kept internally as hnsecs). (e.g. 22 days or 700 seconds). TickDuration DEPRECATED Represents a duration of time in system clock ticks, using the highest precision that the system provides. MonoTime Represents a monotonic timestamp in system clock ticks, using the highest precision that the system provides. Functions convert Generic way of converting between two time units. dur Allows constructing a Duration from the given time units with the given length. weeks days hours
minutes seconds msecs
usecs hnsecs nsecs |
Convenience aliases for dur. |
| abs | Returns the absolute value of a duration. |
From Duration
From TickDuration
From units
To Duration tickDuration.to, std,conv!Duration() dur!"msecs"(5) or 5.msecs()
| To TickDuration |
duration.to, std,conv!TickDuration() |
|
TickDuration.from!"msecs"(msecs) |
| To units |
duration.total!"days" |
tickDuration.msecs |
convert!("days", "msecs")(msecs) |
Source
core/time.d
time : (struct) core.time.MonoTimeImpl!(ClockType.normal)MonoTime, msecs;
import instrument : instr, instrInit, instrNowUs;
import objc.autorelease : autoreleasepool_push, autoreleasepool_pop; // objective-d
// Objective-C runtime selector lookup (objective-d wraps libobjc; declaring
// sel_registerName ourselves would clash with objc.rt's typed declaration).
import objc.rt : SEL;
// --- C-level declarations -----------------------------------------------------
// CoreGraphics C API — linked transitively via the Cocoa umbrella framework.
extern (C) nothrow @nogc
{
uint uint app.CGMainDisplayID() nothrow @nogcCGMainDisplayID();
void* void* app.CGColorSpaceCreateDeviceRGB() nothrow @nogcCGColorSpaceCreateDeviceRGB();
void void app.CGColorSpaceRelease(void* space) nothrow @nogcCGColorSpaceRelease(void* (parameter) void* spacespace);
void* void* app.CGDataProviderCreateWithData(void* info, const(void)* data, ulong size, void* releaseCallback) nothrow @nogcCGDataProviderCreateWithData(void* (parameter) void* infoinfo, const(void)* (parameter) const(void)* datadata, (alias) object.size_t = ulongsize_t (parameter) ulong sizesize,
void* (parameter) void* releaseCallbackreleaseCallback);
void void app.CGDataProviderRelease(void* provider) nothrow @nogcCGDataProviderRelease(void* (parameter) void* providerprovider);
void* void* app.CGImageCreate(ulong width, ulong height, ulong bitsPerComponent, ulong bitsPerPixel, ulong bytesPerRow, void* colorSpace, uint bitmapInfo, void* provider, const(double)* decode, bool shouldInterpolate, int intent) nothrow @nogcCGImageCreate((alias) object.size_t = ulongsize_t (parameter) ulong widthwidth, (alias) object.size_t = ulongsize_t (parameter) ulong heightheight, (alias) object.size_t = ulongsize_t (parameter) ulong bitsPerComponentbitsPerComponent,
(alias) object.size_t = ulongsize_t (parameter) ulong bitsPerPixelbitsPerPixel, (alias) object.size_t = ulongsize_t (parameter) ulong bytesPerRowbytesPerRow, void* (parameter) void* colorSpacecolorSpace, uint (parameter) uint bitmapInfobitmapInfo,
void* (parameter) void* providerprovider, const(double)* (parameter) const(double)* decodedecode, bool (parameter) bool shouldInterpolateshouldInterpolate, int (parameter) int intentintent);
void void app.CGImageRelease(void* image) nothrow @nogcCGImageRelease(void* (parameter) void* imageimage);
void void app.CGContextDrawImage(void* ctx, app.NSRect rect, void* image) nothrow @nogcCGContextDrawImage(void* (parameter) void* ctxctx, (struct) app.NSRectNSRect (parameter) app.NSRect rectrect, void* (parameter) void* imageimage);
}
enum uint (constant) uint app.kCGImageAlphaNoneSkipFirst = 6ukCGImageAlphaNoneSkipFirst = 6; // XRGB: alpha byte present, ignored
enum uint (constant) uint app.kCGBitmapByteOrder32Little = 8192ukCGBitmapByteOrder32Little = 2 << 12; // 32-bit little-endian packing
enum int (constant) int app.kCGRenderingIntentDefault = 0kCGRenderingIntentDefault = 0;
// Cocoa geometry (CGFloat == double on 64-bit). Plain C structs, not ObjC objects.
extern (C) struct (struct) app.NSPointNSPoint
{
double (field) double app.NSPoint.xx, (field) double app.NSPoint.yy;
}
extern (C) struct (struct) app.NSSizeNSSize
{
double (field) double app.NSSize.widthwidth, (field) double app.NSSize.heightheight;
}
extern (C) struct (struct) app.NSRectNSRect
{
(struct) app.NSPointNSPoint (field) app.NSPoint app.NSRect.originorigin;
(struct) app.NSSizeNSSize (field) app.NSSize app.NSRect.sizesize;
}
// --- AppKit class declarations (objective-d bundles Foundation, not AppKit) ----
extern (Objective-C):
extern class (class) app.NSObjectNSObject
{
}
extern class (class) app.NSStringNSString : (class) app.NSObjectNSObject
{
static (class) app.NSStringNSString app.NSString app.NSString.alloc()alloc() @selector("alloc");
(class) app.NSStringNSString app.NSString app.NSString.initWithUTF8String(const(char)* s)initWithUTF8String(const(char)* (parameter) const(char)* ss) @selector("initWithUTF8String:");
}
extern class (class) app.NSDateNSDate : (class) app.NSObjectNSObject
{
static (class) app.NSDateNSDate app.NSDate app.NSDate.dateWithTimeIntervalSinceNow(double secs)dateWithTimeIntervalSinceNow(double (parameter) double secssecs)
@selector("dateWithTimeIntervalSinceNow:");
}
extern class (class) app.NSResponderNSResponder : (class) app.NSObjectNSObject
{
}
extern class (class) app.NSEventNSEvent : (class) app.NSObjectNSObject
{
static (class) app.NSEventNSEvent app.NSEvent app.NSEvent.otherEventWithType(long type, app.NSPoint location, ulong modifierFlags, double timestamp, long windowNumber, app.NSObject context, short subtype, long data1, long data2)otherEventWithType(long (parameter) long typetype, (struct) app.NSPointNSPoint (parameter) app.NSPoint locationlocation, ulong (parameter) ulong modifierFlagsmodifierFlags,
double (parameter) double timestamptimestamp, long (parameter) long windowNumberwindowNumber, (class) app.NSObjectNSObject (parameter) app.NSObject contextcontext, short (parameter) short subtypesubtype,
long (parameter) long data1data1, long (parameter) long data2data2)
@selector("otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:");
}
extern class (class) app.NSTimerNSTimer : (class) app.NSObjectNSObject
{
// Schedules on the *current* run loop in the *default* mode.
static (class) app.NSTimerNSTimer app.NSTimer.scheduledTimerWithTimeIntervalscheduledTimerWithTimeInterval(double interval, (class) app.NSObjectNSObject target,
SEL sel, (class) app.NSObjectNSObject userInfo, bool repeats)
@selector("scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:");
// Creates without scheduling — the caller picks the run-loop mode(s).
static (class) app.NSTimerNSTimer app.NSTimer.timerWithTimeIntervaltimerWithTimeInterval(double interval, (class) app.NSObjectNSObject target,
SEL sel, (class) app.NSObjectNSObject userInfo, bool repeats)
@selector("timerWithTimeInterval:target:selector:userInfo:repeats:");
}
extern class (class) app.NSRunLoopNSRunLoop : (class) app.NSObjectNSObject
{
static (class) app.NSRunLoopNSRunLoop app.NSRunLoop app.NSRunLoop.currentRunLoop()currentRunLoop() @selector("currentRunLoop");
void void app.NSRunLoop.addTimer(app.NSTimer timer, app.NSString mode)addTimer((class) app.NSTimerNSTimer (parameter) app.NSTimer timertimer, (class) app.NSStringNSString (parameter) app.NSString modemode) @selector("addTimer:forMode:");
bool bool app.NSRunLoop.runMode(app.NSString mode, app.NSDate beforeDate)runMode((class) app.NSStringNSString (parameter) app.NSString modemode, (class) app.NSDateNSDate (parameter) app.NSDate beforeDatebeforeDate) @selector("runMode:beforeDate:");
}
extern class (class) app.NSGraphicsContextNSGraphicsContext : (class) app.NSObjectNSObject
{
static (class) app.NSGraphicsContextNSGraphicsContext app.NSGraphicsContext app.NSGraphicsContext.currentContext()currentContext() @selector("currentContext");
void* void* app.NSGraphicsContext.CGContext()CGContext() @selector("CGContext"); // CGContextRef
}
extern class (class) app.NSApplicationNSApplication : (class) app.NSResponderNSResponder
{
static (class) app.NSApplicationNSApplication app.NSApplication app.NSApplication.sharedApplication()sharedApplication() @selector("sharedApplication");
void void app.NSApplication.setActivationPolicy(long policy)setActivationPolicy(long (parameter) long policypolicy) @selector("setActivationPolicy:");
void void app.NSApplication.activateIgnoringOtherApps(bool flag)activateIgnoringOtherApps(bool (parameter) bool flagflag) @selector("activateIgnoringOtherApps:");
void void app.NSApplication.run()run() @selector("run");
void void app.NSApplication.stop(app.NSObject sender)stop((class) app.NSObjectNSObject (parameter) app.NSObject sendersender) @selector("stop:");
void void app.NSApplication.postEvent(app.NSEvent event, bool atStart)postEvent((class) app.NSEventNSEvent (parameter) app.NSEvent eventevent, bool (parameter) bool atStartatStart) @selector("postEvent:atStart:");
void void app.NSApplication.terminate(app.NSObject sender)terminate((class) app.NSObjectNSObject (parameter) app.NSObject sendersender) @selector("terminate:");
}
extern class (class) app.NSViewNSView : (class) app.NSResponderNSResponder
{
void void app.NSView.setFrameSize(app.NSSize newSize)setFrameSize((struct) app.NSSizeNSSize (parameter) app.NSSize newSizenewSize) @selector("setFrameSize:");
void void app.NSView.setNeedsDisplay(bool flag)setNeedsDisplay(bool (parameter) bool flagflag) @selector("setNeedsDisplay:");
(struct) app.NSRectNSRect app.NSRect app.NSView.bounds()bounds() @selector("bounds");
bool bool app.NSView.inLiveResize()inLiveResize() @selector("inLiveResize");
}
extern class (class) app.NSWindowNSWindow : (class) app.NSResponderNSResponder
{
static (class) app.NSWindowNSWindow app.NSWindow app.NSWindow.alloc()alloc() @selector("alloc");
(class) app.NSWindowNSWindow app.NSWindow app.NSWindow.initWithContentRect(app.NSRect contentRect, ulong styleMask, ulong backing, bool defer)initWithContentRect((struct) app.NSRectNSRect (parameter) app.NSRect contentRectcontentRect, ulong (parameter) ulong styleMaskstyleMask,
ulong (parameter) ulong backingbacking, bool (parameter) bool deferdefer) @selector("initWithContentRect:styleMask:backing:defer:");
void void app.NSWindow.setTitle(app.NSString title)setTitle((class) app.NSStringNSString (parameter) app.NSString titletitle) @selector("setTitle:");
void void app.NSWindow.setContentView(app.NSView view)setContentView((class) app.NSViewNSView (parameter) app.NSView viewview) @selector("setContentView:");
void void app.NSWindow.setDelegate(app.NSObject anObject)setDelegate((class) app.NSObjectNSObject (parameter) app.NSObject anObjectanObject) @selector("setDelegate:");
void void app.NSWindow.makeKeyAndOrderFront(app.NSObject sender)makeKeyAndOrderFront((class) app.NSObjectNSObject (parameter) app.NSObject sendersender) @selector("makeKeyAndOrderFront:");
double double app.NSWindow.backingScaleFactor()backingScaleFactor() @selector("backingScaleFactor");
}
// --- The custom NSView subclass, defined in D ----------------------------------
// Same recipe as the scaffold: bodied @selector methods are the implementation;
// selector-only callbacks (timer fires, delegate methods, the live-resize
// notifications) need no base declaration — Objective-C dispatch is by selector.
class (class) app.ColorViewColorView : (class) app.NSViewNSView
{
static (class) app.ColorViewColorView app.ColorView app.ColorView.alloc()alloc() @selector("alloc");
(class) app.ColorViewColorView app.ColorView app.ColorView.initWithFrame(app.NSRect frame)initWithFrame((struct) app.NSRectNSRect (parameter) app.NSRect frameframe) @selector("initWithFrame:");
void void app.ColorView.drawRect(app.NSRect dirtyRect)drawRect((struct) app.NSRectNSRect (parameter) app.NSRect dirtyRectdirtyRect) @selector("drawRect:")
{
onDrawRect(this.this.boundsbounds(), this.this.inLiveResizeinLiveResize());
}
// The control timer: default run-loop mode only.
void void app.ColorView.tickDefault(app.NSTimer timer)tickDefault((class) app.NSTimerNSTimer (parameter) app.NSTimer timertimer) @selector("tickDefault:")
{
onTimerFire(TimerId.TimerId.defaultModedefaultMode);
}
// The animation timer: NSRunLoopCommonModes; drives the color cycle.
void void app.ColorView.tickCommon(app.NSTimer timer)tickCommon((class) app.NSTimerNSTimer (parameter) app.NSTimer timertimer) @selector("tickCommon:")
{
onTimerFire(TimerId.TimerId.commonModescommonModes);
++g_hueStep;
this.this.setNeedsDisplaysetNeedsDisplay(true);
if (g_auto)
onAutoExitCheck();
}
// One-shot phase triggers (their own callouts, so neither measured timer is
// blocked by NSTimer's no-re-entrant-fire rule while the nested loop runs).
void void app.ColorView.enterTracking(app.NSTimer timer)enterTracking((class) app.NSTimerNSTimer (parameter) app.NSTimer timertimer) @selector("enterTracking:")
{
runNestedPhase(Phase.Phase.trackingtracking, g_modeTracking, "NSEventTrackingRunLoopMode");
}
void void app.ColorView.enterPanel(app.NSTimer timer)enterPanel((class) app.NSTimerNSTimer (parameter) app.NSTimer timertimer) @selector("enterPanel:")
{
runNestedPhase(Phase.Phase.panelpanel, g_modePanel, "NSModalPanelRunLoopMode");
}
// AppKit's live-resize bracket — the platform's `modal_enter`/`modal_exit`.
// Fires only for interactive border-drags (Tier C), never programmatically.
void void app.ColorView.viewWillStartLiveResize()viewWillStartLiveResize() @selector("viewWillStartLiveResize")
{
instr("modal_enter", "kind=live_resize");
g_phase = Phase.Phase.trackingtracking; // live-resize IS NSEventTrackingRunLoopMode
}
void void app.ColorView.viewDidEndLiveResize()viewDidEndLiveResize() @selector("viewDidEndLiveResize")
{
instr("modal_exit", "kind=live_resize");
g_phase = Phase.Phase.resumedresumed;
}
void void app.ColorView.windowWillClose(app.NSObject notification)windowWillClose((class) app.NSObjectNSObject (parameter) app.NSObject notificationnotification) @selector("windowWillClose:")
{
instr("close_requested");
g_app.g_app.terminateterminate(null);
}
}
extern (D): // back to D linkage for the rest of the module
enum (constant) int app.NSApplicationActivationPolicyRegular = 0NSApplicationActivationPolicyRegular = 0;
enum : ulong
{
(enum value) app.NSWindowStyleMaskTitled = 1LUNSWindowStyleMaskTitled = 1,
(enum value) app.NSWindowStyleMaskClosable = 2LUNSWindowStyleMaskClosable = 2,
(enum value) app.NSWindowStyleMaskResizable = 8LUNSWindowStyleMaskResizable = 8,
}
enum ulong (constant) ulong app.NSBackingStoreBuffered = 2LUNSBackingStoreBuffered = 2;
enum long (constant) long app.NSEventTypeApplicationDefined = 15LNSEventTypeApplicationDefined = 15;
// --- Demo state -----------------------------------------------------------------
enum (enum) app.TimerIdTimerId : int
{
(enum value) app.TimerId.defaultMode = 0defaultMode = 0, // control: starves in non-default modes
(enum value) app.TimerId.commonModes = 1commonModes = 1, // animation: survives them
}
enum (enum) app.PhasePhase : int
{
(enum value) app.Phase.defaultRun = 0defaultRun = 0, // [NSApp run] pumping the default mode
(enum value) app.Phase.tracking = 1tracking = 1, // nested NSEventTrackingRunLoopMode (== live-resize's mode)
(enum value) app.Phase.panel = 2panel = 2, // nested NSModalPanelRunLoopMode
(enum value) app.Phase.resumed = 3resumed = 3, // back in the default mode after a nested phase
}
immutable (alias) object.string = stringstring[2] (immutable global) immutable(string[2]) app.timerNamestimerNames = ["default", "common"];
immutable (alias) object.string = stringstring[4] (immutable global) immutable(string[4]) app.phaseNamesphaseNames = ["default", "tracking", "panel", "resumed"];
__gshared (class) app.NSApplicationNSApplication (__gshared global) app.NSApplication app.g_appg_app;
__gshared (class) app.NSWindowNSWindow (__gshared global) app.NSWindow app.g_wing_win;
__gshared (class) app.NSRunLoopNSRunLoop (__gshared global) app.NSRunLoop app.g_runLoopg_runLoop;
__gshared (class) app.NSStringNSString (__gshared global) app.NSString app.g_modeTrackingg_modeTracking, (__gshared global) app.NSString app.g_modePanelg_modePanel;
__gshared uint* (__gshared global) uint* app.g_bufg_buf;
__gshared (alias) object.size_t = ulongsize_t (__gshared global) ulong app.g_bufWg_bufW, (__gshared global) ulong app.g_bufHg_bufH;
__gshared (enum) app.PhasePhase (__gshared global) app.Phase app.g_phaseg_phase = Phase.Phase.defaultRundefaultRun;
__gshared bool (__gshared global) bool app.g_autog_auto;
__gshared int (__gshared global) int app.g_hueStepg_hueStep, (__gshared global) int app.g_drawsg_draws;
// Per-timer fire accounting: last fire time, then fires/max-gap per phase.
__gshared ulong[2] (__gshared global) ulong[2] app.g_lastFireUsg_lastFireUs;
__gshared uint[2] (__gshared global) uint[2] app.g_fireCountg_fireCount;
__gshared uint[4][2] (__gshared global) uint[4][2] app.g_firesPerPhaseg_firesPerPhase; // [timer][phase]
__gshared ulong[4][2] (__gshared global) ulong[4][2] app.g_maxGapPerPhaseg_maxGapPerPhase; // [timer][phase], µs
enum (constant) int app.timerIntervalMs = 100timerIntervalMs = 100; // both timers; the color cycle advances per common tick
enum (constant) int app.hueStepsPerCycle = 5hueStepsPerCycle = 5; // 5 × 100 ms = 500 ms per full cycle ≈ 2 Hz
enum (constant) double app.trackingEnterSec = 3.0trackingEnterSec = 3.0; // ~3 s of phase `default` first
enum (constant) double app.panelEnterSec = 7.0panelEnterSec = 7.0; // ~2 s `resumed` between the nested phases
enum (constant) int app.nestedPhaseMs = 2000nestedPhaseMs = 2000; // ~2 s per nested mode
enum (constant) int app.autoExitUs = 12000000autoExitUs = 12_000_000; // ~12 s total
void void app.onTimerFire(app.TimerId id)onTimerFire((enum) app.TimerIdTimerId (parameter) app.TimerId idid)
{
immutable _error_ nownow = instrNowUs();
immutable _error_ gapUsgapUs = g_lastFireUs[id] != 0 ? now - g_lastFireUs[id] : 0;
g_lastFireUs[id] = now;
++g_fireCount[id];
++g_firesPerPhase[id][g_phase];
if (gapUs > g_maxGapPerPhase[id][g_phase])
g_maxGapPerPhase[id][g_phase] = gapUs;
instr("tick", "timer=%.*s n=%u gap_ms=%.1f phase=%.*s",
cast(int) timerNames[id].length, timerNames[id].ptr, g_fireCount[id],
gapUs / 1000.0, cast(int) phaseNames[g_phase].length, phaseNames[g_phase].ptr);
}
// The headline measurement: run the run loop nested in a non-default mode —
// structurally what AppKit does for live-resize/menu tracking
// (NSEventTrackingRunLoopMode) and modal panels (NSModalPanelRunLoopMode). The
// common-modes timer keeps firing into the nested loop; the default-mode timer
// cannot. Entered from a one-shot timer's callout so neither measured timer is
// blocked by its own in-progress callout.
void void app.runNestedPhase(app.Phase phase, app.NSString mode, const(char)* modeName)runNestedPhase((enum) app.PhasePhase (parameter) app.Phase phasephase, (class) app.NSStringNSString (parameter) app.NSString modemode, const(char)* (parameter) const(char)* modeNamemodeName)
{
g_phase = phase;
instr("phase_enter", "name=%.*s mode=%s dur_ms=%d",
cast(int) phaseNames[phase].length, phaseNames[phase].ptr, modeName,
nestedPhaseMs);
immutable _error_ deadlinedeadline = MonoTime.currTime + nestedPhaseMs.msecs;
while (MonoTime.currTime < deadline)
{
auto _error_ poolpool = autoreleasepool_push();
immutable _error_ ranSourceranSource = g_runLoop.g_runLoop.runModerunMode(mode,
NSDate.NSDate.dateWithTimeIntervalSinceNowdateWithTimeIntervalSinceNow(0.05));
autoreleasepool_pop(pool);
if (!ranSource)
usleep(10_000); // mode has no sources/timers: don't spin hot
}
g_phase = Phase.Phase.resumedresumed;
instr("phase_exit", "name=%.*s",
cast(int) phaseNames[phase].length, phaseNames[phase].ptr);
}
void void app.onAutoExitCheck()onAutoExitCheck()
{
if (g_phase != Phase.Phase.resumedresumed || instrNowUs() < autoExitUs)
return;
foreach ((parameter) timertimer; 0 .. 2)
foreach ((parameter) phasephase; 0 .. 4)
instr("gap_summary", "timer=%.*s phase=%.*s fires=%u max_gap_ms=%.1f",
cast(int) timerNames[timer].length, timerNames[timer].ptr,
cast(int) phaseNames[phase].length, phaseNames[phase].ptr,
g_firesPerPhase[timer][phase],
g_maxGapPerPhase[timer][phase] / 1000.0);
instr("step", "name=NSApp_stop");
g_app.g_app.stopstop(null);
// stop: is only checked after an event dispatch; post a synthetic event
// so [NSApp run] wakes up and exits (scaffold finding).
(unresolved type) NSEventNSEvent _error_ evev = NSEvent.NSEvent.otherEventWithTypeotherEventWithType(NSEventTypeApplicationDefined,
NSPoint(0, 0), 0, 0, 0, null, 0, 0, 0);
g_app.g_app.postEventpostEvent(ev, true);
}
void void app.onDrawRect(app.NSRect bounds, bool live)onDrawRect((struct) app.NSRectNSRect (parameter) app.NSRect boundsbounds, bool (parameter) bool livelive)
{
immutable double _error_ scalescale = g_win.g_win.backingScaleFactorbackingScaleFactor();
immutable (unresolved type) size_tsize_t _error_ pwpw = cast((unresolved type) size_tsize_t) (bounds.bounds.sizesize.bounds.size.widthwidth * scale);
immutable (unresolved type) size_tsize_t _error_ phph = cast((unresolved type) size_tsize_t) (bounds.bounds.sizesize.bounds.size.heightheight * scale);
if (pw == 0 || ph == 0)
return;
if (pw != g_bufW || ph != g_bufH)
{
free(g_buf);
g_buf = cast(uint*) malloc(pw * ph * 4);
g_bufW = pw;
g_bufH = ph;
}
// Full-window solid color cycling through hues, one step per common tick
// (~2 Hz full cycle). A frozen color = a starved animation timer.
immutable _error_ colorcolor = hueColor(g_hueStep % hueStepsPerCycle, hueStepsPerCycle);
foreach ((parameter) ii; 0 .. pw * ph)
g_buf[i] = color;
void* _error_ cscs = CGColorSpaceCreateDeviceRGB();
void* _error_ dpdp = CGDataProviderCreateWithData(null, g_buf, pw * ph * 4, null);
void* _error_ imgimg = CGImageCreate(pw, ph, 8, 32, pw * 4, cs,
kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst, dp, null, false,
kCGRenderingIntentDefault);
void* _error_ ctxctx = NSGraphicsContext.NSGraphicsContext.currentContextcurrentContext().NSGraphicsContext.currentContext().CGContextCGContext();
CGContextDrawImage(ctx, bounds, img);
CGImageRelease(img);
CGDataProviderRelease(dp);
CGColorSpaceRelease(cs);
++g_draws;
instr("draw", "n=%d hue_step=%d live_resize=%d phase=%.*s",
g_draws, g_hueStep, live ? 1 : 0,
cast(int) phaseNames[g_phase].length, phaseNames[g_phase].ptr);
}
uint uint app.hueColor(int step, int steps) nothrow @nogchueColor(int (parameter) int stepstep, int (parameter) int stepssteps) nothrow @nogc
{
// Coarse hue wheel: interpolate R->G->B->R over `steps` positions.
immutable _error_ tt = step * 768 / steps; // 0..767
int _error_ rr, _error_ gg, _error_ bb;
if (t < 256)
{
r = 255 - t;
g = t;
}
else if (t < 512)
{
g = 255 - (t - 256);
b = t - 256;
}
else
{
b = 255 - (t - 512);
r = t - 512;
}
return 0xFF00_0000 | (cast(uint) r << 16) | (cast(uint) g << 8) | cast(uint) b;
}
int int D main()main()
{
g_auto = getenv("WSI_AUTO_EXIT") !is null;
instrInit("APPKIT_F03");
instr("init_start", "auto_exit=%d timer_interval_ms=%d", g_auto ? 1 : 0,
timerIntervalMs);
if (CGMainDisplayID() == 0)
{
printf("SKIP: no macOS window server (CGMainDisplayID == 0)\n");
return 0;
}
auto _error_ poolpool = autoreleasepool_push();
scope (exit)
autoreleasepool_pop(pool);
g_app = NSApplication.NSApplication.sharedApplicationsharedApplication();
g_app.g_app.setActivationPolicysetActivationPolicy(NSApplicationActivationPolicyRegular);
immutable _error_ contentRectcontentRect = NSRect(NSPoint(120, 120), NSSize(480, 320));
g_win = NSWindow.NSWindow.allocalloc().NSWindow.alloc().initWithContentRectinitWithContentRect(contentRect,
NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable,
NSBackingStoreBuffered, false);
g_win.g_win.setTitlesetTitle(NSString.NSString.allocalloc().NSString.alloc().initWithUTF8StringinitWithUTF8String("wsi-f03-modal-loop"));
instr("window_created", "scale=%.1f", g_win.g_win.backingScaleFactorbackingScaleFactor());
(unresolved type) ColorViewColorView _error_ viewview = ColorView.ColorView.allocalloc().ColorView.alloc().initWithFrameinitWithFrame(contentRect);
g_win.g_win.setContentViewsetContentView(view);
g_win.g_win.setDelegatesetDelegate(view);
g_win.g_win.makeKeyAndOrderFrontmakeKeyAndOrderFront(null);
g_app.g_app.activateIgnoringOtherAppsactivateIgnoringOtherApps(true);
g_runLoop = NSRunLoop.NSRunLoop.currentRunLoopcurrentRunLoop();
g_modeTracking = NSString.NSString.allocalloc().NSString.alloc().initWithUTF8StringinitWithUTF8String("NSEventTrackingRunLoopMode");
g_modePanel = NSString.NSString.allocalloc().NSString.alloc().initWithUTF8StringinitWithUTF8String("NSModalPanelRunLoopMode");
(unresolved type) NSStringNSString _error_ modeCommonmodeCommon = NSString.NSString.allocalloc().NSString.alloc().initWithUTF8StringinitWithUTF8String("kCFRunLoopCommonModes");
// Timer 1 — the control: scheduledTimerWithTimeInterval: puts it on the
// current run loop in the DEFAULT mode only. This is the naive animation
// timer that freezes during live-resize/menu tracking.
instr("step", "name=timer_default api=scheduledTimerWithTimeInterval mode=default");
NSTimer.NSTimer.scheduledTimerWithTimeIntervalscheduledTimerWithTimeInterval(timerIntervalMs / 1000.0, view,
SEL.register("tickDefault:"), null, true);
// Timer 2 — the fix: an unscheduled timer added explicitly for
// NSRunLoopCommonModes, which AppKit's event-tracking and modal-panel modes
// are members of. This one drives the color cycle.
instr("step", "name=timer_common api=addTimer_forMode mode=NSRunLoopCommonModes");
(unresolved type) NSTimerNSTimer _error_ commoncommon = NSTimer.NSTimer.timerWithTimeIntervaltimerWithTimeInterval(timerIntervalMs / 1000.0, view,
SEL.register("tickCommon:"), null, true);
g_runLoop.g_runLoop.addTimeraddTimer(common, modeCommon);
if (g_auto)
{
// One-shot phase triggers, themselves in common modes (so the tracking
// trigger can still fire if anything else changes the mode underneath).
instr("step", "name=schedule_nested tracking_at_s=%.1f panel_at_s=%.1f",
trackingEnterSec, panelEnterSec);
(unresolved type) NSTimerNSTimer _error_ t1t1 = NSTimer.NSTimer.timerWithTimeIntervaltimerWithTimeInterval(trackingEnterSec, view,
SEL.register("enterTracking:"), null, false);
g_runLoop.g_runLoop.addTimeraddTimer(t1, modeCommon);
(unresolved type) NSTimerNSTimer _error_ t2t2 = NSTimer.NSTimer.timerWithTimeIntervaltimerWithTimeInterval(panelEnterSec, view,
SEL.register("enterPanel:"), null, false);
g_runLoop.g_runLoop.addTimeraddTimer(t2, modeCommon);
}
instr("step", "name=NSApp_run");
g_app.g_app.runrun();
instr("loop_exit", "ticks_default=%u ticks_common=%u draws=%d",
g_fireCount[TimerId.TimerId.defaultModedefaultMode], g_fireCount[TimerId.TimerId.commonModescommonModes], g_draws);
free(g_buf);
return 0;
}