// F02 — resize correctness, Win32 (../../f02-resize.md).
//
// Implements every requirement of ../../../features/f02-resize.md on top of
// the scaffold (../scaffold/app.d):
//
// * Continuously redrawn corner-anchored gradient (red tracks x, green
// tracks y, blue advances per frame) so stretching or stale buffers are
// visible; after every draw the buffer's four corners are verified
// against the expected gradient values (`paint_check`).
// * An aggressive programmatic SetWindowPos storm (WSI_AUTO_EXIT=1):
// pure grows, pure shrinks, mixed grow/shrink, move-only (SWP_NOSIZE)
// and a same-size no-move step — each followed by a synchronous
// UpdateWindow and a `step_result painted=` verdict.
// * Every WM_SIZING / WM_SIZE / WM_WINDOWPOSCHANGING / WM_WINDOWPOSCHANGED
// / WM_ERASEBKGND / WM_PAINT is logged with its wParam / WINDOWPOS
// fields / client size.
// * WSI_NO_INVALIDATE=1 drops the per-step InvalidateRect, reproducing the
// scaffold's "a pure shrink invalidates nothing" finding: shrink steps
// then present no frame and the window keeps a stale, wrongly-anchored
// gradient (step_result painted=0).
// * WSI_GROW_ONLY=1 switches the DIB strategy from realloc-per-resize to
// grow-only reuse (the DIB keeps its high-water-mark size; smaller client
// sizes draw through a stride and log `buffer_reuse` instead of
// `buffer_alloc`).
// * WM_SIZING (and WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE) cannot fire from a
// programmatic storm — their observed count is logged at storm end; the
// interactive modal-resize path is F03's subject (Tier C here).
//
// Only druntime's built-in core.sys.windows bindings — no third-party packages.
module (module) appapp;
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
int (field) int app.Demo.capWcapW, (field) int app.Demo.capHcapH; // allocated DIB size (== width/height unless grow-only)
int (field) int app.Demo.dpidpi = 96; // monitor DPI; scale = dpi / 96
uint (field) uint app.Demo.frameframe; // paint counter (animates the gradient)
uint (field) uint app.Demo.ticksticks; // WM_TIMER counter (auto-exit schedule)
uint (field) uint app.Demo.sizingCountsizingCount; // WM_SIZING messages seen (expected 0 programmatically)
uint (field) uint app.Demo.sizeMoveCountsizeMoveCount; // WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE seen (expected 0)
uint (field) uint app.Demo.checksFailedchecksFailed; // paint_check corner mismatches
bool (field) bool app.Demo.autoExitautoExit; // WSI_AUTO_EXIT=1: bounded run with the resize storm
bool (field) bool app.Demo.noInvalidatenoInvalidate; // WSI_NO_INVALIDATE=1: drop the per-step InvalidateRect
bool (field) bool app.Demo.growOnlygrowOnly; // WSI_GROW_ONLY=1: grow-only DIB reuse instead of realloc
bool (field) bool app.Demo.firstSizeSeenfirstSizeSeen;
bool (field) bool app.Demo.firstPaintDonefirstPaintDone;
}
__gshared (struct) app.DemoDemo _error_ app.gg;
enum UINT_PTR (constant) _error_ app.TIMER_ID = __errorTIMER_ID = 1;
enum (constant) int app.TICK_MS = 16TICK_MS = 16; // ~60 Hz animation tick
enum (constant) int app.STORM_AFTER_TICKS = 30STORM_AFTER_TICKS = 30; // ~0.5 s of animation before the resize storm
// ---------------------------------------------------------------------------
// Backbuffer. Two strategies (the allocation strategy is an F02 finding):
// realloc (default): free + CreateDIBSection on every client-size change.
// grow-only (WSI_GROW_ONLY=1): the DIB only ever grows (per-dimension
// high-water mark); a smaller client size reuses it through a row stride.
void void app.createBackbuffer(int w, int h) nothrowcreateBackbuffer(int (parameter) int ww, int (parameter) int hh) nothrow
{
if ((parameter) int ww <= 0 || (parameter) int hh <= 0)
{
void app.releaseBackbuffer() nothrowreleaseBackbuffer();
return;
}
if (g.(field) _error_ g.growOnlygrowOnly && g.(field) _error_ g.dibdib !is null && (parameter) int ww <= g.(field) _error_ g.capWcapW && (parameter) int hh <= g.(field) _error_ g.capHcapH)
{
g.g.widthwidth = w;
g.g.heightheight = h;
logEvent("buffer_reuse size=%dx%d cap=%dx%d", w, h, g.g.capWcapW, g.g.capHcapH);
return;
}
const (local variable) const(_error_) allocWallocW = g.(field) _error_ g.growOnlygrowOnly ? ((parameter) int ww > g.(field) _error_ g.capWcapW ? (parameter) int ww : g.(field) _error_ g.capWcapW) : (parameter) int ww;
const (local variable) const(_error_) allocHallocH = g.(field) _error_ g.growOnlygrowOnly ? ((parameter) int hh > g.(field) _error_ g.capHcapH ? (parameter) int hh : g.(field) _error_ g.capHcapH) : (parameter) int hh;
void app.releaseBackbuffer() nothrowreleaseBackbuffer();
BITMAPINFO (local variable) _error_ bmibmi;
bmi.bmiHeader.biSize = BITMAPINFOHEADER.sizeof;
bmi.bmiHeader.biWidth = allocW;
bmi.bmiHeader.biHeight = -allocH; // 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.capWcapW = allocW;
g.g.capHcapH = allocH;
g.g.stockBmpstockBmp = cast(HBITMAP) SelectObject(g.g.memDcmemDc, g.g.dibdib);
logEvent("buffer_alloc size=%dx%d cap=%dx%d bytes=%d",
w, h, allocW, allocH, allocW * allocH * 4);
}
void void app.releaseBackbuffer() nothrowreleaseBackbuffer() nothrow
{
if (g.(field) _error_ g.dibdib is null)
return;
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 = g.g.capWcapW = g.g.capHcapH = 0;
}
// Corner-anchored diagonal gradient over the *current* client size, drawn
// through the allocated stride (capW) so grow-only reuse stays anchored.
void void app.drawGradient() nothrowdrawGradient() nothrow
{
if (g.(field) _error_ g.pixelspixels is null)
return;
const (local variable) const(_error_) ww = g.(field) _error_ g.widthwidth, (local variable) const(_error_) hh = g.(field) _error_ g.heightheight;
const (local variable) const(_error_) blueblue = (g.(field) _error_ g.frameframe * 4) & 0xff;
foreach ((parameter) yy; 0 .. h)
{
uint* _error_ rowrow = g.g.pixelspixels + cast(size_t) y * g.g.capWcapW;
const _error_ greengreen = h > 1 ? (y * 255) / (h - 1) : 0;
foreach ((parameter) xx; 0 .. w)
{
const _error_ redred = w > 1 ? (x * 255) / (w - 1) : 0;
row[x] = cast(uint)((red << 16) | (green << 8) | blue);
}
}
}
// Verify the gradient really is anchored to the current size: the four
// corners of the w x h region must hold the expected channel extremes.
// A stale or wrongly-strided buffer fails immediately (F02 requirement 1).
void void app.checkCorners() nothrowcheckCorners() nothrow
{
if (g.(field) _error_ g.pixelspixels is null || g.(field) _error_ g.widthwidth < 2 || g.(field) _error_ g.heightheight < 2)
return;
const (local variable) const(_error_) ww = g.(field) _error_ g.widthwidth, (local variable) const(_error_) hh = g.(field) _error_ g.heightheight;
const (local variable) const(_error_) blueblue = (g.(field) _error_ g.frameframe * 4) & 0xff;
uint uint app.checkCorners.at(int x, int y) pure nothrow @nogc @safeat(int (parameter) int xx, int (parameter) int yy) nothrow
{
return g.(field) _error_ g.pixelspixels[cast(size_t) y * g.g.capWcapW + x];
}
const (local variable) const(_error_) expTlexpTl = cast(uint) blue;
const (local variable) const(_error_) expTrexpTr = cast(uint)((255 << 16) | blue);
const (local variable) const(_error_) expBlexpBl = cast(uint)((255 << 8) | blue);
const (local variable) const(_error_) expBrexpBr = cast(uint)((255 << 16) | (255 << 8) | blue);
const (local variable) const(_error_) okok = uint app.checkCorners.at(int x, int y) pure nothrow @nogc @safeat(0, 0) == expTl && uint app.checkCorners.at(int x, int y) pure nothrow @nogc @safeat(w - 1, 0) == expTr
&& uint app.checkCorners.at(int x, int y) pure nothrow @nogc @safeat(0, h - 1) == expBl && uint app.checkCorners.at(int x, int y) pure nothrow @nogc @safeat(w - 1, h - 1) == expBr;
if (!ok)
{
++g.(field) _error_ g.checksFailedchecksFailed;
logEvent("paint_check ok=0 size=%dx%d tl=%06x tr=%06x bl=%06x br=%06x",
w, h, at(0, 0), at(w - 1, 0), at(0, h - 1), at(w - 1, h - 1));
}
}
// ---------------------------------------------------------------------------
// The resize storm: 14 programmatic SetWindowPos steps covering pure grows,
// pure shrinks, mixed grow/shrink, move-only and a same-size no-move step
// (F02 requirement 2). Each step's messages dispatch re-entrantly inside
// SetWindowPos; the per-step verdict logs whether a frame was presented.
struct (struct) app.StormStepStormStep
{
const(char)* (field) const(char)* app.StormStep.kindkind; // grow | shrink | mixed | move | same
int (field) int app.StormStep.xx, (field) int app.StormStep.yy; // position (move steps; others pass SWP_NOMOVE)
int (field) int app.StormStep.ww, (field) int app.StormStep.hh; // outer size (size steps; move steps pass SWP_NOSIZE)
UINT (field) _error_ app.StormStep.extraFlagsextraFlags; // SWP_NOMOVE or SWP_NOSIZE
}
void app.runResizeStormrunResizeStorm(HWND (parameter) HWND hwndhwnd) nothrow
{
static immutable (unresolved type) StormStepStormStep[14] StormStep[14] stepssteps = [
StormStep("grow", 0, 0, 520, 360, SWP_NOMOVE),
StormStep("grow", 0, 0, 640, 480, SWP_NOMOVE),
StormStep("grow", 0, 0, 800, 600, SWP_NOMOVE),
StormStep("grow", 0, 0, 1024, 768, SWP_NOMOVE),
StormStep("shrink", 0, 0, 700, 500, SWP_NOMOVE),
StormStep("shrink", 0, 0, 500, 350, SWP_NOMOVE),
StormStep("shrink", 0, 0, 320, 240, SWP_NOMOVE),
StormStep("mixed", 0, 0, 240, 640, SWP_NOMOVE),
StormStep("mixed", 0, 0, 640, 240, SWP_NOMOVE),
StormStep("move", 120, 120, 0, 0, SWP_NOSIZE),
StormStep("move", 240, 180, 0, 0, SWP_NOSIZE),
StormStep("same", 0, 0, 640, 240, SWP_NOMOVE), // same outer size
StormStep("grow", 0, 0, 800, 600, SWP_NOMOVE),
StormStep("shrink", 0, 0, 480, 320, SWP_NOMOVE),
];
logEvent("resize_storm_begin steps=%d invalidate=%d grow_only=%d",
cast(int) steps.length, g.g.noInvalidatenoInvalidate ? 0 : 1, g.g.growOnlygrowOnly ? 1 : 0);
foreach ((parameter) ii, (parameter) ss; steps)
{
logEvent("step name=SetWindowPos i=%d kind=%s pos=%d,%d size=%dx%d",
cast(int) i, s.s.kindkind, s.s.xx, s.s.yy, s.s.ww, s.s.hh);
const _error_ framesBeforeframesBefore = g.g.frameframe;
const _error_ wBeforewBefore = g.g.widthwidth, _error_ hBeforehBefore = g.g.heightheight;
SetWindowPos(hwnd, null, s.s.xx, s.s.yy, s.s.ww, s.s.hh,
SWP_NOZORDER | SWP_NOACTIVATE | s.s.extraFlagsextraFlags);
// The storm runs inside one WM_TIMER dispatch, so queued WM_PAINTs
// would never be seen before DestroyWindow — present synchronously.
// The InvalidateRect is load-bearing: a pure shrink invalidates
// nothing by itself (the retained surface already covers the smaller
// client area), so without it UpdateWindow is a no-op and the window
// keeps a stale gradient. WSI_NO_INVALIDATE=1 demonstrates exactly
// that (the step_result below records painted=0 for shrink steps).
if (!g.g.noInvalidatenoInvalidate)
InvalidateRect(hwnd, null, FALSE);
UpdateWindow(hwnd);
const _error_ paintedpainted = g.g.frameframe != framesBefore;
const _error_ sizeChangedsizeChanged = g.g.widthwidth != wBefore || g.g.heightheight != hBefore;
logEvent("step_result i=%d kind=%s painted=%d client=%dx%d",
cast(int) i, s.s.kindkind, painted ? 1 : 0, g.g.widthwidth, g.g.heightheight);
// Stale only when the client size changed and no frame followed: the
// window then shows the previous frame's gradient, wrongly anchored
// for the new size (move-only / same-size steps are harmless).
if (!painted && sizeChanged)
logEvent("stale_content i=%d kind=%s was=%dx%d now=%dx%d "
~ "note=window_still_shows_frame_anchored_to_old_size",
cast(int) i, s.s.kindkind, wBefore, hBefore, g.g.widthwidth, g.g.heightheight);
}
logEvent("resize_storm_end wm_sizing_count=%u wm_entersizemove_count=%u "
~ "note=modal_resize_loop_not_reachable_programmatically_see_f03",
g.g.sizingCountsizingCount, g.g.sizeMoveCountsizeMoveCount);
logEvent("paint_checks failed=%u", g.g.checksFailedchecksFailed);
DestroyWindow(hwnd);
}
// ---------------------------------------------------------------------------
// The window procedure: every F02-relevant message is logged with its payload.
extern (Windows) LRESULT app.wndProcwndProc(HWND (parameter) HWND hwndhwnd, UINT (parameter) UINT msgmsg, WPARAM wParam, LPARAM lParam) nothrow
{
switch (msg)
{
case WM_NCCREATE:
logEvent("msg name=WM_NCCREATE");
goto default;
case WM_NCCALCSIZE:
logEvent("msg name=WM_NCCALCSIZE");
goto default;
case WM_CREATE:
logEvent("msg name=WM_CREATE");
g.g.memDcmemDc = CreateCompatibleDC(null);
// druntime's core.sys.windows has no GetDpiForWindow (Win10 1607+);
// the system DPI from the screen DC is enough for the scale= field.
HDC _error_ screenscreen = GetDC(null);
g.g.dpidpi = GetDeviceCaps(screen, LOGPIXELSX);
ReleaseDC(null, screen);
return 0;
case WM_SHOWWINDOW:
logEvent("msg name=WM_SHOWWINDOW shown=%d", cast(int) wParam);
goto default;
case WM_ENTERSIZEMOVE: // interactive size/move modal loop — F03's subject
case WM_EXITSIZEMOVE:
++g.g.sizeMoveCountsizeMoveCount;
logEvent("msg name=%s", msg == WM_ENTERSIZEMOVE
? "WM_ENTERSIZEMOVE".ptr : "WM_EXITSIZEMOVE".ptr);
goto default;
case WM_SIZING: // only the interactive border drag produces this
++g.g.sizingCountsizingCount;
const _error_ rr = cast(RECT*) lParam;
logEvent("msg name=WM_SIZING edge=%d rect=%ld,%ld-%ld,%ld",
cast(int) wParam, r.left, r.top, r.right, r.bottom);
goto default;
case WM_WINDOWPOSCHANGING:
const _error_ wpgwpg = cast(WINDOWPOS*) lParam;
logEvent("msg name=WM_WINDOWPOSCHANGING pos=%d,%d size=%dx%d flags=0x%04x",
wpg.wpg.xx, wpg.wpg.yy, wpg.cx, wpg.cy, wpg.flags);
goto default;
case WM_WINDOWPOSCHANGED:
const _error_ wpcwpc = cast(WINDOWPOS*) lParam;
logEvent("msg name=WM_WINDOWPOSCHANGED pos=%d,%d size=%dx%d flags=0x%04x",
wpc.wpc.xx, wpc.wpc.yy, wpc.cx, wpc.cy, wpc.flags);
goto default; // DefWindowProcW synthesizes WM_SIZE/WM_MOVE from this
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_SIZE:
const _error_ ww = cast(int)(lParam & 0xffff);
const _error_ hh = cast(int)((lParam >> 16) & 0xffff);
logEvent("msg name=WM_SIZE wparam=%d size=%dx%d", cast(int) wParam, w, h);
if (!g.g.firstSizeSeenfirstSizeSeen)
{
g.g.firstSizeSeenfirstSizeSeen = true;
logEvent("first_configure size=%dx%d", w, h);
}
logEvent("resize size=%dx%d scale=%d.%02d",
w, h, g.g.dpidpi / 96, (g.g.dpidpi % 96) * 100 / 96);
if (wParam == SIZE_MINIMIZED)
return 0;
if (w != g.g.widthwidth || h != g.g.heightheight)
createBackbuffer(w, h);
return 0;
case WM_ERASEBKGND:
logEvent("msg name=WM_ERASEBKGND");
return 1; // claim erased — WM_PAINT repaints the full client anyway
case WM_PAINT:
logEvent("msg name=WM_PAINT");
PAINTSTRUCT _error_ psps;
HDC _error_ hdchdc = BeginPaint(hwnd, &ps);
++g.g.frameframe;
drawGradient();
checkCorners();
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);
}
logEvent("frame_callback t=%lld frame=%u size=%dx%d",
nowUs(), g.g.frameframe, g.g.widthwidth, g.g.heightheight);
EndPaint(hwnd, &ps);
return 0;
case WM_TIMER:
if (wParam != TIMER_ID)
return 0;
++g.g.ticksticks;
InvalidateRect(hwnd, null, FALSE); // schedule the next WM_PAINT
if (g.g.autoExitautoExit && g.g.ticksticks == STORM_AFTER_TICKS)
runResizeStorm(hwnd); // ends in DestroyWindow
return 0;
case WM_CLOSE:
logEvent("close_requested");
goto default; // DefWindowProcW responds with DestroyWindow
case WM_DESTROY:
logEvent("msg name=WM_DESTROY");
KillTimer(hwnd, TIMER_ID);
releaseBackbuffer();
if (g.g.memDcmemDc !is null)
{
DeleteDC(g.g.memDcmemDc);
g.g.memDcmemDc = null;
}
PostQuitMessage(0);
return 0;
case WM_NCDESTROY:
logEvent("msg name=WM_NCDESTROY");
goto default;
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("f02_resize_win32");
logEvent("init_start");
g.g.autoExitautoExit = envFlag("WSI_AUTO_EXIT"w.ptr);
g.g.noInvalidatenoInvalidate = envFlag("WSI_NO_INVALIDATE"w.ptr);
g.g.growOnlygrowOnly = envFlag("WSI_GROW_ONLY"w.ptr);
logEvent("mode auto_exit=%d invalidate=%d grow_only=%d",
g.g.autoExitautoExit ? 1 : 0, g.g.noInvalidatenoInvalidate ? 0 : 1, g.g.growOnlygrowOnly ? 1 : 0);
logEvent("step name=GetModuleHandleW");
HINSTANCE (local variable) _error_ hInsthInst = GetModuleHandleW(null);
logEvent("step name=LoadCursorW");
HCURSOR (local variable) _error_ arrowarrow = LoadCursorW(null, IDC_ARROW);
auto (local variable) wstring clsNameclsName = "wsi-f02-class"w;
WNDCLASSEXW (local variable) _error_ wcwc;
wc.cbSize = WNDCLASSEXW.sizeof;
wc.lpfnWndProc = &wndProc;
wc.hInstance = hInst;
wc.lpszClassName = clsName.ptr;
wc.hCursor = arrow;
// No hbrBackground: WM_ERASEBKGND is handled, the DIB covers every pixel.
logEvent("step name=RegisterClassExW");
if (!RegisterClassExW(&wc))
{
logEvent("error what=RegisterClassExW code=%lu", GetLastError());
return 1;
}
logEvent("step name=CreateWindowExW");
HWND (local variable) _error_ hwndhwnd = CreateWindowExW(0, clsName.ptr, "wsi-f02-resize"w.ptr,
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 480, 320,
null, null, hInst, null);
if (hwnd is null)
{
logEvent("error what=CreateWindowExW code=%lu", GetLastError());
return 1;
}
logEvent("window_created");
logEvent("step name=ShowWindow");
ShowWindow(hwnd, SW_SHOW);
logEvent("step name=UpdateWindow");
UpdateWindow(hwnd); // forces the first WM_PAINT synchronously, now
logEvent("step name=SetTimer interval_ms=%d", TICK_MS);
SetTimer(hwnd, TIMER_ID, TICK_MS, null);
MSG (local variable) _error_ msgmsg;
while (GetMessageW(&msg, null, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
logEvent("exit code=%d", cast(int) msg.wParam);
return cast(int) msg.wParam;
}