// xdg-shell windowing scaffold — the full "first window" handshake that the
// minimal ../../example/app.d deliberately stops short of. It opens a real
// toplevel window titled "wsi-scaffold" and exercises the complete Wayland
// pixel pipeline:
//
// wl_display → wl_registry → bind {wl_compositor, wl_shm, xdg_wm_base, wl_seat}
// → wl_surface → xdg_surface → xdg_toplevel → initial *no-buffer* commit
// → xdg_surface.configure → ack_configure → wl_shm double-buffered ARGB
// gradient → wl_surface.attach/damage_buffer/commit → wl_surface.frame
// callbacks drive the redraw loop.
//
// Resize is negotiated, never imposed: every xdg_surface.configure is acked
// with its serial *before* the next commit, and the committed buffer is
// asserted to match the acked size (the F02 contract). Under WSI_AUTO_EXIT=1
// the demo drives its own resize storm — set_maximized at frame 30, unset at
// frame 60 — and exits 0 after ~120 frames (or ~2.5 s).
//
// The xdg-shell glue is generated by wayland-scanner (./generate.sh); the
// `static inline` request helpers are re-exported as `wsi_*` by the ImportC
// shim ./c.c. Instrumentation contract: ./instrument.d. Findings: ../../scaffold.md.
//
// Headless-safe: no compositor → SKIP, exit 0.
module (module) appapp;
import c; // ImportC: <wayland-client.h> + xdg-shell glue + wsi_* wrappers
import instrument;
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;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stringD header file for C99.
pubs.opengroup.org/onlinepubs/009695399/basedefs/string.h.html, string.h
Source
core/stdc/string.d
string : (alias) app.strcmp = int core.stdc.string.strcmp(scope const(char*) s1, scope const(char*) s2) pure nothrow @nogcstrcmp;
// ----------------------------------------------------------------- tunables
enum int (constant) int app.defaultWidth = 640defaultWidth = 640; // used while the compositor lets us pick (0x0 configure)
enum int (constant) int app.defaultHeight = 480defaultHeight = 480;
enum int (constant) int app.maximizeAtFrame = 30maximizeAtFrame = 30; // auto-exit choreography: resize storm …
enum int (constant) int app.unmaximizeAtFrame = 60unmaximizeAtFrame = 60; // … and back
enum int (constant) int app.autoExitFrames = 120autoExitFrames = 120; // ≈ 2 s at 60 Hz
enum long (constant) long app.autoExitUsCap = 2500000LautoExitUsCap = 2_500_000; // wall-clock backstop
// -------------------------------------------------------------------- state
/// One wl_shm-backed ARGB8888 buffer. `busy` is owned by the compositor
/// between wl_surface.commit and the wl_buffer.release event.
struct (struct) app.BufferOne wl_shm-backed ARGB8888 buffer. busy is owned by the compositor
between wl_surface.commit and the wl_buffer.release event.
Buffer
{
wl_buffer* (field) _error_ app.Buffer.handlehandle;
uint* (field) uint* app.Buffer.pixelspixels; // mmap'ed, shared with the compositor
(alias) object.size_t = ulongsize_t (field) ulong app.Buffer.byteSizebyteSize;
int (field) int app.Buffer.widthwidth, (field) int app.Buffer.heightheight;
bool (field) bool app.Buffer.busybusy;
}
__gshared
{
wl_display* _error_ app.g_displayg_display;
wl_registry* _error_ app.g_registryg_registry;
wl_compositor* _error_ app.g_compositorg_compositor;
wl_shm* _error_ app.g_shmg_shm;
wl_seat* _error_ app.g_seatg_seat;
xdg_wm_base* _error_ app.g_wmBaseg_wmBase;
wl_surface* _error_ app.g_surfaceg_surface;
xdg_surface* _error_ app.g_xdgSurfaceg_xdgSurface;
xdg_toplevel* _error_ app.g_toplevelg_toplevel;
wl_callback* _error_ app.g_frameCbg_frameCb; // at most one outstanding frame callback
(struct) app.BufferOne wl_shm-backed ARGB8888 buffer. busy is owned by the compositor
between wl_surface.commit and the wl_buffer.release event.
Buffer[2] _error_ app.g_buffersg_buffers; // double buffering: paint one while the other is on screen
int (__gshared global) int app.g_widthg_width = defaultWidth; // last *acked* size — buffers must match it
int (__gshared global) int app.g_heightg_height = defaultHeight;
int (__gshared global) int app.g_pendingWidthg_pendingWidth; // from xdg_toplevel.configure (0 = client's choice)
int (__gshared global) int app.g_pendingHeightg_pendingHeight;
bool (__gshared global) bool app.g_configuredg_configured; // first configure acked (may attach buffers from now on)
bool (__gshared global) bool app.g_presentedg_presented; // first frame callback after the first buffer commit
bool (__gshared global) bool app.g_runningg_running = true;
bool (__gshared global) bool app.g_autoExitg_autoExit;
int (__gshared global) int app.g_framesg_frames; // frame callbacks received
int (__gshared global) int app.g_commitsg_commits; // buffer commits sent
}
// ---------------------------------------------------------- shm buffer pool
/// (Re)allocate `b` so it matches `w`×`h`. Only ever called on non-busy
/// buffers, so destroying a stale-sized wl_buffer here is race-free.
bool app.ensureBuffer(Re)allocate b so it matches w×h. Only ever called on non-busy
buffers, so destroying a stale-sized wl_buffer here is race-free.
ensureBuffer(ref (struct) app.BufferOne wl_shm-backed ARGB8888 buffer. busy is owned by the compositor
between wl_surface.commit and the wl_buffer.release event.
Buffer (parameter) Buffer bb, int (parameter) int ww, int (parameter) int hh) nothrow @nogc
{
if (b.b.handlehandle !is null && (b.b.widthwidth != w || b.b.heightheight != h))
{
wsi_buffer_destroy(b.b.handlehandle);
munmap(b.b.pixelspixels, b.b.byteSizebyteSize);
b = Buffer.init;
}
if (b.b.handlehandle !is null)
return true;
immutable _error_ stridestride = w * 4;
immutable _error_ sizesize = cast((unresolved type) size_tsize_t)(stride) * h;
immutable _error_ fdfd = memfd_create("wsi-scaffold", MFD_CLOEXEC);
if (fd < 0)
return false;
if (ftruncate(fd, cast(long) size) != 0)
{
close(fd);
return false;
}
void* _error_ memmem = mmap(null, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mem is cast(void*)-1)
{
close(fd);
return false;
}
instrStep("memfd_create+mmap");
// The pool can be destroyed right after create_buffer: the buffer keeps
// the backing memory alive, and the compositor has dup'ed the fd.
wl_shm_pool* _error_ poolpool = wsi_shm_create_pool(g_shm, fd, cast(int) size);
instrStep("wl_shm_create_pool");
b.b.handlehandle = wsi_shm_pool_create_buffer(pool, 0, w, h, stride, WL_SHM_FORMAT_ARGB8888);
instrStep("wl_shm_pool_create_buffer");
wsi_shm_pool_destroy(pool);
close(fd);
wsi_buffer_add_listener(b.b.handlehandle, &g_bufferListener, &b);
b.b.pixelspixels = cast(uint*) mem;
b.b.byteSizebyteSize = size;
b.b.widthwidth = w;
b.b.heightheight = h;
b.b.busybusy = false;
instrEvent("buffer_alloc", "size=%dx%d bytes=%zu", w, h, size);
return true;
}
/// Corner-anchored diagonal gradient (F02: geometry must visibly track the
/// window size) with a frame-driven blue channel so each redraw is observable.
void app.paintCorner-anchored diagonal gradient (F02: geometry must visibly track the
window size) with a frame-driven blue channel so each redraw is observable.
paint(ref (struct) app.BufferOne wl_shm-backed ARGB8888 buffer. busy is owned by the compositor
between wl_surface.commit and the wl_buffer.release event.
Buffer (parameter) Buffer bb, int frame) nothrow @nogc
{
immutable _error_ maxXmaxX = b.b.widthwidth > 1 ? b.b.widthwidth - 1 : 1;
immutable _error_ maxYmaxY = b.b.heightheight > 1 ? b.b.heightheight - 1 : 1;
immutable uint _error_ blueblue = (frame * 2) & 0xff;
foreach ((parameter) yy; 0 .. b.b.heightheight)
{
uint* _error_ rowrow = b.b.pixelspixels + cast((unresolved type) size_tsize_t) y * b.b.widthwidth;
immutable uint _error_ gg = cast(uint)(y * 255 / maxY) << 8;
foreach ((parameter) xx; 0 .. b.b.widthwidth)
row[x] = 0xff00_0000 | (cast(uint)(x * 255 / maxX) << 16) | g | blue;
}
}
/// Paint into a free buffer and commit it, requesting the next frame callback.
/// Called from the first configure (first pixel), from later size-changing
/// configures, and from every frame callback.
void void app.render() nothrow @nogcPaint into a free buffer and commit it, requesting the next frame callback.
Called from the first configure (first pixel), from later size-changing
configures, and from every frame callback.
render() nothrow @nogc
{
(struct) app.BufferOne wl_shm-backed ARGB8888 buffer. busy is owned by the compositor
between wl_surface.commit and the wl_buffer.release event.
Buffer* (local variable) _error_ bufbuf = null;
foreach (ref (parameter) bb; g_buffers)
if (!b.b.busybusy)
{
buf = &b;
break;
}
if (buf is null)
{
// Both buffers held by the compositor: drop this frame. The
// wl_buffer.release handler re-renders as soon as one comes back.
instrEvent("frame_skipped", "reason=all_buffers_busy");
return;
}
if (!ensureBuffer(*buf, g_width, g_height))
{
(__gshared global) bool app.g_runningg_running = false;
return;
}
paint(*buf, g_frames);
// F02 contract: the buffer we are about to commit matches the acked size.
assert(buf.(field) _error_ buf.widthwidth == (__gshared global) int app.g_widthg_width && buf.(field) _error_ buf.heightheight == (__gshared global) int app.g_heightg_height,
"committed buffer size does not match the acked configure size");
wsi_surface_attach(g_surface, buf.buf.handlehandle, 0, 0);
wsi_surface_damage_buffer(g_surface, 0, 0, buf.buf.widthwidth, buf.buf.heightheight);
if (g_frameCb is null) // keep exactly one frame callback in flight
{
g_frameCb = wsi_surface_frame(g_surface);
wsi_callback_add_listener(g_frameCb, &g_frameListener, null);
}
wsi_surface_commit(g_surface);
buf.buf.busybusy = true;
(__gshared global) int app.g_commitsg_commits++;
if ((__gshared global) int app.g_commitsg_commits == 1)
instrEvent("first_commit", "size=%dx%d", buf.buf.widthwidth, buf.buf.heightheight);
}
// ---------------------------------------------------------------- listeners
// All callbacks are `extern (C) nothrow @nogc` to match the listener
// function-pointer types the ImportC `#pragma attribute` stamped in c.c.
extern (C) void app.onGlobalonGlobal(void* data, wl_registry* reg, uint name,
const(char)* iface, uint ver) nothrow @nogc
{
static uint uint capped(uint advertised, uint want) nothrow @nogccapped(uint (parameter) uint advertisedadvertised, uint (parameter) uint wantwant) nothrow @nogc
{
return advertised < want ? advertised : want;
}
if (strcmp(iface, wl_compositor_interface.name) == 0)
g_compositor = cast(wl_compositor*) wsi_registry_bind(reg, name,
&wl_compositor_interface, capped(ver, 4)); // v4: wl_surface.damage_buffer
else if (strcmp(iface, wl_shm_interface.name) == 0)
g_shm = cast(wl_shm*) wsi_registry_bind(reg, name, &wl_shm_interface, 1);
else if (strcmp(iface, xdg_wm_base_interface.name) == 0)
g_wmBase = cast(xdg_wm_base*) wsi_registry_bind(reg, name, &xdg_wm_base_interface, 1);
else if (strcmp(iface, wl_seat_interface.name) == 0)
g_seat = cast(wl_seat*) wsi_registry_bind(reg, name, &wl_seat_interface,
capped(ver, 2)); // v2: name event
else
return;
instrEvent("step", "name=wl_registry_bind iface=%s version=%u", iface, ver);
}
extern (C) void app.onGlobalRemoveonGlobalRemove(void* data, wl_registry* reg, uint name) nothrow @nogc
{
}
extern (C) void app.onWmBasePingonWmBasePing(void* data, xdg_wm_base* (parameter) xdg_wm_base* bb, uint serial) nothrow @nogc
{
wsi_wm_base_pong(b, serial); // liveness check — answer or be killed
instrEvent("ping_pong", "serial=%u", serial);
}
extern (C) void app.onSeatCapabilitiesonSeatCapabilities(void* data, wl_seat* s, uint caps) nothrow @nogc
{
instrEvent("seat_capabilities", "caps=0x%x", caps);
}
extern (C) void app.onSeatNameonSeatName(void* data, wl_seat* s, const(char)* name) nothrow @nogc
{
instrEvent("seat_name", "name=%s", name);
}
/// xdg_toplevel.configure: the compositor *suggests* a size (0×0 = "you
/// pick"). Only latched here; it takes effect at xdg_surface.configure.
extern (C) void app.onToplevelConfigurexdg_toplevel.configure: the compositor suggests a size (0×0 = "you
pick"). Only latched here; it takes effect at xdg_surface.configure.
onToplevelConfigure(void* data, xdg_toplevel* t, int (parameter) int ww, int (parameter) int hh,
wl_array* states) nothrow @nogc
{
g_pendingWidth = w;
g_pendingHeight = h;
int _error_ maximizedmaximized = 0;
const _error_ startstart = cast(const(uint)*) states.data;
foreach ((parameter) ii; 0 .. states.states.sizesize / uint.sizeof)
if (start[i] == XDG_TOPLEVEL_STATE_MAXIMIZED)
maximized = 1;
instrEvent("xdg_toplevel_configure", "size=%dx%d maximized=%d", w, h, maximized);
}
/// xdg_surface.configure: the atomic "apply everything" event. Protocol order
/// proven in the WAYLAND_DEBUG trace: ack_configure(serial) *then* the commit
/// of a buffer matching the new size.
extern (C) void app.onXdgSurfaceConfigurexdg_surface.configure: the atomic "apply everything" event. Protocol order
proven in the WAYLAND_DEBUG trace: ack_configure(serial) then the commit
of a buffer matching the new size.
onXdgSurfaceConfigure(void* data, xdg_surface* s, uint serial) nothrow @nogc
{
immutable _error_ ww = g_pendingWidth > 0 ? g_pendingWidth : defaultWidth;
immutable _error_ hh = g_pendingHeight > 0 ? g_pendingHeight : defaultHeight;
instrEvent("configure", "serial=%u size=%dx%d", serial, w, h);
wsi_xdg_surface_ack_configure(s, serial);
instrStep("xdg_surface_ack_configure");
immutable _error_ resizedresized = w != g_width || h != g_height;
g_width = w;
g_height = h;
if (!g_configured)
{
g_configured = true;
instrFirstConfigure();
render(); // the first buffer commit — the window becomes visible
}
else if (resized)
{
instrResize(w, h, 1); // no fractional-scale binding in the scaffold
render(); // commit a matching buffer right away, don't wait a frame
}
}
extern (C) void app.onToplevelCloseonToplevelClose(void* data, xdg_toplevel* t) nothrow @nogc
{
instrCloseRequested();
g_running = false;
}
// Bound at xdg_wm_base v1, so these v4/v5 events never arrive; the listener
// struct generated from wayland-protocols 1.47 still has the slots to fill.
extern (C) void app.onToplevelConfigureBoundsonToplevelConfigureBounds(void* data, xdg_toplevel* t, int (parameter) int ww, int (parameter) int hh) nothrow @nogc
{
}
extern (C) void app.onToplevelWmCapabilitiesonToplevelWmCapabilities(void* data, xdg_toplevel* t, wl_array* caps) nothrow @nogc
{
}
/// wl_buffer.release: the compositor no longer reads the buffer; reuse it.
extern (C) void app.onBufferReleasewl_buffer.release: the compositor no longer reads the buffer; reuse it.
onBufferRelease(void* data, wl_buffer* (parameter) wl_buffer* bb) nothrow @nogc
{
auto _error_ bufbuf = cast((unresolved type) BufferBuffer*) data;
buf.buf.busybusy = false;
// If a frame was dropped because both buffers were busy, recover here.
if (g_running && g_configured && g_frameCb is null)
render();
}
/// wl_surface.frame `done`: the compositor presented the surface and is ready
/// for the next frame — the vsync-paced redraw driver.
extern (C) void app.onFrameDonewl_surface.frame done: the compositor presented the surface and is ready
for the next frame — the vsync-paced redraw driver.
onFrameDone(void* data, wl_callback* cb, uint timeMs) nothrow @nogc
{
wsi_callback_destroy(cb); // one-shot object
g_frameCb = null;
g_frames++;
instrFrameCallback(timeMs);
if (!g_presented)
{
g_presented = true;
// First pixel confirmed: a post-ack buffer commit has been presented.
instrFirstPixelPresented();
}
if (g_autoExit)
{
if (g_frames == maximizeAtFrame)
{
wsi_toplevel_set_maximized(g_toplevel);
instrStep("xdg_toplevel_set_maximized");
}
else if (g_frames == unmaximizeAtFrame)
{
wsi_toplevel_unset_maximized(g_toplevel);
instrStep("xdg_toplevel_unset_maximized");
}
if (g_frames >= autoExitFrames || instrNowUs() > autoExitUsCap)
{
g_running = false;
return;
}
}
render();
}
__gshared wl_registry_listener _error_ app.g_registryListenerg_registryListener = {&onGlobal, &onGlobalRemove};
__gshared xdg_wm_base_listener _error_ app.g_wmBaseListenerg_wmBaseListener = {&onWmBasePing};
__gshared wl_seat_listener _error_ app.g_seatListenerg_seatListener = {&onSeatCapabilities, &onSeatName};
__gshared xdg_surface_listener _error_ app.g_xdgSurfaceListenerg_xdgSurfaceListener = {&onXdgSurfaceConfigure};
__gshared xdg_toplevel_listener _error_ app.g_toplevelListenerg_toplevelListener = {
&onToplevelConfigure, &onToplevelClose,
&onToplevelConfigureBounds, &onToplevelWmCapabilities
};
__gshared wl_buffer_listener _error_ app.g_bufferListenerg_bufferListener = {&onBufferRelease};
__gshared wl_callback_listener _error_ app.g_frameListenerg_frameListener = {&onFrameDone};
// ----------------------------------------------------------------- teardown
void void app.teardown() nothrow @nogcteardown() nothrow @nogc
{
instrEvent("teardown_start");
foreach (ref (parameter) bb; g_buffers)
if (b.b.handlehandle !is null)
{
wsi_buffer_destroy(b.b.handlehandle);
munmap(b.b.pixelspixels, b.b.byteSizebyteSize);
b = Buffer.init;
}
if (g_frameCb !is null)
wsi_callback_destroy(g_frameCb);
if (g_toplevel !is null)
wsi_toplevel_destroy(g_toplevel);
if (g_xdgSurface !is null)
wsi_xdg_surface_destroy(g_xdgSurface);
if (g_surface !is null)
wsi_surface_destroy(g_surface);
if (g_wmBase !is null)
wsi_wm_base_destroy(g_wmBase);
// The remaining globals have no destructor request at the bound versions;
// destroying the client-side proxy is the correct cleanup.
if (g_seat !is null)
wl_proxy_destroy(cast(wl_proxy*) g_seat);
if (g_shm !is null)
wl_proxy_destroy(cast(wl_proxy*) g_shm);
if (g_compositor !is null)
wl_proxy_destroy(cast(wl_proxy*) g_compositor);
if (g_registry !is null)
wl_proxy_destroy(cast(wl_proxy*) g_registry);
wl_display_disconnect(g_display);
instrEvent("teardown_done");
}
// --------------------------------------------------------------------- main
int int D main()main()
{
instrInit("scaffold-wayland");
const (local variable) const(char*) autoEnvautoEnv = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv("WSI_AUTO_EXIT");
(__gshared global) bool app.g_autoExitg_autoExit = (local variable) const(char*) autoEnvautoEnv !is null && *(local variable) const(char*) autoEnvautoEnv == '1';
// 1. Connect (SKIP cleanly on headless hosts without a compositor).
g_display = wl_display_connect(null);
if (g_display is null)
{
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("SKIP: no Wayland compositor (wl_display_connect returned null)\n");
return 0;
}
instrStep("wl_display_connect");
// 2. Registry: discover and bind the four globals a window needs.
g_registry = wsi_display_get_registry(g_display);
instrStep("wl_display_get_registry");
wsi_registry_add_listener(g_registry, &g_registryListener, null);
wl_display_roundtrip(g_display); // blocks; onGlobal binds during this
instrStep("wl_display_roundtrip");
if (g_compositor is null || g_shm is null || g_wmBase is null)
{
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("SKIP: compositor lacks a required global (wl_compositor/wl_shm/xdg_wm_base)\n");
void app.teardown() nothrow @nogcteardown();
return 0;
}
wsi_wm_base_add_listener(g_wmBase, &g_wmBaseListener, null);
if (g_seat !is null)
wsi_seat_add_listener(g_seat, &g_seatListener, null);
// 3. Build the window object tree: wl_surface → xdg_surface → xdg_toplevel.
g_surface = wsi_compositor_create_surface(g_compositor);
instrStep("wl_compositor_create_surface");
g_xdgSurface = wsi_wm_base_get_xdg_surface(g_wmBase, g_surface);
instrStep("xdg_wm_base_get_xdg_surface");
wsi_xdg_surface_add_listener(g_xdgSurface, &g_xdgSurfaceListener, null);
g_toplevel = wsi_xdg_surface_get_toplevel(g_xdgSurface);
instrStep("xdg_surface_get_toplevel");
wsi_toplevel_add_listener(g_toplevel, &g_toplevelListener, null);
wsi_toplevel_set_title(g_toplevel, "wsi-scaffold");
instrStep("xdg_toplevel_set_title");
wsi_toplevel_set_app_id(g_toplevel, "wsi-scaffold");
instrStep("xdg_toplevel_set_app_id");
instrWindowCreated();
// 4. The mandatory initial commit *without* a buffer (attaching one before
// the first configure is a protocol error). The compositor answers with
// the first xdg_surface.configure; the first pixel happens in its handler.
wsi_surface_commit(g_surface);
instrStep("wl_surface_commit");
// 5. Event loop. wl_display_dispatch flushes requests, blocks on the
// socket, and dispatches a batch of events; the listeners above do all
// the work. Frame callbacks keep the loop ticking ~60 Hz.
while ((__gshared global) bool app.g_runningg_running && wl_display_dispatch(g_display) != -1)
{
if ((__gshared global) bool app.g_autoExitg_autoExit && instrNowUs() > (constant) long app.autoExitUsCap = 2500000LautoExitUsCap + 2_000_000)
(__gshared global) bool app.g_runningg_running = false; // hard backstop if frame callbacks ever stall
}
// 6. Clean teardown: buffers → frame callback → role objects → surface →
// globals → registry → disconnect (children before parents).
void app.teardown() nothrow @nogcteardown();
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("ok: presented %d frames in %d commits, final size %dx%d\n",
(__gshared global) int app.g_framesg_frames, (__gshared global) int app.g_commitsg_commits, (__gshared global) int app.g_widthg_width, (__gshared global) int app.g_heightg_height);
return 0;
}