// F03 — modal-loop survival, Win32 (../../f03-modal-loop.md).
//
// Implements the Win32 headline of ../../../features/f03-modal-loop.md on top
// of the scaffold (../scaffold/app.d):
//
// * A ~2 Hz full-window color-cycle animation. Crucially, the animation is
// driven from the *main loop body* (PeekMessage drain -> tick -> bounded
// MsgWaitForMultipleObjects wait), the way a game/UI loop renders — NOT
// from a SetTimer. That is exactly the shape the Win32 modal size/move
// loop starves: once DefWindowProcW enters its nested pump, the app's own
// loop body stops running and the animation freezes.
// * WSI_AUTO_EXIT=1 enters the modal loop *programmatically*, three ways in
// sequence, each bracketed by WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE
// ("modal_enter"/"modal_exit"):
// 1. SendMessage(WM_SYSCOMMAND, SC_SIZE | WMSZ_BOTTOMRIGHT) — the
// mouse-grab sizing variant custom-chrome apps use from
// WM_NCLBUTTONDOWN (the loop expects the button to be held);
// 2. SendMessage(WM_SYSCOMMAND, SC_SIZE) — keyboard-mode sizing
// ("Size" from the system menu; waits for arrow keys);
// 3. SendMessage(WM_SYSCOMMAND, SC_MOVE) — keyboard-mode move.
// Because no human is present, a watchdog thread feeds each loop with
// synthetic input via PostMessage (a WM_MOUSEMOVE, arrow-key WM_KEYDOWNs
// that really size/move the window, then VK_RETURN to confirm) and
// escalates to VK_ESCAPE -> WM_CANCELMODE -> WM_CLOSE if the loop refuses
// to exit, so the freeze-measurement window is always bounded.
// * WSI_MODAL_FIX=1 arms the survey-wide countermeasure: SetTimer on
// WM_ENTERSIZEMOVE, render a tick from each WM_TIMER (the modal loop's
// internal pump DOES dispatch timers), KillTimer on WM_EXITSIZEMOVE.
// Ticks then continue *inside* the modal loop (src=timer).
// * Every tick logs `tick t=... src=loop|timer`; inter-tick gaps are
// tracked globally and per modal window, and each modal window emits a
// `modal_summary ... max_gap_us=` line measuring the freeze (no-fix) or
// its absence (fix).
//
// Without WSI_AUTO_EXIT the demo runs until closed — grab a border or the
// titlebar and watch the ticks (the Tier C interactive script on Windows).
//
// Only druntime's built-in core.sys.windows bindings — no third-party packages.
module (module) appapp;
import (package) corecore.(module) core.atomicThe atomic module provides basic support for lock-free
concurrent programming.
Use the -preview=nosharedaccess compiler flag to detect
unsafe individual read or write operations on shared data.
Source
core/atomic.d
Examples
int y = 2;
shared int x = y; // OK
//x++; // read modify write error
x.atomicOp!"+="(1); // OK
//y = x; // read error with preview flag
y = x.atomicLoad(); // OK
assert(y == 3);
//x = 5; // write error with preview flag
x.atomicStore(5); // OK
assert(x.atomicLoad() == 5);
atomic : (alias template) app.atomicLoad = core.atomic.atomicLoad(MemoryOrder ms = MemoryOrder.seq, T)(auto ref return scope const T val) if (!is(T == shared(U), U) && !is(T == shared(inout(U)), U) && !is(T == shared(const(U)), U))Loads 'val' from memory and returns it. The memory barrier specified
by 'ms' is applied to the operation, which is fully sequenced by
default. Valid memory orders are MemoryOrder.raw, MemoryOrder.acq,
and MemoryOrder.seq.
atomicLoad, (alias template) app.atomicStore = core.atomic.atomicStore(MemoryOrder ms = MemoryOrder.seq, T, V)(ref T val, V newval) if (!is(T == shared) && !is(V == shared))Writes 'newval' into 'val'. The memory barrier specified by 'ms' is
applied to the operation, which is fully sequenced by default.
Valid memory orders are MemoryOrder.raw, MemoryOrder.rel, and
MemoryOrder.seq.
atomicStore;
import (package) corecore.(package) core.syssys.(package) core.sys.windowswindows.(module) core.sys.windows.windowsWindows API header module
Translated from MinGW API for MS-Windows 4.0
Source
core/sys/windows/windows.d
windows;
import instrument;
struct (struct) app.DemoDemo
{
HDC (field) _error_ app.Demo.memDcmemDc; // memory DC the DIB section is selected into
HBITMAP (field) _error_ app.Demo.dibdib; // top-down 32-bit DIB section (CPU-visible backbuffer)
HBITMAP (field) _error_ app.Demo.stockBmpstockBmp; // the 1x1 stock bitmap displaced by SelectObject
uint* (field) uint* app.Demo.pixelspixels; // DIB bits: 0x00RRGGBB, row-major, row 0 = top row
int (field) int app.Demo.widthwidth, (field) int app.Demo.heightheight; // current client size, physical pixels
uint (field) uint app.Demo.colorcolor; // current animation color (solid fill)
uint (field) uint app.Demo.tickCounttickCount; // animation ticks so far
long (field) long app.Demo.lastTickUslastTickUs; // timestamp of the previous tick
long (field) long app.Demo.maxGapUsmaxGapUs; // worst inter-tick gap over the whole run
bool (field) bool app.Demo.modalFixmodalFix; // WSI_MODAL_FIX=1: SetTimer countermeasure armed
bool (field) bool app.Demo.autoExitautoExit; // WSI_AUTO_EXIT=1: bounded run with programmatic modal entry
bool (field) bool app.Demo.firstPaintDonefirstPaintDone;
// Current modal window (the attempts run strictly one at a time).
const(char)* (field) const(char)* app.Demo.curNamecurName; // attempt name for the log lines
long (field) long app.Demo.curEnterUscurEnterUs, (field) long app.Demo.curExitUscurExitUs;
long (field) long app.Demo.curMaxGapUscurMaxGapUs; // worst tick gap from last pre-enter tick to first post-exit tick
uint (field) uint app.Demo.curTickscurTicks; // ticks observed while inside the modal loop
uint (field) uint app.Demo.curSizingcurSizing, (field) uint app.Demo.curMovingcurMoving; // WM_SIZING / WM_MOVING seen inside the loop
bool (field) bool app.Demo.bridgePendingbridgePending; // modal exited; next tick closes the measurement
bool (field) bool app.Demo.everEnteredeverEntered; // WM_ENTERSIZEMOVE seen for the current attempt
}
__gshared (struct) app.DemoDemo _error_ app.gg;
__gshared HWND _error_ app.g_hwndg_hwnd;
shared bool (shared global) shared(bool) app.s_inModals_inModal; // read by the watchdog thread
shared bool (shared global) shared(bool) app.s_attemptOvers_attemptOver; // the SendMessage returned; watchdog exits early
enum UINT_PTR (constant) _error_ app.MODAL_TIMER_ID = __errorMODAL_TIMER_ID = 2; // the WM_ENTERSIZEMOVE countermeasure timer
enum (constant) int app.TICK_MS = 16TICK_MS = 16; // ~60 Hz animation tick (loop body and modal timer alike)
enum (constant) int app.CYCLE_US = 500000CYCLE_US = 500_000; // 2 Hz: one full hue cycle every 500 ms
enum (constant) int app.WARMUP_TICKS = 30WARMUP_TICKS = 30; // ~0.5 s of animation before/between modal attempts
// WMSZ_* direction nibble ORed onto SC_SIZE (winuser.h; absent from druntime).
enum (constant) int app.WMSZ_BOTTOMRIGHT = 8WMSZ_BOTTOMRIGHT = 8;
// ---------------------------------------------------------------------------
// Backbuffer: a DIB section reallocated on every client-size change
// (same strategy as the scaffold; the arrow-key sizing resizes mid-loop).
void void app.createBackbuffer(int w, int h) nothrowcreateBackbuffer(int (parameter) int ww, int (parameter) int hh) nothrow
{
if (g.(field) _error_ g.dibdib !is null)
{
SelectObject(g.g.memDcmemDc, g.g.stockBmpstockBmp);
DeleteObject(g.g.dibdib);
g.g.dibdib = null;
g.g.pixelspixels = null;
g.g.widthwidth = g.g.heightheight = 0;
}
if ((parameter) int ww <= 0 || (parameter) int hh <= 0)
return;
BITMAPINFO (local variable) _error_ bmibmi;
bmi.bmiHeader.biSize = BITMAPINFOHEADER.sizeof;
bmi.bmiHeader.biWidth = w;
bmi.bmiHeader.biHeight = -h; // negative height = top-down rows
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* (local variable) void* bitsbits;
g.g.dibdib = CreateDIBSection(null, &bmi, DIB_RGB_COLORS, &bits, null, 0);
if (g.(field) _error_ g.dibdib is null)
{
logEvent("error what=CreateDIBSection code=%lu", GetLastError());
return;
}
g.g.pixelspixels = cast(uint*) bits;
g.g.widthwidth = w;
g.g.heightheight = h;
g.g.stockBmpstockBmp = cast(HBITMAP) SelectObject(g.g.memDcmemDc, g.g.dibdib);
logEvent("buffer_alloc size=%dx%d bytes=%d", w, h, w * h * 4);
}
// ~2 Hz full-saturation hue cycle: any >=1-frame freeze is a visible color
// jump, and the per-tick log line is the measurable counterpart.
uint uint app.colorAt(long tUs) nothrow @nogccolorAt(long (parameter) long tUstUs) nothrow @nogc
{
const (local variable) const(double) phasephase = cast(double)((parameter) long tUstUs % (constant) int app.CYCLE_US = 500000CYCLE_US) / (constant) int app.CYCLE_US = 500000CYCLE_US;
const (local variable) const(double) hh = (local variable) const(double) phasephase * 6.0;
const (local variable) const(int) sectorsector = cast(int) (local variable) const(double) hh % 6;
const (local variable) const(double) ff = (local variable) const(double) hh - (local variable) const(int) sectorsector;
const (local variable) const(uint) downdown = cast(uint)(255.0 * (1.0 - (local variable) const(double) ff));
const (local variable) const(uint) upup = cast(uint)(255.0 * (local variable) const(double) ff);
uint uint app.colorAt.rgb(uint r, uint gr, uint b) pure nothrow @nogc @safergb(uint (parameter) uint rr, uint (parameter) uint grgr, uint (parameter) uint bb) @nogc nothrow
{
return ((parameter) uint rr << 16) | ((parameter) uint grgr << 8) | (parameter) uint bb;
}
switch ((local variable) const(int) sectorsector)
{
case 0:
return uint app.colorAt.rgb(uint r, uint gr, uint b) pure nothrow @nogc @safergb(255, (local variable) const(uint) upup, 0);
case 1:
return uint app.colorAt.rgb(uint r, uint gr, uint b) pure nothrow @nogc @safergb((local variable) const(uint) downdown, 255, 0);
case 2:
return uint app.colorAt.rgb(uint r, uint gr, uint b) pure nothrow @nogc @safergb(0, 255, (local variable) const(uint) upup);
case 3:
return uint app.colorAt.rgb(uint r, uint gr, uint b) pure nothrow @nogc @safergb(0, (local variable) const(uint) downdown, 255);
case 4:
return uint app.colorAt.rgb(uint r, uint gr, uint b) pure nothrow @nogc @safergb((local variable) const(uint) upup, 0, 255);
default:
return uint app.colorAt.rgb(uint r, uint gr, uint b) pure nothrow @nogc @safergb(255, 0, (local variable) const(uint) downdown);
}
}
void void app.fillSolid(uint color) nothrowfillSolid(uint (parameter) uint colorcolor) nothrow
{
if (g.(field) _error_ g.pixelspixels is null)
return;
const (local variable) const(_error_) nn = cast((alias) object.size_t = ulongsize_t) g.(field) _error_ g.widthwidth * g.(field) _error_ g.heightheight;
foreach ((parameter) ii; 0 .. n)
g.g.pixelspixels[i] = color;
}
// ---------------------------------------------------------------------------
// The animation tick: advance the color, log, account the inter-tick gap, and
// present synchronously. Called from the main loop body (src=loop) and — only
// while the modal loop holds the thread, in fix mode — from WM_TIMER
// (src=timer).
void void app.tick(const(char)* src) nothrowtick(const(char)* (parameter) const(char)* srcsrc) nothrow
{
const (local variable) const(_error_) tt = nowUs();
if (g.(field) _error_ g.tickCounttickCount > 0)
{
const (local variable) const(_error_) gapgap = t - g.(field) _error_ g.lastTickUslastTickUs;
if (gap > g.(field) _error_ g.maxGapUsmaxGapUs)
g.g.maxGapUsmaxGapUs = gap;
if (bool core.atomic.atomicLoad!(MemoryOrder.seq, bool)(ref return scope shared(const(bool)) val) pure nothrow @nogc @trustedLoads 'val' from memory and returns it. The memory barrier specified
by 'ms' is applied to the operation, which is fully sequenced by
default. Valid memory orders are MemoryOrder.raw, MemoryOrder.acq,
and MemoryOrder.seq.
atomicLoad((shared global) shared(bool) app.s_inModals_inModal))
{
if (gap > g.(field) _error_ g.curMaxGapUscurMaxGapUs)
g.g.curMaxGapUscurMaxGapUs = gap;
++g.(field) _error_ g.curTickscurTicks;
}
else if (g.(field) _error_ g.bridgePendingbridgePending)
{
// First tick after WM_EXITSIZEMOVE: the bridge gap (last tick
// before/inside the loop -> this tick) completes the measurement.
if (gap > g.(field) _error_ g.curMaxGapUscurMaxGapUs)
g.g.curMaxGapUscurMaxGapUs = gap;
g.g.bridgePendingbridgePending = false;
logEvent("modal_summary name=%s dur_us=%lld ticks_during=%u "
~ "max_gap_us=%lld sizing=%u moving=%u",
g.g.curNamecurName, g.g.curExitUscurExitUs - g.g.curEnterUscurEnterUs, g.g.curTickscurTicks,
g.g.curMaxGapUscurMaxGapUs, g.g.curSizingcurSizing, g.g.curMovingcurMoving);
}
}
g.g.lastTickUslastTickUs = t;
++g.(field) _error_ g.tickCounttickCount;
g.g.colorcolor = colorAt(t);
logEvent("tick t=%lld src=%s frame=%u", t, src, g.g.tickCounttickCount);
InvalidateRect(g_hwnd, null, FALSE);
UpdateWindow(g_hwnd); // present now — queued WM_PAINTs may never be seen
}
// ---------------------------------------------------------------------------
// Watchdog thread: feeds the modal loop synthetic input so the freeze window
// is bounded with no human present, then escalates until the loop exits.
// Plain CreateThread — it only Sleeps, injects input, and logs (no GC).
//
// Two feed styles, matching how the two loop modes really consume input:
// * mouse (grab variants, the realistic drag): SendInput relative
// MOUSEEVENTF_MOVEs while the button injected by the main thread is held,
// then MOUSEEVENTF_LEFTUP — the loop's documented exit condition.
// * kbd (keyboard variants): posted WM_KEYDOWN arrows pick the edge and
// size/move the window, then VK_RETURN confirms.
// Escalation ladder if the loop is still alive: VK_ESCAPE -> button-up ->
// WM_CANCELMODE -> WM_CLOSE.
enum (enum) app.FeedKindFeedKind
{
(enum value) app.FeedKind.mouse = 0mouse,
(enum value) app.FeedKind.kbd = 1kbd,
}
enum (constant) int app.FREEZE_HOLD_MS = 600FREEZE_HOLD_MS = 600; // evidence window before the feed starts
void app.postKeypostKey(DWORD vk, const(char)* (parameter) const(char)* namename) nothrow
{
logEvent("watchdog action=post_key vk=%s ok=%d",
name, PostMessageW(g_hwnd, WM_KEYDOWN, vk, 0) ? 1 : 0);
}
void app.sendMousesendMouse(DWORD flags, int dx, int dy, const(char)* what) nothrow
{
INPUT _error_ inpinp;
inp.type = INPUT_MOUSE;
inp.mi.dx = dx;
inp.mi.dy = dy;
inp.mi.dwFlags = flags;
const _error_ sentsent = SendInput(1, &inp, INPUT.sizeof);
logEvent("watchdog action=send_input what=%s sent=%u", what, sent);
}
// Sleep in 50 ms slices, bailing as soon as the SendMessage has returned.
bool bool app.sleepUnlessOver(int ms) nothrowsleepUnlessOver(int (parameter) int msms) nothrow
{
foreach ((local variable) int ii; 0 .. (parameter) int msms / 50)
{
if (bool core.atomic.atomicLoad!(MemoryOrder.seq, bool)(ref return scope shared(const(bool)) val) pure nothrow @nogc @trustedLoads 'val' from memory and returns it. The memory barrier specified
by 'ms' is applied to the operation, which is fully sequenced by
default. Valid memory orders are MemoryOrder.raw, MemoryOrder.acq,
and MemoryOrder.seq.
atomicLoad((shared global) shared(bool) app.s_attemptOvers_attemptOver))
return false;
Sleep(50);
}
return !bool core.atomic.atomicLoad!(MemoryOrder.seq, bool)(ref return scope shared(const(bool)) val) pure nothrow @nogc @trustedLoads 'val' from memory and returns it. The memory barrier specified
by 'ms' is applied to the operation, which is fully sequenced by
default. Valid memory orders are MemoryOrder.raw, MemoryOrder.acq,
and MemoryOrder.seq.
atomicLoad((shared global) shared(bool) app.s_attemptOvers_attemptOver);
}
extern (Windows) DWORD app.watchdogProcwatchdogProc(LPVOID param) nothrow
{
const _error_ feedfeed = cast((unresolved type) FeedKindFeedKind) cast((unresolved type) size_tsize_t) param;
// Let the freeze accumulate a measurable evidence window first. (For the
// keyboard variants this also covers start_size_move's pre-loop, which
// runs *before* WM_ENTERSIZEMOVE and eats the first input.)
if (!sleepUnlessOver(FREEZE_HOLD_MS))
return 0;
if (feed == FeedKind.FeedKind.mousemouse)
{
// Relative moves: each one reaches the modal loop as a real
// WM_MOUSEMOVE and drives a WM_SIZING/WM_MOVING + window update.
foreach ((parameter) ii; 0 .. 4)
{
sendMouse(MOUSEEVENTF_MOVE, 16, 12, "move+16+12");
if (!sleepUnlessOver(50))
return 0;
}
if (!sleepUnlessOver(150))
return 0;
sendMouse(MOUSEEVENTF_LEFTUP, 0, 0, "leftup"); // the documented exit
}
else
{
// Arrows: in the pre-loop they pick the resize edge; in the modal
// loop proper they move the cursor 8 px per press (sizing/moving).
foreach ((parameter) ii; 0 .. 3)
{
postKey(VK_RIGHT, "RIGHT");
if (!sleepUnlessOver(50))
return 0;
}
foreach ((parameter) ii; 0 .. 2)
{
postKey(VK_DOWN, "DOWN");
if (!sleepUnlessOver(50))
return 0;
}
if (!sleepUnlessOver(100))
return 0;
postKey(VK_RETURN, "RETURN"); // confirm = exit the modal loop
}
// Escalation ladder, ~500 ms per rung, until the SendMessage returns.
if (!sleepUnlessOver(500))
return 0;
postKey(VK_ESCAPE, "ESCAPE");
if (!sleepUnlessOver(500))
return 0;
sendMouse(MOUSEEVENTF_LEFTUP, 0, 0, "leftup_escalate");
if (!sleepUnlessOver(500))
return 0;
logEvent("watchdog action=post_cancelmode ok=%d",
PostMessageW(g_hwnd, WM_CANCELMODE, 0, 0) ? 1 : 0);
if (!sleepUnlessOver(500))
return 0;
logEvent("watchdog event=giveup name=%s action=post_close", g.g.curNamecurName);
PostMessageW(g_hwnd, WM_CLOSE, 0, 0);
return 0;
}
// ---------------------------------------------------------------------------
// One programmatic modal-loop attempt. SendMessage to our own window is a
// direct WndProc call; the unhandled WM_SYSCOMMAND falls through to
// DefWindowProcW, which runs the entire interactive size/move modal loop
// inside this call — it does not return until the loop exits.
void app.runModalAttemptrunModalAttempt(const(char)* (parameter) const(char)* namename, WPARAM (parameter) WPARAM wparamwparam, (enum) app.FeedKindFeedKind (parameter) FeedKind feedfeed, bool (parameter) bool holdButtonholdButton) nothrow
{
g.g.curNamecurName = name;
g.g.curEnterUscurEnterUs = g.g.curExitUscurExitUs = 0;
g.g.curMaxGapUscurMaxGapUs = 0;
g.g.curTickscurTicks = g.g.curSizingcurSizing = g.g.curMovingcurMoving = 0;
g.g.bridgePendingbridgePending = false;
g.g.everEnteredeverEntered = false;
RECT _error_ rr;
GetWindowRect(g_hwnd, &r);
const _error_ cxcx = (r.left + r.right) / 2, _error_ cycy = (r.top + r.bottom) / 2;
// Pre-flight: the conditions the size/move loop bails on — zoomed,
// invisible — plus the activation state, since interactive size/move
// presumes a foreground window.
logEvent("probe name=%s visible=%d zoomed=%d iconic=%d foreground=%d active=%d",
name, IsWindowVisible(g_hwnd) ? 1 : 0, IsZoomed(g_hwnd) ? 1 : 0,
IsIconic(g_hwnd) ? 1 : 0,
GetForegroundWindow() is g_hwnd ? 1 : 0,
GetActiveWindow() is g_hwnd ? 1 : 0);
// The grab variants model a real drag, where the user already holds the
// left button (a custom-chrome app sends SC_SIZE|edge from
// WM_NCLBUTTONDOWN). The loop polls VK_LBUTTON and treats button-up as
// "drag over", so a *real* (injected) press must precede the request.
SetCursorPos(cx, cy);
if (holdButton)
sendMouse(MOUSEEVENTF_LEFTDOWN, 0, 0, "leftdown");
logEvent("modal_request name=%s wparam=0x%04llx cursor=%d,%d",
name, cast(ulong) wparam, cx, cy);
atomicStore(s_attemptOver, false);
HANDLE _error_ thth = CreateThread(null, 0, &watchdogProc,
cast(void*) cast((unresolved type) size_tsize_t) feed, 0, null);
const _error_ t0t0 = nowUs();
SendMessageW(g_hwnd, WM_SYSCOMMAND, wparam,
cast(LPARAM)((cast(uint) cy << 16) | (cast(uint) cx & 0xffff)));
atomicStore(s_attemptOver, true);
logEvent("modal_request_returned name=%s dur_us=%lld entered=%d",
name, nowUs() - t0, g.g.everEnteredeverEntered ? 1 : 0);
if (th !is null)
{
WaitForSingleObject(th, 5000); // no stray input into the next attempt
CloseHandle(th);
}
if (holdButton) // defensive: never leave the synthetic button held
sendMouse(MOUSEEVENTF_LEFTUP, 0, 0, "leftup_cleanup");
if (!g.g.everEnteredeverEntered)
logEvent("modal_summary name=%s dur_us=0 ticks_during=0 max_gap_us=0 "
~ "sizing=0 moving=0 note=never_entered", name);
}
// ---------------------------------------------------------------------------
// The window procedure.
extern (Windows) LRESULT app.wndProcwndProc(HWND hwnd, UINT (parameter) UINT msgmsg, WPARAM wParam, LPARAM lParam) nothrow
{
switch (msg)
{
case WM_CREATE:
g.g.memDcmemDc = CreateCompatibleDC(null);
return 0;
case WM_SYSCOMMAND:
logEvent("msg name=WM_SYSCOMMAND sc=0x%04llx", cast(ulong) wParam);
goto default; // DefWindowProcW runs the modal loop right here
case WM_ENTERSIZEMOVE:
g.g.curEnterUscurEnterUs = nowUs();
g.g.everEnteredeverEntered = true;
atomicStore(s_inModal, true);
logEvent("modal_enter t=%lld fix=%d", g.g.curEnterUscurEnterUs, g.g.modalFixmodalFix ? 1 : 0);
if (g.g.modalFixmodalFix)
{
// The countermeasure: the modal loop's internal pump dispatches
// WM_TIMER, so a timer armed here keeps the animation ticking
// while GetMessageW never returns to our own loop.
SetTimer(hwnd, MODAL_TIMER_ID, TICK_MS, null);
logEvent("step name=SetTimer id=modal interval_ms=%d", TICK_MS);
}
goto default;
case WM_EXITSIZEMOVE:
g.g.curExitUscurExitUs = nowUs();
if (g.g.modalFixmodalFix)
KillTimer(hwnd, MODAL_TIMER_ID);
atomicStore(s_inModal, false);
g.g.bridgePendingbridgePending = true;
logEvent("modal_exit t=%lld dur_us=%lld ticks_during=%u",
g.g.curExitUscurExitUs, g.g.curExitUscurExitUs - g.g.curEnterUscurEnterUs, g.g.curTickscurTicks);
goto default;
case WM_TIMER:
if (wParam == MODAL_TIMER_ID && atomicLoad(s_inModal))
tick("timer"); // a frame from *inside* the modal loop
return 0;
case WM_CANCELMODE:
logEvent("msg name=WM_CANCELMODE");
goto default;
case WM_GETMINMAXINFO:
// Queried by the size/move loop just before WM_ENTERSIZEMOVE —
// seeing it proves the modal-loop machinery actually started.
logEvent("msg name=WM_GETMINMAXINFO");
goto default;
case WM_CAPTURECHANGED:
logEvent("msg name=WM_CAPTURECHANGED");
return 0;
case WM_SIZING:
++g.g.curSizingcurSizing;
const _error_ srsr = cast(RECT*) lParam;
logEvent("msg name=WM_SIZING edge=%d rect=%ld,%ld-%ld,%ld",
cast(int) wParam, sr.left, sr.top, sr.right, sr.bottom);
goto default;
case WM_MOVING:
++g.g.curMovingcurMoving;
const _error_ mrmr = cast(RECT*) lParam;
logEvent("msg name=WM_MOVING rect=%ld,%ld-%ld,%ld",
mr.left, mr.top, mr.right, mr.bottom);
goto default;
case WM_KEYDOWN:
// Watchdog keys that arrive here were NOT consumed by a modal loop.
logEvent("msg name=WM_KEYDOWN vk=0x%02llx in_modal=%d",
cast(ulong) wParam, atomicLoad(s_inModal) ? 1 : 0);
return 0;
case WM_SIZE:
const _error_ ww = cast(int)(lParam & 0xffff);
const _error_ hh = cast(int)((lParam >> 16) & 0xffff);
logEvent("resize size=%dx%d", w, h);
if (wParam == SIZE_MINIMIZED)
return 0;
if (w != g.g.widthwidth || h != g.g.heightheight)
createBackbuffer(w, h);
return 0;
case WM_MOVE:
logEvent("msg name=WM_MOVE pos=%d,%d",
cast(int) cast(short)(lParam & 0xffff),
cast(int) cast(short)((lParam >> 16) & 0xffff));
return 0;
case WM_ERASEBKGND:
return 1; // the solid fill covers every pixel
case WM_PAINT:
PAINTSTRUCT _error_ psps;
HDC _error_ hdchdc = BeginPaint(hwnd, &ps);
fillSolid(g.g.colorcolor);
if (g.g.pixelspixels !is null)
BitBlt(hdc, 0, 0, g.g.widthwidth, g.g.heightheight, g.g.memDcmemDc, 0, 0, SRCCOPY);
if (!g.g.firstPaintDonefirstPaintDone)
{
g.g.firstPaintDonefirstPaintDone = true;
logEvent("first_pixel_presented size=%dx%d", g.g.widthwidth, g.g.heightheight);
}
EndPaint(hwnd, &ps);
return 0;
case WM_CLOSE:
logEvent("close_requested");
goto default; // DefWindowProcW responds with DestroyWindow
case WM_DESTROY:
logEvent("msg name=WM_DESTROY");
KillTimer(hwnd, MODAL_TIMER_ID);
createBackbuffer(0, 0); // frees the DIB section
if (g.g.memDcmemDc !is null)
{
DeleteDC(g.g.memDcmemDc);
g.g.memDcmemDc = null;
}
PostQuitMessage(0);
return 0;
default:
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
}
// ---------------------------------------------------------------------------
bool bool app.envFlag(const(wchar)* name) nothrowenvFlag(const(wchar)* (parameter) const(wchar)* namename) nothrow
{
WCHAR[8] (local variable) _error_ bufbuf;
const (local variable) const(_error_) nn = GetEnvironmentVariableW(name, buf.ptr, buf.length);
return n >= 1 && n < buf.length && buf[0] == '1';
}
int int D main()main()
{
instrumentInit("f03_modal_loop_win32");
logEvent("init_start");
g.g.autoExitautoExit = envFlag("WSI_AUTO_EXIT"w.ptr);
g.g.modalFixmodalFix = envFlag("WSI_MODAL_FIX"w.ptr);
logEvent("mode auto_exit=%d modal_fix=%d", g.g.autoExitautoExit ? 1 : 0, g.g.modalFixmodalFix ? 1 : 0);
HINSTANCE (local variable) _error_ hInsthInst = GetModuleHandleW(null);
HCURSOR (local variable) _error_ arrowarrow = LoadCursorW(null, IDC_ARROW);
auto (local variable) wstring clsNameclsName = "wsi-f03-class"w;
WNDCLASSEXW (local variable) _error_ wcwc;
wc.cbSize = WNDCLASSEXW.sizeof;
wc.lpfnWndProc = &wndProc;
wc.hInstance = hInst;
wc.lpszClassName = clsName.ptr;
wc.hCursor = arrow;
logEvent("step name=RegisterClassExW");
if (!RegisterClassExW(&wc))
{
logEvent("error what=RegisterClassExW code=%lu", GetLastError());
return 1;
}
logEvent("step name=CreateWindowExW");
g_hwnd = CreateWindowExW(0, clsName.ptr, "wsi-f03-modal-loop"w.ptr,
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 480, 320,
null, null, hInst, null);
if (g_hwnd is null)
{
logEvent("error what=CreateWindowExW code=%lu", GetLastError());
return 1;
}
logEvent("window_created");
ShowWindow(g_hwnd, SW_SHOW);
SetForegroundWindow(g_hwnd); // size/move presumes a foreground window
UpdateWindow(g_hwnd);
// The main loop: drain the queue, render one tick from the loop body,
// sleep on the queue with a TICK_MS cap. No SetTimer drives the normal
// animation — that is the point: this loop body is what the modal loop
// starves, exactly like a PeekMessage game loop or a toolkit iteration.
static struct (struct) app.main.AttemptAttempt
{
(alias) object.string = stringstring (field) string app.main.Attempt.namename;
WPARAM (field) _error_ app.main.Attempt.wparamwparam;
(enum) app.FeedKindFeedKind (field) app.FeedKind app.main.Attempt.feedfeed;
bool (field) bool app.main.Attempt.holdButtonholdButton;
}
static immutable (struct) app.main.AttemptAttempt[3] (immutable global) immutable(_error_) app.main.attemptsattempts = [
// The two realistic interactions (button held, mouse-fed): an
// interactive border resize and a title-bar drag — F03 requirement 2.
Attempt("sc_size_grab", SC_SIZE | WMSZ_BOTTOMRIGHT, FeedKind.FeedKind.mousemouse, true),
Attempt("sc_move_caption", SC_MOVE | 2 /* HTCAPTION nibble */, FeedKind.FeedKind.mousemouse, true),
// The keyboard variant ("Size" from the system menu), arrow-key-fed.
Attempt("sc_size_kbd", SC_SIZE, FeedKind.FeedKind.kbdkbd, false),
];
uint (local variable) uint attemptIdxattemptIdx = 0;
uint (local variable) uint nextActionTicknextActionTick = (constant) int app.WARMUP_TICKS = 30WARMUP_TICKS;
bool (local variable) bool runningrunning = true;
int (local variable) int exitCodeexitCode = 0;
while ((local variable) bool runningrunning)
{
MSG (local variable) _error_ msgmsg;
while (PeekMessageW(&msg, null, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
{
(local variable) bool runningrunning = false;
(local variable) int exitCodeexitCode = cast(int) msg.wParam;
break;
}
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
if (!(local variable) bool runningrunning)
break;
void app.tick(const(char)* src) nothrowtick("loop");
if (g.(field) _error_ g.autoExitautoExit && g.(field) _error_ g.tickCounttickCount >= (local variable) uint nextActionTicknextActionTick)
{
if ((local variable) uint attemptIdxattemptIdx < attempts.length)
{
const (local variable) const(_error_) aa = attempts[attemptIdx++];
runModalAttempt(a.a.namename.ptr, a.a.wparamwparam, a.a.feedfeed, a.a.holdButtonholdButton);
(local variable) uint nextActionTicknextActionTick = g.(field) _error_ g.tickCounttickCount + WARMUP_TICKS;
}
else
{
logEvent("summary mode=%s ticks=%u max_gap_us=%lld attempts=%d",
g.g.modalFixmodalFix ? "fix".ptr : "nofix".ptr, g.g.tickCounttickCount,
g.g.maxGapUsmaxGapUs, cast(int) attempts.length);
DestroyWindow(g_hwnd);
continue; // drain WM_DESTROY ... WM_QUIT
}
}
// Wake on any message or after TICK_MS — a poll-style frame cadence
// without a busy loop.
MsgWaitForMultipleObjects(0, null, FALSE, TICK_MS, QS_ALLINPUT);
}
logEvent("exit code=%d", exitCode);
return (local variable) int exitCodeexitCode;
}