// F04 vsync / frame pacing on Wayland (../../f04-frame-pacing.md): how steady
// is the native frame clock, and what happens to it when the window goes away?
//
// Wayland's "draw now, in sync with the display" primitive is the
// wl_surface.frame callback: request one before commit, and the compositor
// fires `done(callback_data)` when it is "a good time to start drawing a new
// frame". The demo drives a trivially cheap redraw (solid color flip, two
// alternating colors) from NOTHING but that callback — no sleep, no timer, no
// busy loop — and:
//
// 1. logs `frame_callback t=<callback_data>` for 600 consecutive measured
// frames, then computes min/median/p99/max inter-frame delta and a
// coarse 1 ms histogram (printed to stdout at exit). Deltas are measured
// on the client's own MonoTime µs clock; the callback's ms-resolution
// time argument is logged alongside for clock-identity comparison.
// 2. probes occlusion: requests xdg_toplevel.set_minimized, keeps a frame
// callback requested, and measures — against an independent poll(2)
// timeout clock, so a callback that never comes cannot deadlock the
// demo — how long the frame clock simply STOPS. After ~3 s it restores
// via set_maximized + a fresh attach/commit and logs the resumption gap.
// 3. dumps every registry global (`global iface=… version=…`), which
// answers whether wp_presentation (the protocol that adds real
// presentation timestamps to this "draw now" signal) is advertised.
//
// Headless caveat, prominently: under `weston --backend=headless` the repaint
// clock is synthetic (default 60 Hz; `--refresh-rate=N` overrides). The
// numbers characterize the protocol's delivery jitter, not hardware vsync.
//
// Event-loop note: instead of the scaffold's blocking wl_display_dispatch,
// this demo runs the libwayland prepare_read/poll/read_events/dispatch_pending
// pattern so the poll timeout doubles as the independent clock for phase 2.
//
// Based on the scaffold (../scaffold/app.d, findings ../../scaffold.md);
// instrumentation contract: ./instrument.d. 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, (alias) app.qsort = void core.stdc.stdlib.qsort(void* base, ulong nmemb, ulong size, extern (C) int function(const(void*), const(void*)) compar) nothrow @nogcqsort;
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.measuredFrames = 600measuredFrames = 600; // inter-frame deltas to collect (601 callbacks)
enum long (constant) long app.minimizedProbeUs = 3000000LminimizedProbeUs = 3_000_000; // hold the minimized state ~3 s
enum long (constant) long app.restoreTimeoutUs = 3000000LrestoreTimeoutUs = 3_000_000; // give up if no callback after restore
enum int (constant) int app.pollTimeoutMs = 50pollTimeoutMs = 50; // the independent clock's tick
enum long (constant) long app.hardCapUs = 30000000LhardCapUs = 30_000_000; // absolute wall-clock backstop
enum int (constant) int app.histBuckets = 40histBuckets = 40; // 1 ms histogram buckets: 0..38 ms + overflow
// -------------------------------------------------------------------- state
enum (enum) app.PhasePhase
{
(enum value) app.Phase.measure = 0measure, // collecting the 600 inter-frame deltas
(enum value) app.Phase.minimized = 1minimized, // set_minimized sent; counting the silence
(enum value) app.Phase.restoring = 2restoring, // set_maximized + commit sent; waiting for the clock to resume
(enum value) app.Phase.done = 3done,
}
/// 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;
int (__gshared global) int app.g_framesg_frames; // frame callbacks received, total
int (__gshared global) int app.g_commitsg_commits; // buffer commits sent
bool (__gshared global) bool app.g_sawPresentationg_sawPresentation; // wp_presentation in the registry?
(enum) app.PhasePhase (__gshared global) app.Phase app.g_phaseg_phase = Phase.Phase.measuremeasure;
// Phase 1: pacing measurement.
long[(constant) int app.measuredFrames = 600measuredFrames] (__gshared global) long[600] app.g_deltasUsg_deltasUs; // inter-callback deltas, client MonoTime µs
int (__gshared global) int app.g_deltaCountg_deltaCount;
long (__gshared global) long app.g_lastCbUsg_lastCbUs = -1; // arrival time of the previous callback (µs)
uint (__gshared global) uint app.g_lastCbArgMsg_lastCbArgMs; // previous callback's own time argument (ms)
uint (__gshared global) uint app.g_firstCbArgMsg_firstCbArgMs, (__gshared global) uint app.g_finalCbArgMsg_finalCbArgMs; // for the callback-arg clock identity
// Phase 2: occlusion probe (all µs on the client MonoTime clock).
long (__gshared global) long app.g_minimizeReqUsg_minimizeReqUs = -1; // when set_minimized was requested
long (__gshared global) long app.g_lastTickBeforeMinUsg_lastTickBeforeMinUs = -1; // last callback before the minimize
int (__gshared global) int app.g_cbWhileMinimizedg_cbWhileMinimized; // callbacks that fired during the silence
long (__gshared global) long app.g_restoreReqUsg_restoreReqUs = -1; // when set_maximized + commit was sent
long (__gshared global) long app.g_resumeCbUsg_resumeCbUs = -1; // first callback after the restore
}
// ---------------------------------------------------------- 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-f04", 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;
}
// 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);
b.b.handlehandle = wsi_shm_pool_create_buffer(pool, 0, w, h, stride, WL_SHM_FORMAT_ARGB8888);
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;
}
/// The trivially cheap frame: a full-window solid fill alternating between
/// two colors, so every presented frame is distinguishable but costs only a
/// memset-grade loop.
void app.paintThe trivially cheap frame: a full-window solid fill alternating between
two colors, so every presented frame is distinguishable but costs only a
memset-grade loop.
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 uint _error_ colorcolor = (frame & 1) ? 0xff20_6080 : 0xff80_4020;
immutable _error_ nn = cast((unresolved type) size_tsize_t) b.b.widthwidth * b.b.heightheight;
foreach ((parameter) ii; 0 .. n)
b.b.pixelspixels[i] = color;
}
/// Paint into a free buffer and commit it, requesting the next frame callback.
void void app.render() nothrow @nogcPaint into a free buffer and commit it, requesting the next 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);
void app.requestFrameCallback() nothrow @nogcrequestFrameCallback();
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);
}
void void app.requestFrameCallback() nothrow @nogcrequestFrameCallback() nothrow @nogc
{
if (g_frameCb !is null) // keep exactly one in flight
return;
g_frameCb = wsi_surface_frame(g_surface);
wsi_callback_add_listener(g_frameCb, &g_frameListener, null);
}
// ---------------------------------------------------------------- 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;
}
// Full registry dump: F04 needs to know whether wp_presentation — the
// protocol that upgrades "draw now" to real presentation timestamps — is
// among the advertised globals.
instrEvent("global", "iface=%s version=%u", iface, ver);
if (strcmp(iface, "wp_presentation") == 0)
g_sawPresentation = true;
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
}
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
}
extern (C) void app.onSeatCapabilitiesonSeatCapabilities(void* data, wl_seat* s, uint caps) nothrow @nogc
{
}
extern (C) void app.onSeatNameonSeatName(void* data, wl_seat* s, const(char)* name) nothrow @nogc
{
}
/// xdg_toplevel.configure: latch the suggested size; applied at
/// xdg_surface.configure.
extern (C) void app.onToplevelConfigurexdg_toplevel.configure: latch the suggested size; applied 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;
instrEvent("xdg_toplevel_configure", "size=%dx%d", w, h);
}
extern (C) void app.onXdgSurfaceConfigureonXdgSurfaceConfigure(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);
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 && g_phase != Phase.Phase.minimizedminimized)
{
instrResize(w, h, 1);
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 && g_phase != Phase.Phase.minimizedminimized)
render();
}
/// wl_surface.frame `done` — the frame clock tick under measurement.
/// `timeMs` is the callback's own time argument: milliseconds, "with an
/// undefined base" per the spec — its clock identity is an F04 finding.
extern (C) void app.onFrameDonewl_surface.frame done — the frame clock tick under measurement.
timeMs is the callback's own time argument: milliseconds, "with an
undefined base" per the spec — its clock identity is an F04 finding.
onFrameDone(void* data, wl_callback* cb, uint timeMs) nothrow @nogc
{
wsi_callback_destroy(cb); // one-shot object
g_frameCb = null;
g_frames++;
immutable _error_ nownow = instrNowUs();
instrEvent("frame_callback", "t=%u", timeMs);
if (!g_presented)
{
g_presented = true;
g_firstCbArgMs = timeMs;
instrFirstPixelPresented();
}
final switch (g_phase)
{
case Phase.Phase.measuremeasure:
if (g_lastCbUs >= 0 && g_deltaCount < measuredFrames)
{
g_deltasUs[g_deltaCount] = now - g_lastCbUs;
g_deltaCount++;
}
g_lastCbUs = now;
g_lastCbArgMs = timeMs;
if (g_deltaCount >= measuredFrames)
{
// Phase 1 complete → start the occlusion probe. Keep a frame
// callback requested across the minimize so any tick the
// compositor did deliver to a minimized surface would be seen.
g_finalCbArgMs = timeMs;
g_lastTickBeforeMinUs = now;
g_phase = Phase.Phase.minimizedminimized;
g_minimizeReqUs = now;
wsi_toplevel_set_minimized(g_toplevel);
requestFrameCallback();
wsi_surface_commit(g_surface); // commit makes the frame request current
instrEvent("vis_change", "state=minimized_requested");
}
else
render();
break;
case Phase.Phase.minimizedminimized:
// The probe's point: does this ever fire while minimized?
g_cbWhileMinimized++;
instrEvent("frame_callback_while_minimized", "t=%u n=%d", timeMs, g_cbWhileMinimized);
requestFrameCallback();
wsi_surface_commit(g_surface);
break;
case Phase.Phase.restoringrestoring:
g_resumeCbUs = now;
instrEvent("vis_change", "state=restored resume_after_us=%lld silence_us=%lld",
now - g_restoreReqUs, now - g_lastTickBeforeMinUs);
g_phase = Phase.Phase.donedone;
g_running = false;
break;
case Phase.Phase.donedone:
break;
}
}
__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};
// ----------------------------------------------- poll-based event pump
// The libwayland prepare_read/read_events pattern (thread-safe variant of
// wl_display_dispatch) with a poll(2) timeout — the timeout is the
// *independent clock* that lets phase 2 measure a frame clock that has
// stopped without deadlocking on a callback that never comes.
/// Pump once: dispatch pending events, flush, wait up to `timeoutMs`.
/// Returns false on connection error.
bool bool app.pumpOnce(int timeoutMs) nothrow @nogcPump once: dispatch pending events, flush, wait up to timeoutMs.
Returns false on connection error.
pumpOnce(int (parameter) int timeoutMstimeoutMs) nothrow @nogc
{
while (wl_display_prepare_read(g_display) != 0)
if (wl_display_dispatch_pending(g_display) < 0)
return false;
wl_display_flush(g_display);
pollfd (local variable) _error_ pfdpfd;
pfd.pfd.fdfd = wl_display_get_fd(g_display);
pfd.events = POLLIN;
immutable (local variable) immutable(_error_) rr = poll(&pfd, 1, timeoutMs);
if (r <= 0) // timeout (the independent clock ticked) or error
{
wl_display_cancel_read(g_display);
return r == 0;
}
if (wl_display_read_events(g_display) < 0)
return false;
return wl_display_dispatch_pending(g_display) >= 0;
}
// ------------------------------------------------------------------ stats
extern (C) int int app.cmpLong(const(void)* a, const(void)* b) nothrow @nogccmpLong(const(void)* (parameter) const(void)* aa, const(void)* (parameter) const(void)* bb) nothrow @nogc
{
immutable (local variable) immutable(long) xx = *cast(const(long)*) (parameter) const(void)* aa;
immutable (local variable) immutable(long) yy = *cast(const(long)*) (parameter) const(void)* bb;
return ((local variable) immutable(long) xx > (local variable) immutable(long) yy) - ((local variable) immutable(long) xx < (local variable) immutable(long) yy);
}
/// min/median/p99/max + 1 ms histogram over the recorded deltas, to stdout.
void void app.printStats() nothrow @nogcmin/median/p99/max + 1 ms histogram over the recorded deltas, to stdout.
printStats() nothrow @nogc
{
if ((__gshared global) int app.g_deltaCountg_deltaCount == 0)
{
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("stats: no deltas recorded\n");
return;
}
void core.stdc.stdlib.qsort(void* base, ulong nmemb, ulong size, extern (C) int function(const(void*), const(void*)) compar) nothrow @nogcqsort((__gshared global) long[600] app.g_deltasUsg_deltasUs.(constant) long* long[600].ptr = &g_deltasUsptr, (__gshared global) int app.g_deltaCountg_deltaCount, long.(constant) ulong long.sizeof = 8LUsizeof, &int app.cmpLong(const(void)* a, const(void)* b) nothrow @nogccmpLong);
immutable long (local variable) immutable(long) minmin = (__gshared global) long[600] app.g_deltasUsg_deltasUs[0];
immutable long (local variable) immutable(long) maxmax = (__gshared global) long[600] app.g_deltasUsg_deltasUs[(__gshared global) int app.g_deltaCountg_deltaCount - 1];
immutable long (local variable) immutable(long) medianmedian = (__gshared global) long[600] app.g_deltasUsg_deltasUs[(__gshared global) int app.g_deltaCountg_deltaCount / 2];
immutable (alias) object.size_t = ulongsize_t (local variable) immutable(ulong) p99Idxp99Idx = cast((alias) object.size_t = ulongsize_t)((__gshared global) int app.g_deltaCountg_deltaCount - 1) * 99 / 100;
immutable long (local variable) immutable(long) p99p99 = (__gshared global) long[600] app.g_deltasUsg_deltasUs[(local variable) immutable(ulong) p99Idxp99Idx];
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("stats source=monotonic_us n=%d min_us=%lld median_us=%lld p99_us=%lld max_us=%lld\n",
(__gshared global) int app.g_deltaCountg_deltaCount, (local variable) immutable(long) minmin, (local variable) immutable(long) medianmedian, (local variable) immutable(long) p99p99, (local variable) immutable(long) maxmax);
// The callback's own ms-resolution argument, as a cross-check of the
// measurement window: (last - first) / n ≈ the same mean period.
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("stats source=callback_arg_ms first=%u last=%u span_ms=%u mean_period_ms=%.3f\n",
(__gshared global) uint app.g_firstCbArgMsg_firstCbArgMs, (__gshared global) uint app.g_finalCbArgMsg_finalCbArgMs, (__gshared global) uint app.g_finalCbArgMsg_finalCbArgMs - (__gshared global) uint app.g_firstCbArgMsg_firstCbArgMs,
cast(double)((__gshared global) uint app.g_finalCbArgMsg_finalCbArgMs - (__gshared global) uint app.g_firstCbArgMsg_firstCbArgMs) / (__gshared global) int app.g_deltaCountg_deltaCount);
int[(constant) int app.histBuckets = 40histBuckets] (local variable) int[40] histhist;
foreach ((local variable) int ii; 0 .. (__gshared global) int app.g_deltaCountg_deltaCount)
{
auto (local variable) long bucketbucket = (__gshared global) long[600] app.g_deltasUsg_deltasUs[(local variable) int ii] / 1000; // 1 ms buckets
if ((local variable) long bucketbucket >= (constant) int app.histBuckets = 40histBuckets)
(local variable) long bucketbucket = (constant) int app.histBuckets = 40histBuckets - 1;
(local variable) int[40] histhist[cast((alias) object.size_t = ulongsize_t) (local variable) long bucketbucket]++;
}
foreach ((local variable) int ii; 0 .. (constant) int app.histBuckets = 40histBuckets)
if ((local variable) int[40] histhist[(local variable) int ii] != 0)
{
if ((local variable) int ii == (constant) int app.histBuckets = 40histBuckets - 1)
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("hist bucket_ms=%d+ count=%d\n", (local variable) int ii, (local variable) int[40] histhist[(local variable) int ii]);
else
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("hist bucket_ms=%d-%d count=%d ", (local variable) int ii, (local variable) int ii + 1, (local variable) int[40] histhist[(local variable) int ii]);
if ((local variable) int ii < (constant) int app.histBuckets = 40histBuckets - 1)
{
foreach ((local variable) int __; 0 .. (local variable) int[40] histhist[(local variable) int ii] / 10 + 1)
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("#");
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("\n");
}
}
}
// ----------------------------------------------------------------- teardown
void void app.teardown() nothrow @nogcteardown() nothrow @nogc
{
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);
}
// --------------------------------------------------------------------- main
int int D main()main()
{
instrInit("f04_wayland");
// The pacing demo is meaningless without its bounded run; WSI_AUTO_EXIT
// is accepted for runner symmetry but the demo always auto-terminates.
cast(void) char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv("WSI_AUTO_EXIT");
// 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;
}
// 2. Registry: dump every global (the wp_presentation question) and bind
// the ones a window needs.
g_registry = wsi_display_get_registry(g_display);
wsi_registry_add_listener(g_registry, &g_registryListener, null);
wl_display_roundtrip(g_display); // blocks; onGlobal binds during this
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. Window object tree: wl_surface → xdg_surface → xdg_toplevel.
g_surface = wsi_compositor_create_surface(g_compositor);
g_xdgSurface = wsi_wm_base_get_xdg_surface(g_wmBase, g_surface);
wsi_xdg_surface_add_listener(g_xdgSurface, &g_xdgSurfaceListener, null);
g_toplevel = wsi_xdg_surface_get_toplevel(g_xdgSurface);
wsi_toplevel_add_listener(g_toplevel, &g_toplevelListener, null);
wsi_toplevel_set_title(g_toplevel, "wsi-f04-frame-pacing");
wsi_toplevel_set_app_id(g_toplevel, "wsi-f04-frame-pacing");
instrWindowCreated();
// 4. The mandatory initial no-buffer commit; first pixel happens in the
// configure handler.
wsi_surface_commit(g_surface);
// 5. Poll-pumped event loop. The poll timeout is the independent clock:
// when the frame clock stops (phase 2), the loop still ticks every
// pollTimeoutMs and the wall-clock checks below run.
while ((__gshared global) bool app.g_runningg_running)
{
if (!bool app.pumpOnce(int timeoutMs) nothrow @nogcPump once: dispatch pending events, flush, wait up to timeoutMs.
Returns false on connection error.
pumpOnce((constant) int app.pollTimeoutMs = 50pollTimeoutMs))
break;
immutable (local variable) immutable(_error_) nownow = instrNowUs();
if ((__gshared global) app.Phase app.g_phaseg_phase == (enum) app.PhasePhase.(enum value) app.Phase.minimized = 1minimized && now - (__gshared global) long app.g_minimizeReqUsg_minimizeReqUs >= minimizedProbeUs)
{
// ~3 s of silence measured — restore. xdg-shell has no
// unset_minimized; un-minimizing is the compositor's (user's)
// call. set_maximized + a fresh attach/commit is the strongest
// client-side "make me visible again" available.
(__gshared global) app.Phase app.g_phaseg_phase = (enum) app.PhasePhase.(enum value) app.Phase.restoring = 2restoring;
(__gshared global) long app.g_restoreReqUsg_restoreReqUs = now;
instrEvent("vis_change",
"state=restore_requested minimized_for_us=%lld cb_while_minimized=%d",
now - g_minimizeReqUs, g_cbWhileMinimized);
wsi_toplevel_set_maximized(g_toplevel);
void app.render() nothrow @nogcPaint into a free buffer and commit it, requesting the next frame callback.
render(); // fresh attach + damage + frame request + commit
}
else if ((__gshared global) app.Phase app.g_phaseg_phase == (enum) app.PhasePhase.(enum value) app.Phase.restoring = 2restoring && now - (__gshared global) long app.g_restoreReqUsg_restoreReqUs >= restoreTimeoutUs)
{
instrEvent("vis_change", "state=restore_timeout silence_us=%lld",
now - g_lastTickBeforeMinUs);
(__gshared global) bool app.g_runningg_running = false;
}
if (now > hardCapUs)
{
instrEvent("hard_cap_hit");
(__gshared global) bool app.g_runningg_running = false;
}
}
// 6. Teardown + the numbers.
void app.teardown() nothrow @nogcteardown();
void app.printStats() nothrow @nogcmin/median/p99/max + 1 ms histogram over the recorded deltas, to stdout.
printStats();
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("occlusion: minimized_for_us=%lld frame_callbacks_while_minimized=%d ",
(__gshared global) long app.g_restoreReqUsg_restoreReqUs >= 0 ? (__gshared global) long app.g_restoreReqUsg_restoreReqUs - (__gshared global) long app.g_minimizeReqUsg_minimizeReqUs : -1, (__gshared global) int app.g_cbWhileMinimizedg_cbWhileMinimized);
if ((__gshared global) long app.g_resumeCbUsg_resumeCbUs >= 0)
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("resumed_after_restore_us=%lld total_silence_us=%lld\n",
(__gshared global) long app.g_resumeCbUsg_resumeCbUs - (__gshared global) long app.g_restoreReqUsg_restoreReqUs, (__gshared global) long app.g_resumeCbUsg_resumeCbUs - (__gshared global) long app.g_lastTickBeforeMinUsg_lastTickBeforeMinUs);
else
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("resume=NEVER\n");
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("wp_presentation advertised: %s\n", (__gshared global) bool app.g_sawPresentationg_sawPresentation ? "yes".(constant) immutable(char)* "yes".ptr = "yes"ptr : "no".(constant) immutable(char)* "no".ptr = "no"ptr);
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("ok: %d frame callbacks, %d commits, %d measured deltas\n",
(__gshared global) int app.g_framesg_frames, (__gshared global) int app.g_commitsg_commits, (__gshared global) int app.g_deltaCountg_deltaCount);
return (__gshared global) int app.g_deltaCountg_deltaCount == (constant) int app.measuredFrames = 600measuredFrames ? 0 : 1;
}