// F17 Wayland demo — threading probes: exercise (and deliberately violate)
// libwayland's threading model and record exactly what happens
// (../../../features/f17-threading.md; findings in ../../f17-threading.md).
//
// libwayland-client documents wl_display as thread-safe: any thread may issue
// requests, and events are routed to wl_event_queue objects that threads
// dispatch independently (wl_display_create_queue / wl_proxy_set_queue /
// wl_display_dispatch_queue). The read-intent protocol
// (wl_display_prepare_read_queue → read_events|cancel_read) serializes the
// one socket read among any number of reader threads. These probes measure
// that story instead of trusting it.
//
// Probes (--probe=N; no argument = fork+run every probe TWICE, the CI
// default, so race-dependent outcomes show their spread):
// 1 the whole window — registry binds, surface tree, first commit, frame
// dispatch — built on a WORKER thread against a wl_display that the
// main thread connected, while main sleeps (no main-thread rule?)
// 2 TWO threads both calling wl_display_dispatch on the same default
// queue concurrently while frame callbacks keep events flowing — the
// documented multi-reader shape; how do events distribute?
// 3 the designed pattern: a worker-owned wl_event_queue. The worker makes
// a wl_proxy_create_wrapper of the display, wl_proxy_set_queue's it,
// chains 50 wl_display.sync callbacks through its own queue via
// wl_display_dispatch_queue — while main dispatches the default queue
// and renders. Prove both run concurrently.
// 4 render thread: a worker paints the wl_shm buffer and issues
// attach/damage/frame/commit (+ its own wl_display_flush) on the SHARED
// wl_surface proxy for 100 frames while main dispatches; watch for
// protocol errors / corruption.
// 5 one wl_display connection per thread — the X11 display-per-thread
// analog; trivially safe, prove it.
// 6 read-intent protocol violated: thread A holds a successful
// wl_display_prepare_read while thread B calls wl_display_read_events
// WITHOUT its own prepare; then a health roundtrip. Timeboxed.
//
// Every probe ends in a verdict line
// probe n=<N> result=ok|error|crash|deadlock|silent detail=...
// that survives ANY outcome: wl_display_get_error is checked after every
// probe, SIGSEGV/SIGABRT/SIGBUS handlers turn crashes into a flushed verdict
// + _exit(0), and a SIGALRM watchdog turns hangs into result=deadlock.
// Crash probes still exit 0 — crashing is their job.
//
// All probes are self-bounded (frame counts + wall-clock caps + the
// watchdog); WSI_AUTO_EXIT=1 is accepted for uniformity with the other
// Wayland demos but changes nothing.
//
// Headless-safe: no compositor -> prints `SKIP:` and exits 0.
module (module) appapp;
import c; // ImportC: <wayland-client.h> + xdg-shell glue + wsi_* wrappers
import instrument;
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.atomicOp = core.atomic.atomicOp(string op, T, V1)(ref shared T val, V1 mod) if (__traits(compiles, mixin("*cast(T*)&val" ~ op ~ "mod")))Performs the binary operation 'op' on val using 'mod' as the modifier.
atomicOp, (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.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, (alias) app.snprintf = int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf;
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;
// glibc's <pthread.h> is not ImportC-able (linux/types.h __int128), so the
// thread API comes from druntime's POSIX declarations (see f05-loop-wakeup).
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(module) core.sys.posix.pthreadD header file for POSIX.
pthread : (alias) app.pthread_create = int core.sys.posix.pthread.pthread_create(ulong*, scope const(core.sys.posix.sys.types.pthread_attr_t*), extern (C) void* function(void*), void*) nothrow @nogcpthread_create, (alias) app.pthread_join = int core.sys.posix.pthread.pthread_join(ulong, void**) nothrow @nogcpthread_join, (alias) app.pthread_self = ulong core.sys.posix.pthread.pthread_self() nothrow @nogcpthread_self, pthread_t;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(module) core.sys.posix.signalD header file for POSIX.
Source
core/sys/posix/signal.d
signal : (alias) app.sigaction = int core.sys.posix.signal.sigaction(int, scope const(core.sys.posix.signal.sigaction_t*), core.sys.posix.signal.sigaction_t*) nothrow @nogcsigaction, (struct) core.sys.posix.signal.sigaction_tsigaction_t, (alias) app.sigemptyset = int core.sys.posix.signal.sigemptyset(core.sys.posix.signal.sigset_t*) nothrow @nogcsigemptyset,
(alias constant) app.SIGABRT = int core.stdc.signal.SIGABRT = 6SIGABRT, (alias constant) app.SIGALRM = int core.sys.posix.signal.SIGALRM = 14SIGALRM, (alias constant) app.SIGBUS = int core.sys.posix.signal.SIGBUS = 7SIGBUS, (alias constant) app.SIGSEGV = int core.stdc.signal.SIGSEGV = 11SIGSEGV;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(package) core.sys.posix.syssys.(module) core.sys.posix.sys.waitD header file for POSIX.
wait : (alias) app.waitpid = int core.sys.posix.sys.wait.waitpid(int, int*, int) nothrow @nogcwaitpid;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(module) core.sys.posix.unistdD header file for POSIX.
unistd : (alias) app._exit = noreturn core.sys.posix.unistd._exit(int) nothrow @nogc @trusted_exit, (alias) app.alarm = uint core.sys.posix.unistd.alarm(uint) nothrow @nogc @trustedalarm, (alias) app.fork = int core.sys.posix.unistd.fork() nothrow @nogc @trustedfork, (alias) app.usleep = int core.sys.posix.unistd.usleep(uint) nothrow @nogc @trustedusleep, (alias) app.write = long core.sys.posix.unistd.write(int, scope const(void*), ulong) nothrow @nogcwrite;
enum int (constant) int app.defaultWidth = 640defaultWidth = 640;
enum int (constant) int app.defaultHeight = 480defaultHeight = 480;
// -- verdict plumbing: must survive crashes ------------------------------------
private __gshared int (__gshared global) int app.g_probeg_probe;
private shared bool (shared global) shared(bool) app.g_verdictDoneg_verdictDone;
private __gshared const(char)* (__gshared global) const(char)* app.g_watchdogDetailg_watchdogDetail = "watchdog_timeout_12s";
/// The one line the spec requires per run. Async-signal-safe on purpose
/// (snprintf into a static buffer + write(2)) so the signal handlers can
/// call it; the normal path uses it too so the format is identical.
void void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict(scope const(char)* (parameter) const(char)* resultresult, scope const(char)* (parameter) const(char)* detaildetail) @nogc nothrow
{
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.g_verdictDoneg_verdictDone))
return;
void core.atomic.atomicStore!(MemoryOrder.seq, bool, bool)(ref shared(bool) val, bool newval) pure nothrow @nogc @trustedWrites '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((shared global) shared(bool) app.g_verdictDoneg_verdictDone, true);
static __gshared char[320] (__gshared global) char[320] app.verdict.bufbuf;
const (local variable) const(_error_) nn = int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf((__gshared global) char[320] app.verdict.bufbuf.(constant) char* char[320].ptr = &bufptr, (__gshared global) char[320] app.verdict.bufbuf.(constant) ulong char[320].length = 320LUlength, "%lld f17_wayland probe n=%d result=%s detail=%s\n",
nowUs(), (__gshared global) int app.g_probeg_probe, (parameter) const(char)* resultresult, (parameter) const(char)* detaildetail);
long core.sys.posix.unistd.write(int, scope const(void*), ulong) nothrow @nogcwrite(2, (__gshared global) char[320] app.verdict.bufbuf.(constant) char* char[320].ptr = &bufptr, n);
}
extern (C) void void app.signalHandler(int sig) nothrow @nogcsignalHandler(int (parameter) int sigsig) @nogc nothrow
{
static __gshared char[64] (__gshared global) char[64] app.signalHandler.dd;
int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf((__gshared global) char[64] app.signalHandler.dd.(constant) char* char[64].ptr = &dptr, (__gshared global) char[64] app.signalHandler.dd.(constant) ulong char[64].length = 64LUlength, "fatal_signal=%d", (parameter) int sigsig);
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict("crash", (__gshared global) char[64] app.signalHandler.dd.(constant) char* char[64].ptr = &dptr);
noreturn core.sys.posix.unistd._exit(int) nothrow @nogc @trusted_exit(0);
}
extern (C) void void app.alarmHandler(int __param_0) nothrow @nogcalarmHandler(int) @nogc nothrow
{
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict("deadlock", (__gshared global) const(char)* app.g_watchdogDetailg_watchdogDetail);
noreturn core.sys.posix.unistd._exit(int) nothrow @nogc @trusted_exit(0);
}
void void app.installCrashHandlers() nothrow @nogcinstallCrashHandlers() @nogc nothrow
{
(struct) core.sys.posix.signal.sigaction_tsigaction_t (local variable) core.sys.posix.signal.sigaction_t sasa;
(local variable) core.sys.posix.signal.sigaction_t sasa.(field) extern (C) void function(int) core.sys.posix.signal.sigaction_t.sa_handlersa_handler = &void app.signalHandler(int sig) nothrow @nogcsignalHandler;
int core.sys.posix.signal.sigemptyset(core.sys.posix.signal.sigset_t*) nothrow @nogcsigemptyset(&(local variable) core.sys.posix.signal.sigaction_t sasa.(field) core.sys.posix.signal.sigset_t core.sys.posix.signal.sigaction_t.sa_masksa_mask);
(local variable) core.sys.posix.signal.sigaction_t sasa.(field) int core.sys.posix.signal.sigaction_t.sa_flagssa_flags = 0;
int core.sys.posix.signal.sigaction(int, scope const(core.sys.posix.signal.sigaction_t*), core.sys.posix.signal.sigaction_t*) nothrow @nogcsigaction((constant) int core.stdc.signal.SIGSEGV = 11SIGSEGV, &(local variable) core.sys.posix.signal.sigaction_t sasa, null);
int core.sys.posix.signal.sigaction(int, scope const(core.sys.posix.signal.sigaction_t*), core.sys.posix.signal.sigaction_t*) nothrow @nogcsigaction((constant) int core.sys.posix.signal.SIGBUS = 7SIGBUS, &(local variable) core.sys.posix.signal.sigaction_t sasa, null);
int core.sys.posix.signal.sigaction(int, scope const(core.sys.posix.signal.sigaction_t*), core.sys.posix.signal.sigaction_t*) nothrow @nogcsigaction((constant) int core.stdc.signal.SIGABRT = 6SIGABRT, &(local variable) core.sys.posix.signal.sigaction_t sasa, null);
(local variable) core.sys.posix.signal.sigaction_t sasa.(field) extern (C) void function(int) core.sys.posix.signal.sigaction_t.sa_handlersa_handler = &void app.alarmHandler(int __param_0) nothrow @nogcalarmHandler;
int core.sys.posix.signal.sigaction(int, scope const(core.sys.posix.signal.sigaction_t*), core.sys.posix.signal.sigaction_t*) nothrow @nogcsigaction((constant) int core.sys.posix.signal.SIGALRM = 14SIGALRM, &(local variable) core.sys.posix.signal.sigaction_t sasa, null);
uint core.sys.posix.unistd.alarm(uint) nothrow @nogc @trustedalarm(12); // the deadlock watchdog
}
// -- window machinery (the scaffold, re-parameterized on a Ctx*) ---------------
/// One wl_shm ARGB8888 buffer. `busy` is owned by the compositor between
/// wl_surface.commit and wl_buffer.release — and in probe 4 it crosses
/// threads (release lands on the dispatching main thread, the render worker
/// polls it), hence atomic.
struct (struct) app.BufferOne wl_shm ARGB8888 buffer. busy is owned by the compositor between
wl_surface.commit and wl_buffer.release — and in probe 4 it crosses
threads (release lands on the dispatching main thread, the render worker
polls it), hence atomic.
Buffer
{
wl_buffer* (field) _error_ app.Buffer.handlehandle;
uint* (field) uint* app.Buffer.pixelspixels;
(alias) object.size_t = ulongsize_t (field) ulong app.Buffer.byteSizebyteSize;
int (field) int app.Buffer.widthwidth, (field) int app.Buffer.heightheight;
shared bool (field) shared(bool) app.Buffer.busybusy;
}
/// Everything one connection-plus-window owns. Probes share one global Ctx,
/// except probe 5, which gives each thread its own.
struct (struct) app.CtxEverything one connection-plus-window owns. Probes share one global Ctx,
except probe 5, which gives each thread its own.
Ctx
{
const(char)* (field) const(char)* app.Ctx.tagtag = "main";
wl_display* (field) _error_ app.Ctx.displaydisplay;
wl_registry* (field) _error_ app.Ctx.registryregistry;
wl_compositor* (field) _error_ app.Ctx.compositorcompositor;
wl_shm* (field) _error_ app.Ctx.shmshm;
xdg_wm_base* (field) _error_ app.Ctx.wmBasewmBase;
wl_surface* (field) _error_ app.Ctx.surfacesurface;
xdg_surface* (field) _error_ app.Ctx.xdgSurfacexdgSurface;
xdg_toplevel* (field) _error_ app.Ctx.topleveltoplevel;
wl_callback* (field) _error_ app.Ctx.frameCbframeCb;
(struct) app.BufferOne wl_shm ARGB8888 buffer. busy is owned by the compositor between
wl_surface.commit and wl_buffer.release — and in probe 4 it crosses
threads (release lands on the dispatching main thread, the render worker
polls it), hence atomic.
Buffer[2] (field) _error_ app.Ctx.buffersbuffers;
int (field) int app.Ctx.widthwidth = (constant) int app.defaultWidth = 640defaultWidth;
int (field) int app.Ctx.heightheight = (constant) int app.defaultHeight = 480defaultHeight;
int (field) int app.Ctx.pendingWpendingW, (field) int app.Ctx.pendingHpendingH;
bool (field) bool app.Ctx.configuredconfigured;
bool (field) bool app.Ctx.autoRenderautoRender = true; // frame callback re-renders (probes 1,2,3,5)
shared bool (field) shared(bool) app.Ctx.runningrunning = true;
shared int (field) shared(int) app.Ctx.framesframes; // frame callbacks received (any thread)
int (field) int app.Ctx.commitscommits;
}
bool app.ensureBufferensureBuffer((struct) app.CtxEverything one connection-plus-window owns. Probes share one global Ctx,
except probe 5, which gives each thread its own.
Ctx* (parameter) Ctx* ctxctx, ref (struct) app.BufferOne wl_shm ARGB8888 buffer. busy is owned by the compositor between
wl_surface.commit and wl_buffer.release — and in probe 4 it crosses
threads (release lands on the dispatching main thread, the render worker
polls it), hence atomic.
Buffer (parameter) Buffer bb, int w, int h) @nogc nothrow
{
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-f17", 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;
}
wl_shm_pool* _error_ poolpool = wsi_shm_create_pool(ctx.ctx.shmshm, 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;
atomicStore(b.b.busybusy, false);
return true;
}
/// Solid fill keyed to the frame counter — cheap, and each redraw observable.
void app.paintSolid fill keyed to the frame counter — cheap, and each redraw observable.
paint(ref (struct) app.BufferOne wl_shm ARGB8888 buffer. busy is owned by the compositor between
wl_surface.commit and wl_buffer.release — and in probe 4 it crosses
threads (release lands on the dispatching main thread, the render worker
polls it), hence atomic.
Buffer (parameter) Buffer bb, int (parameter) int frameframe) @nogc nothrow
{
immutable uint _error_ colorcolor = 0xff00_0000 | ((frame * 2 & 0xff) << 16)
| ((255 - (frame * 2 & 0xff)) << 8) | 0x40;
foreach ((parameter) ii; 0 .. cast((unresolved type) size_tsize_t) b.b.widthwidth * b.b.heightheight)
b.b.pixelspixels[i] = color;
}
(struct) app.BufferOne wl_shm ARGB8888 buffer. busy is owned by the compositor between
wl_surface.commit and wl_buffer.release — and in probe 4 it crosses
threads (release lands on the dispatching main thread, the render worker
polls it), hence atomic.
Buffer* app.freeBufferfreeBuffer((struct) app.CtxEverything one connection-plus-window owns. Probes share one global Ctx,
except probe 5, which gives each thread its own.
Ctx* (parameter) Ctx* ctxctx) @nogc nothrow
{
foreach (ref (parameter) bb; ctx.ctx.buffersbuffers)
if (!atomicLoad(b.b.busybusy))
return &b;
return null;
}
/// Paint + attach/damage/commit (+ at most one frame callback in flight).
/// Runs on whichever thread the probe says — that is the experiment.
bool app.renderPaint + attach/damage/commit (+ at most one frame callback in flight).
Runs on whichever thread the probe says — that is the experiment.
render((struct) app.CtxEverything one connection-plus-window owns. Probes share one global Ctx,
except probe 5, which gives each thread its own.
Ctx* (parameter) Ctx* ctxctx) @nogc nothrow
{
(unresolved type) BufferBuffer* _error_ bufbuf = freeBuffer(ctx);
if (buf is null)
return true; // both held by the compositor; release will free one
if (!ensureBuffer(ctx, *buf, ctx.ctx.widthwidth, ctx.ctx.heightheight))
return false;
paint(*buf, atomicLoad(ctx.ctx.framesframes));
wsi_surface_attach(ctx.ctx.surfacesurface, buf.buf.handlehandle, 0, 0);
wsi_surface_damage_buffer(ctx.ctx.surfacesurface, 0, 0, buf.buf.widthwidth, buf.buf.heightheight);
if (ctx.ctx.frameCbframeCb is null)
{
ctx.ctx.frameCbframeCb = wsi_surface_frame(ctx.ctx.surfacesurface);
wsi_callback_add_listener(ctx.ctx.frameCbframeCb, &g_frameListener, ctx);
}
wsi_surface_commit(ctx.ctx.surfacesurface);
atomicStore(buf.buf.busybusy, true);
ctx.ctx.commitscommits++;
return true;
}
extern (C) void app.onGlobalonGlobal(void* data, wl_registry* reg, uint name,
const(char)* iface, uint ver) @nogc nothrow
{
auto _error_ ctxctx = cast((unresolved type) CtxCtx*) data;
if (strcmp(iface, wl_compositor_interface.name) == 0)
ctx.ctx.compositorcompositor = cast(wl_compositor*) wsi_registry_bind(reg, name,
&wl_compositor_interface, ver < 4 ? ver : 4);
else if (strcmp(iface, wl_shm_interface.name) == 0)
ctx.ctx.shmshm = cast(wl_shm*) wsi_registry_bind(reg, name, &wl_shm_interface, 1);
else if (strcmp(iface, xdg_wm_base_interface.name) == 0)
ctx.ctx.wmBasewmBase = cast(xdg_wm_base*) wsi_registry_bind(reg, name, &xdg_wm_base_interface, 1);
}
extern (C) void app.onGlobalRemoveonGlobalRemove(void* data, wl_registry* reg, uint name) @nogc nothrow
{
}
extern (C) void app.onWmBasePingonWmBasePing(void* data, xdg_wm_base* (parameter) xdg_wm_base* bb, uint serial) @nogc nothrow
{
wsi_wm_base_pong(b, serial);
}
extern (C) void app.onToplevelConfigureonToplevelConfigure(void* data, xdg_toplevel* (parameter) xdg_toplevel* tt, int w, int h,
wl_array* states) @nogc nothrow
{
auto _error_ ctxctx = cast((unresolved type) CtxCtx*) data;
ctx.ctx.pendingWpendingW = w;
ctx.ctx.pendingHpendingH = h;
}
extern (C) void app.onXdgSurfaceConfigureonXdgSurfaceConfigure(void* data, xdg_surface* s, uint serial) @nogc nothrow
{
auto _error_ ctxctx = cast((unresolved type) CtxCtx*) data;
wsi_xdg_surface_ack_configure(s, serial);
ctx.ctx.widthwidth = ctx.ctx.pendingWpendingW > 0 ? ctx.ctx.pendingWpendingW : defaultWidth;
ctx.ctx.heightheight = ctx.ctx.pendingHpendingH > 0 ? ctx.ctx.pendingHpendingH : defaultHeight;
if (!ctx.ctx.configuredconfigured)
{
ctx.ctx.configuredconfigured = true;
emitf("first_configure", "tag=%s size=%dx%d", ctx.ctx.tagtag, ctx.ctx.widthwidth, ctx.ctx.heightheight);
render(ctx);
}
}
extern (C) void app.onToplevelCloseonToplevelClose(void* data, xdg_toplevel* (parameter) xdg_toplevel* tt) @nogc nothrow
{
atomicStore((cast((unresolved type) CtxCtx*) data).(cast(Ctx*)data).runningrunning, false);
}
extern (C) void app.onToplevelConfigureBoundsonToplevelConfigureBounds(void* data, xdg_toplevel* (parameter) xdg_toplevel* tt, int w, int h) @nogc nothrow
{
}
extern (C) void app.onToplevelWmCapabilitiesonToplevelWmCapabilities(void* data, xdg_toplevel* (parameter) xdg_toplevel* tt, wl_array* caps) @nogc nothrow
{
}
extern (C) void app.onBufferReleaseonBufferRelease(void* data, wl_buffer* (parameter) wl_buffer* bb) @nogc nothrow
{
atomicStore((cast((unresolved type) BufferBuffer*) data).(cast(Buffer*)data).busybusy, false);
}
// Probe-2 bookkeeping: which thread ran the frame-callback handler?
private __gshared pthread_t[2] (__gshared global) ulong[2] app.g_dispThreadsg_dispThreads;
private shared int[3] (shared global) shared(int[3]) app.g_framesOnThreadg_framesOnThread; // [main, dispatcher a, dispatcher b]
extern (C) void app.onFrameDoneonFrameDone(void* data, wl_callback* (parameter) wl_callback* cbcb, uint timeMs) @nogc nothrow
{
auto _error_ ctxctx = cast((unresolved type) CtxCtx*) data;
wsi_callback_destroy(cb);
ctx.ctx.frameCbframeCb = null;
(template instance) atomicOp!"+="atomicOp!"+="(ctx.ctx.framesframes, 1);
const _error_ selfself = pthread_self();
if (self == g_dispThreads[0])
(template instance) atomicOp!"+="atomicOp!"+="(g_framesOnThread[1], 1);
else if (self == g_dispThreads[1])
(template instance) atomicOp!"+="atomicOp!"+="(g_framesOnThread[2], 1);
else
(template instance) atomicOp!"+="atomicOp!"+="(g_framesOnThread[0], 1);
if (ctx.ctx.autoRenderautoRender && atomicLoad(ctx.ctx.runningrunning))
render(ctx);
}
__gshared wl_registry_listener _error_ app.g_registryListenerg_registryListener = {&onGlobal, &onGlobalRemove};
__gshared xdg_wm_base_listener _error_ app.g_wmBaseListenerg_wmBaseListener = {&onWmBasePing};
__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};
/// Registry roundtrip + surface tree + initial no-buffer commit + dispatch
/// until the first configure is acked (which commits the first buffer).
/// Runs on whichever thread the probe says.
bool app.setupWindowRegistry roundtrip + surface tree + initial no-buffer commit + dispatch
until the first configure is acked (which commits the first buffer).
Runs on whichever thread the probe says.
setupWindow((struct) app.CtxEverything one connection-plus-window owns. Probes share one global Ctx,
except probe 5, which gives each thread its own.
Ctx* (parameter) Ctx* ctxctx, const(char)* title) @nogc nothrow
{
ctx.ctx.registryregistry = wsi_display_get_registry(ctx.ctx.displaydisplay);
wsi_registry_add_listener(ctx.ctx.registryregistry, &g_registryListener, ctx);
if (wl_display_roundtrip(ctx.ctx.displaydisplay) < 0)
return false;
if (ctx.ctx.compositorcompositor is null || ctx.ctx.shmshm is null || ctx.ctx.wmBasewmBase is null)
return false;
wsi_wm_base_add_listener(ctx.ctx.wmBasewmBase, &g_wmBaseListener, ctx);
ctx.ctx.surfacesurface = wsi_compositor_create_surface(ctx.ctx.compositorcompositor);
ctx.ctx.xdgSurfacexdgSurface = wsi_wm_base_get_xdg_surface(ctx.ctx.wmBasewmBase, ctx.ctx.surfacesurface);
wsi_xdg_surface_add_listener(ctx.ctx.xdgSurfacexdgSurface, &g_xdgSurfaceListener, ctx);
ctx.ctx.topleveltoplevel = wsi_xdg_surface_get_toplevel(ctx.ctx.xdgSurfacexdgSurface);
wsi_toplevel_add_listener(ctx.ctx.topleveltoplevel, &g_toplevelListener, ctx);
wsi_toplevel_set_title(ctx.ctx.topleveltoplevel, title);
wsi_toplevel_set_app_id(ctx.ctx.topleveltoplevel, title);
wsi_surface_commit(ctx.ctx.surfacesurface); // the mandatory no-buffer initial commit
emitf("window_created", "tag=%s", ctx.ctx.tagtag);
while (!ctx.ctx.configuredconfigured)
if (wl_display_dispatch(ctx.ctx.displaydisplay) < 0)
return false;
return true;
}
/// Blocking-dispatch the default queue until `nFrames` frame callbacks or
/// `capUs` elapse.
bool app.dispatchFramesBlocking-dispatch the default queue until nFrames frame callbacks or
capUs elapse.
dispatchFrames((struct) app.CtxEverything one connection-plus-window owns. Probes share one global Ctx,
except probe 5, which gives each thread its own.
Ctx* (parameter) Ctx* ctxctx, int nFrames, long capUs) @nogc nothrow
{
const _error_ deadlinedeadline = nowUs() + capUs;
while (atomicLoad(ctx.ctx.framesframes) < nFrames && nowUs() < deadline)
if (wl_display_dispatch(ctx.ctx.displaydisplay) < 0)
return false;
return true;
}
void app.teardownCtxteardownCtx((struct) app.CtxEverything one connection-plus-window owns. Probes share one global Ctx,
except probe 5, which gives each thread its own.
Ctx* (parameter) Ctx* ctxctx, bool disconnect) @nogc nothrow
{
foreach (ref (parameter) bb; ctx.ctx.buffersbuffers)
if (b.b.handlehandle !is null)
{
wsi_buffer_destroy(b.b.handlehandle);
munmap(b.b.pixelspixels, b.b.byteSizebyteSize);
b = Buffer.init;
}
if (ctx.ctx.frameCbframeCb !is null)
wsi_callback_destroy(ctx.ctx.frameCbframeCb);
if (ctx.ctx.topleveltoplevel !is null)
wsi_toplevel_destroy(ctx.ctx.topleveltoplevel);
if (ctx.ctx.xdgSurfacexdgSurface !is null)
wsi_xdg_surface_destroy(ctx.ctx.xdgSurfacexdgSurface);
if (ctx.ctx.surfacesurface !is null)
wsi_surface_destroy(ctx.ctx.surfacesurface);
if (ctx.ctx.wmBasewmBase !is null)
wsi_wm_base_destroy(ctx.ctx.wmBasewmBase);
if (ctx.ctx.shmshm !is null)
wl_proxy_destroy(cast(wl_proxy*) ctx.ctx.shmshm);
if (ctx.ctx.compositorcompositor !is null)
wl_proxy_destroy(cast(wl_proxy*) ctx.ctx.compositorcompositor);
if (ctx.ctx.registryregistry !is null)
wl_proxy_destroy(cast(wl_proxy*) ctx.ctx.registryregistry);
if (disconnect && ctx.ctx.displaydisplay !is null)
wl_display_disconnect(ctx.ctx.displaydisplay);
}
/// Post-probe health check shared by every probe that keeps its connection.
const(char)* app.protocolErrorTextPost-probe health check shared by every probe that keeps its connection.
protocolErrorText((struct) app.CtxEverything one connection-plus-window owns. Probes share one global Ctx,
except probe 5, which gives each thread its own.
Ctx* (parameter) Ctx* ctxctx) @nogc nothrow
{
return wl_display_get_error(ctx.ctx.displaydisplay) == 0 ? "0"."0".ptrptr : "SET"."SET".ptrptr;
}
private __gshared (struct) app.CtxEverything one connection-plus-window owns. Probes share one global Ctx,
except probe 5, which gives each thread its own.
Ctx _error_ app.g_ctxg_ctx;
// -- probe 1: the whole window built on a worker thread, main sleeps -----------
private shared bool (shared global) shared(bool) app.g_workerDoneg_workerDone;
private shared bool (shared global) shared(bool) app.g_workerOkg_workerOk;
extern (C) void* void* app.windowWorker(void* __param_0) nothrow @nogcwindowWorker(void*) @nogc nothrow
{
emit("thread=worker action=setup_window_start");
bool (local variable) bool okok = setupWindow(&g_ctx, "wsi-f17-threading");
if ((local variable) bool okok)
(local variable) bool okok = dispatchFrames(&g_ctx, 30, 4_000_000);
emitf("thread=worker", "action=done ok=%d frames=%d commits=%d",
cast(int) ok, atomicLoad(g_ctx.g_ctx.framesframes), g_ctx.g_ctx.commitscommits);
teardownCtx(&g_ctx, false); // even teardown happens off-main
(template function) core.atomic.atomicStore(MemoryOrder ms = MemoryOrder.seq, T, V)(ref T val, V newval) if (!is(T == shared) && !is(V == shared))atomicStore((shared global) shared(bool) app.g_workerOkg_workerOk, (local variable) bool okok && (template function) 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))atomicLoad(g_ctx.(field) _error_ g_ctx.framesframes) >= 30);
void core.atomic.atomicStore!(MemoryOrder.seq, bool, bool)(ref shared(bool) val, bool newval) pure nothrow @nogc @trustedWrites '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((shared global) shared(bool) app.g_workerDoneg_workerDone, true);
return null;
}
int int app.probeWindowOnWorker() nothrow @nogcprobeWindowOnWorker() @nogc nothrow
{
g_ctx.g_ctx.displaydisplay = wl_display_connect(null); // connected on MAIN …
if (g_ctx.(field) _error_ g_ctx.displaydisplay is null)
{
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict("error", "no_compositor_in_child");
return 0;
}
emit("step name=wl_display_connect thread=main");
pthread_t (local variable) ulong tt;
int core.sys.posix.pthread.pthread_create(ulong*, scope const(core.sys.posix.sys.types.pthread_attr_t*), extern (C) void* function(void*), void*) nothrow @nogcpthread_create(&(local variable) ulong tt, null, &void* app.windowWorker(void* __param_0) nothrow @nogcwindowWorker, null);
// … and never touched by main again until the worker is done: main SLEEPS.
while (!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.g_workerDoneg_workerDone))
int core.sys.posix.unistd.usleep(uint) nothrow @nogc @trustedusleep(10_000);
int core.sys.posix.pthread.pthread_join(ulong, void**) nothrow @nogcpthread_join((local variable) ulong tt, null);
static __gshared char[192] (__gshared global) char[192] app.probeWindowOnWorker.dd;
int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf((__gshared global) char[192] app.probeWindowOnWorker.dd.(constant) char* char[192].ptr = &dptr, (__gshared global) char[192] app.probeWindowOnWorker.dd.(constant) ulong char[192].length = 192LUlength,
"window_and_30_frames_entirely_on_worker=%d connect_thread=main protocol_error=%s",
cast(int) 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.g_workerOkg_workerOk), protocolErrorText(&g_ctx));
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict(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.g_workerOkg_workerOk) ? "ok" : "error", (__gshared global) char[192] app.probeWindowOnWorker.dd.(constant) char* char[192].ptr = &dptr);
wl_display_disconnect(g_ctx.g_ctx.displaydisplay);
return 0;
}
// -- probe 2: two threads dispatching the same default queue -------------------
private shared bool (shared global) shared(bool) app.g_p2Runningg_p2Running;
private shared int (shared global) shared(int) app.g_aliveg_alive;
private shared int[2] (shared global) shared(int[2]) app.g_dispatchedg_dispatched; // events returned by wl_display_dispatch
private shared int[2] (shared global) shared(int[2]) app.g_dispatchCallsg_dispatchCalls;
private shared int (shared global) shared(int) app.g_syncDoneg_syncDone;
extern (C) void app.onSyncCountonSyncCount(void* data, wl_callback* (parameter) wl_callback* cbcb, uint (parameter) uint tt) @nogc nothrow
{
wsi_callback_destroy(cb);
(template instance) atomicOp!"+="atomicOp!"+="(g_syncDone, 1);
}
__gshared wl_callback_listener _error_ app.g_syncCountListenerg_syncCountListener = {&onSyncCount};
extern (C) void* void* app.dispatcher(void* arg) nothrow @nogcdispatcher(void* (parameter) void* argarg) @nogc nothrow
{
const (local variable) const(int) idxidx = cast(int) cast((alias) object.size_t = ulongsize_t) (parameter) void* argarg;
int core.atomic.atomicOp!("+=", int, int)(ref shared(int) val, int mod) pure nothrow @nogc @safePerforms the binary operation 'op' on val using 'mod' as the modifier.
atomicOp!"+="((shared global) shared(int) app.g_aliveg_alive, 1);
emitf("thread_start", "thread=dispatcher_%c", cast(char)('a' + idx));
while (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.g_p2Runningg_p2Running))
{
const (local variable) const(_error_) nn = wl_display_dispatch(g_ctx.g_ctx.displaydisplay); // blocking, default queue
if (n < 0)
break;
(template function) core.atomic.atomicOp(string op, T, V1)(ref shared T val, V1 mod) if (__traits(compiles, mixin("*cast(T*)&val" ~ op ~ "mod")))atomicOp!"+="((shared global) shared(int[2]) app.g_dispatchedg_dispatched[(local variable) const(int) idxidx], n);
int core.atomic.atomicOp!("+=", int, int)(ref shared(int) val, int mod) pure nothrow @nogc @safePerforms the binary operation 'op' on val using 'mod' as the modifier.
atomicOp!"+="((shared global) shared(int[2]) app.g_dispatchCallsg_dispatchCalls[(local variable) const(int) idxidx], 1);
}
emitf("thread_done", "thread=dispatcher_%c events=%d calls=%d",
cast(char)('a' + idx), atomicLoad(g_dispatched[idx]), atomicLoad(g_dispatchCalls[idx]));
int core.atomic.atomicOp!("-=", int, int)(ref shared(int) val, int mod) pure nothrow @nogc @safePerforms the binary operation 'op' on val using 'mod' as the modifier.
atomicOp!"-="((shared global) shared(int) app.g_aliveg_alive, 1);
return null;
}
int int app.probeConcurrentDispatch() nothrow @nogcprobeConcurrentDispatch() @nogc nothrow
{
g_ctx.g_ctx.displaydisplay = wl_display_connect(null);
if (g_ctx.(field) _error_ g_ctx.displaydisplay is null || !setupWindow(&g_ctx, "wsi-f17-threading"))
{
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict("error", "setup_failed");
return 0;
}
// autoRender keeps frame callbacks (≈60 Hz events) flowing; the frame
// handler runs on whichever thread happens to dispatch it.
void core.atomic.atomicStore!(MemoryOrder.seq, bool, bool)(ref shared(bool) val, bool newval) pure nothrow @nogc @trustedWrites '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((shared global) shared(bool) app.g_p2Runningg_p2Running, true);
pthread_t[2] (local variable) ulong[2] tsts;
int core.sys.posix.pthread.pthread_create(ulong*, scope const(core.sys.posix.sys.types.pthread_attr_t*), extern (C) void* function(void*), void*) nothrow @nogcpthread_create(&(local variable) ulong[2] tsts[0], null, &void* app.dispatcher(void* arg) nothrow @nogcdispatcher, cast(void*) 0);
int core.sys.posix.pthread.pthread_create(ulong*, scope const(core.sys.posix.sys.types.pthread_attr_t*), extern (C) void* function(void*), void*) nothrow @nogcpthread_create(&(local variable) ulong[2] tsts[1], null, &void* app.dispatcher(void* arg) nothrow @nogcdispatcher, cast(void*) 1);
(__gshared global) ulong[2] app.g_dispThreadsg_dispThreads[0] = (local variable) ulong[2] tsts[0];
(__gshared global) ulong[2] app.g_dispThreadsg_dispThreads[1] = (local variable) ulong[2] tsts[1];
int core.sys.posix.unistd.usleep(uint) nothrow @nogc @trustedusleep(2_000_000); // main does NOT dispatch — only the two workers do
void core.atomic.atomicStore!(MemoryOrder.seq, bool, bool)(ref shared(bool) val, bool newval) pure nothrow @nogc @trustedWrites '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((shared global) shared(bool) app.g_p2Runningg_p2Running, false);
// A thread blocked in wl_display_dispatch only wakes when an event
// arrives — feed wl_display.sync done events until both exit (cap 400).
int (local variable) int wakeswakes = 0;
while (int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int) app.g_aliveg_alive) > 0 && (local variable) int wakeswakes < 400)
{
auto (local variable) _error_ cbcb = wsi_display_sync(g_ctx.g_ctx.displaydisplay);
wsi_callback_add_listener(cb, &g_syncCountListener, null);
wl_display_flush(g_ctx.g_ctx.displaydisplay);
++(local variable) int wakeswakes;
int core.sys.posix.unistd.usleep(uint) nothrow @nogc @trustedusleep(5000);
}
const (local variable) const(int) stuckstuck = int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int) app.g_aliveg_alive);
static __gshared char[256] (__gshared global) char[256] app.probeConcurrentDispatch.dd;
if ((local variable) const(int) stuckstuck > 0)
{
int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf((__gshared global) char[256] app.probeConcurrentDispatch.dd.(constant) char* char[256].ptr = &dptr, (__gshared global) char[256] app.probeConcurrentDispatch.dd.(constant) ulong char[256].length = 256LUlength,
"threads_stuck_in_dispatch=%d after_%d_sync_wakes events_a=%d events_b=%d",
(local variable) const(int) stuckstuck, (local variable) int wakeswakes, int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int[2]) app.g_dispatchedg_dispatched[0]), int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int[2]) app.g_dispatchedg_dispatched[1]));
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict("deadlock", (__gshared global) char[256] app.probeConcurrentDispatch.dd.(constant) char* char[256].ptr = &dptr);
noreturn core.sys.posix.unistd._exit(int) nothrow @nogc @trusted_exit(0); // cannot join a stuck thread; verdict is flushed
}
int core.sys.posix.pthread.pthread_join(ulong, void**) nothrow @nogcpthread_join((local variable) ulong[2] tsts[0], null);
int core.sys.posix.pthread.pthread_join(ulong, void**) nothrow @nogcpthread_join((local variable) ulong[2] tsts[1], null);
int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf((__gshared global) char[256] app.probeConcurrentDispatch.dd.(constant) char* char[256].ptr = &dptr, (__gshared global) char[256] app.probeConcurrentDispatch.dd.(constant) ulong char[256].length = 256LUlength,
"events_a=%d events_b=%d frames=%d frames_handled_on_a=%d on_b=%d sync_wakes_to_unblock=%d protocol_error=%s",
int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int[2]) app.g_dispatchedg_dispatched[0]), int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int[2]) app.g_dispatchedg_dispatched[1]),
(template function) 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))atomicLoad(g_ctx.(field) _error_ g_ctx.framesframes), int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int[3]) app.g_framesOnThreadg_framesOnThread[1]),
int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int[3]) app.g_framesOnThreadg_framesOnThread[2]), (local variable) int wakeswakes, protocolErrorText(&g_ctx));
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict(wl_display_get_error(g_ctx.g_ctx.displaydisplay) == 0 ? "ok" : "error", (__gshared global) char[256] app.probeConcurrentDispatch.dd.(constant) char* char[256].ptr = &dptr);
teardownCtx(&g_ctx, true);
return 0;
}
// -- probe 3: the designed pattern — a worker-owned wl_event_queue -------------
private shared bool (shared global) shared(bool) app.g_qSyncSeeng_qSyncSeen;
private shared int (shared global) shared(int) app.g_workerSyncsg_workerSyncs;
private shared long (shared global) shared(long) app.g_workerFirstUsg_workerFirstUs, (shared global) shared(long) app.g_workerLastUsg_workerLastUs;
private shared bool (shared global) shared(bool) app.g_p3WorkerDoneg_p3WorkerDone;
extern (C) void app.onQueueSynconQueueSync(void* data, wl_callback* (parameter) wl_callback* cbcb, uint (parameter) uint tt) @nogc nothrow
{
wsi_callback_destroy(cb);
const _error_ nownow = nowUs();
if (atomicLoad(g_workerFirstUs) == 0)
atomicStore(g_workerFirstUs, now);
atomicStore(g_workerLastUs, now);
atomicStore(g_qSyncSeen, true);
}
__gshared wl_callback_listener _error_ app.g_queueSyncListenerg_queueSyncListener = {&onQueueSync};
extern (C) void* void* app.queueWorker(void* __param_0) nothrow @nogcqueueWorker(void*) @nogc nothrow
{
auto (local variable) _error_ displaydisplay = g_ctx.(field) _error_ g_ctx.displaydisplay;
// The worker's own queue, and a display *wrapper* to assign it through —
// wl_proxy_set_queue on a wrapper is the race-free idiom: objects created
// via the wrapper are born on the worker's queue, never the default one.
auto (local variable) _error_ queuequeue = wl_display_create_queue(display);
auto (local variable) _error_ wrapperwrapper = cast(wl_display*) wl_proxy_create_wrapper(display);
wl_proxy_set_queue(cast(wl_proxy*) wrapper, queue);
emit("thread=worker action=queue_and_wrapper_created");
foreach ((local variable) int ii; 0 .. 50)
{
void core.atomic.atomicStore!(MemoryOrder.seq, bool, bool)(ref shared(bool) val, bool newval) pure nothrow @nogc @trustedWrites '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((shared global) shared(bool) app.g_qSyncSeeng_qSyncSeen, false);
auto (local variable) _error_ cbcb = wsi_display_sync(wrapper); // done event -> worker's queue
wsi_callback_add_listener(cb, &g_queueSyncListener, null);
bool (local variable) bool failedfailed = false;
while (!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.g_qSyncSeeng_qSyncSeen))
if (wl_display_dispatch_queue(display, queue) < 0)
{
(local variable) bool failedfailed = true;
break;
}
if ((local variable) bool failedfailed)
break;
int core.atomic.atomicOp!("+=", int, int)(ref shared(int) val, int mod) pure nothrow @nogc @safePerforms the binary operation 'op' on val using 'mod' as the modifier.
atomicOp!"+="((shared global) shared(int) app.g_workerSyncsg_workerSyncs, 1);
int core.sys.posix.unistd.usleep(uint) nothrow @nogc @trustedusleep(10_000); // ~10 ms apart, so the runs overlap main's frames
}
wl_proxy_wrapper_destroy(cast(wl_proxy*) wrapper);
wl_event_queue_destroy(queue);
emitf("thread=worker", "action=done syncs=%d", atomicLoad(g_workerSyncs));
void core.atomic.atomicStore!(MemoryOrder.seq, bool, bool)(ref shared(bool) val, bool newval) pure nothrow @nogc @trustedWrites '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((shared global) shared(bool) app.g_p3WorkerDoneg_p3WorkerDone, true);
return null;
}
int int app.probePerThreadQueue() nothrow @nogcprobePerThreadQueue() @nogc nothrow
{
g_ctx.g_ctx.displaydisplay = wl_display_connect(null);
if (g_ctx.(field) _error_ g_ctx.displaydisplay is null || !setupWindow(&g_ctx, "wsi-f17-threading"))
{
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict("error", "setup_failed");
return 0;
}
const (local variable) const(_error_) mainFirstUsmainFirstUs = nowUs();
pthread_t (local variable) ulong tt;
int core.sys.posix.pthread.pthread_create(ulong*, scope const(core.sys.posix.sys.types.pthread_attr_t*), extern (C) void* function(void*), void*) nothrow @nogcpthread_create(&(local variable) ulong tt, null, &void* app.queueWorker(void* __param_0) nothrow @nogcqueueWorker, null);
// Main dispatches the DEFAULT queue (frame callbacks + rendering) while
// the worker dispatches ITS queue — concurrently, on one connection.
while (!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.g_p3WorkerDoneg_p3WorkerDone))
if (wl_display_dispatch(g_ctx.g_ctx.displaydisplay) < 0)
break;
int core.sys.posix.pthread.pthread_join(ulong, void**) nothrow @nogcpthread_join((local variable) ulong tt, null);
const (local variable) const(_error_) mainLastUsmainLastUs = nowUs();
// Concurrency evidence: the worker's 50 sync round-trips and main's frame
// stream span overlapping time windows on the same wl_display.
const (local variable) const(_error_) overlapoverlap = long core.atomic.atomicLoad!(MemoryOrder.seq, long)(ref return scope shared(const(long)) 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(long) app.g_workerFirstUsg_workerFirstUs) < mainLastUs
&& mainFirstUs < long core.atomic.atomicLoad!(MemoryOrder.seq, long)(ref return scope shared(const(long)) 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(long) app.g_workerLastUsg_workerLastUs)
&& (template function) 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))atomicLoad(g_ctx.(field) _error_ g_ctx.framesframes) > 0;
static __gshared char[256] (__gshared global) char[256] app.probePerThreadQueue.dd;
int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf((__gshared global) char[256] app.probePerThreadQueue.dd.(constant) char* char[256].ptr = &dptr, (__gshared global) char[256] app.probePerThreadQueue.dd.(constant) ulong char[256].length = 256LUlength,
"worker_syncs=%d/50 main_frames=%d overlap=%d worker_window_us=%lld..%lld protocol_error=%s",
int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int) app.g_workerSyncsg_workerSyncs), (template function) 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))atomicLoad(g_ctx.(field) _error_ g_ctx.framesframes), cast(int) overlap,
long core.atomic.atomicLoad!(MemoryOrder.seq, long)(ref return scope shared(const(long)) 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(long) app.g_workerFirstUsg_workerFirstUs), long core.atomic.atomicLoad!(MemoryOrder.seq, long)(ref return scope shared(const(long)) 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(long) app.g_workerLastUsg_workerLastUs), protocolErrorText(&g_ctx));
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict(int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int) app.g_workerSyncsg_workerSyncs) == 50 && overlap
&& wl_display_get_error(g_ctx.g_ctx.displaydisplay) == 0 ? "ok" : "error", (__gshared global) char[256] app.probePerThreadQueue.dd.(constant) char* char[256].ptr = &dptr);
teardownCtx(&g_ctx, true);
return 0;
}
// -- probe 4: render thread committing the shared wl_surface -------------------
private shared int (shared global) shared(int) app.g_renderFramesg_renderFrames; // frame callbacks for worker commits
private shared int (shared global) shared(int) app.g_renderCommitsg_renderCommits;
private shared bool (shared global) shared(bool) app.g_p4WorkerDoneg_p4WorkerDone;
extern (C) void app.onRenderFrameonRenderFrame(void* data, wl_callback* (parameter) wl_callback* cbcb, uint (parameter) uint tt) @nogc nothrow
{
wsi_callback_destroy(cb);
(template instance) atomicOp!"+="atomicOp!"+="(g_renderFrames, 1);
}
__gshared wl_callback_listener _error_ app.g_renderFrameListenerg_renderFrameListener = {&onRenderFrame};
extern (C) void* void* app.renderWorker(void* __param_0) nothrow @nogcrenderWorker(void*) @nogc nothrow
{
emit("thread=render action=start frames_target=100");
foreach ((local variable) int frameframe; 0 .. 100)
{
// Wait for a free buffer (wl_buffer.release lands on the main pump).
(struct) app.BufferOne wl_shm ARGB8888 buffer. busy is owned by the compositor between
wl_surface.commit and wl_buffer.release — and in probe 4 it crosses
threads (release lands on the dispatching main thread, the render worker
polls it), hence atomic.
Buffer* (local variable) _error_ bufbuf = null;
const (local variable) const(_error_) bufDeadlinebufDeadline = nowUs() + 1_000_000;
while (buf is null && nowUs() < bufDeadline)
{
buf = freeBuffer(&g_ctx);
if (buf is null)
int core.sys.posix.unistd.usleep(uint) nothrow @nogc @trustedusleep(500);
}
if (buf is null)
break;
if (!ensureBuffer(&g_ctx, *buf, g_ctx.g_ctx.widthwidth, g_ctx.g_ctx.heightheight))
break;
paint(*buf, frame);
// Requests on the shared wl_surface proxy, from the worker:
wsi_surface_attach(g_ctx.g_ctx.surfacesurface, buf.buf.handlehandle, 0, 0);
wsi_surface_damage_buffer(g_ctx.g_ctx.surfacesurface, 0, 0, buf.buf.widthwidth, buf.buf.heightheight);
auto (local variable) _error_ cbcb = wsi_surface_frame(g_ctx.g_ctx.surfacesurface);
wsi_callback_add_listener(cb, &g_renderFrameListener, null);
wsi_surface_commit(g_ctx.g_ctx.surfacesurface);
(template function) core.atomic.atomicStore(MemoryOrder ms = MemoryOrder.seq, T, V)(ref T val, V newval) if (!is(T == shared) && !is(V == shared))atomicStore(buf.(field) _error_ buf.busybusy, true);
wl_display_flush(g_ctx.g_ctx.displaydisplay); // each thread flushes its own requests
int core.atomic.atomicOp!("+=", int, int)(ref shared(int) val, int mod) pure nothrow @nogc @safePerforms the binary operation 'op' on val using 'mod' as the modifier.
atomicOp!"+="((shared global) shared(int) app.g_renderCommitsg_renderCommits, 1);
// Throttle on presentation: the frame callback is dispatched by MAIN.
const (local variable) const(_error_) frameDeadlineframeDeadline = nowUs() + 1_000_000;
while (int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int) app.g_renderFramesg_renderFrames) < int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int) app.g_renderCommitsg_renderCommits)
&& nowUs() < frameDeadline)
int core.sys.posix.unistd.usleep(uint) nothrow @nogc @trustedusleep(500);
}
emitf("thread=render", "action=done commits=%d frames_acked=%d",
atomicLoad(g_renderCommits), atomicLoad(g_renderFrames));
void core.atomic.atomicStore!(MemoryOrder.seq, bool, bool)(ref shared(bool) val, bool newval) pure nothrow @nogc @trustedWrites '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((shared global) shared(bool) app.g_p4WorkerDoneg_p4WorkerDone, true);
// One last sync so the main dispatcher wakes and sees the done flag.
auto (local variable) _error_ cbcb = wsi_display_sync(g_ctx.g_ctx.displaydisplay);
wsi_callback_add_listener(cb, &g_syncCountListener, null);
wl_display_flush(g_ctx.g_ctx.displaydisplay);
return null;
}
int int app.probeRenderThread() nothrow @nogcprobeRenderThread() @nogc nothrow
{
g_ctx.g_ctx.autoRenderautoRender = false; // main only pumps; the worker owns rendering
g_ctx.g_ctx.displaydisplay = wl_display_connect(null);
if (g_ctx.(field) _error_ g_ctx.displaydisplay is null || !setupWindow(&g_ctx, "wsi-f17-threading"))
{
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict("error", "setup_failed");
return 0;
}
pthread_t (local variable) ulong tt;
int core.sys.posix.pthread.pthread_create(ulong*, scope const(core.sys.posix.sys.types.pthread_attr_t*), extern (C) void* function(void*), void*) nothrow @nogcpthread_create(&(local variable) ulong tt, null, &void* app.renderWorker(void* __param_0) nothrow @nogcrenderWorker, null);
while (!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.g_p4WorkerDoneg_p4WorkerDone))
if (wl_display_dispatch(g_ctx.g_ctx.displaydisplay) < 0)
break;
int core.sys.posix.pthread.pthread_join(ulong, void**) nothrow @nogcpthread_join((local variable) ulong tt, null);
const (local variable) const(int) commitscommits = int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int) app.g_renderCommitsg_renderCommits);
const (local variable) const(int) ackedacked = int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int) app.g_renderFramesg_renderFrames);
static __gshared char[224] (__gshared global) char[224] app.probeRenderThread.dd;
int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf((__gshared global) char[224] app.probeRenderThread.dd.(constant) char* char[224].ptr = &dptr, (__gshared global) char[224] app.probeRenderThread.dd.(constant) ulong char[224].length = 224LUlength,
"commits_from_render_thread=%d/100 frame_callbacks=%d protocol_error=%s",
(local variable) const(int) commitscommits, (local variable) const(int) ackedacked, protocolErrorText(&g_ctx));
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict((local variable) const(int) commitscommits == 100 && (local variable) const(int) ackedacked >= 99 && wl_display_get_error(g_ctx.g_ctx.displaydisplay) == 0
? "ok" : "error", (__gshared global) char[224] app.probeRenderThread.dd.(constant) char* char[224].ptr = &dptr);
teardownCtx(&g_ctx, true);
return 0;
}
// -- probe 5: one wl_display connection per thread ------------------------------
private __gshared (struct) app.CtxEverything one connection-plus-window owns. Probes share one global Ctx,
except probe 5, which gives each thread its own.
Ctx[2] _error_ app.g_p5ctxg_p5ctx;
private shared int (shared global) shared(int) app.g_p5Okg_p5Ok;
extern (C) void* void* app.connectionPerThread(void* arg) nothrow @nogcconnectionPerThread(void* (parameter) void* argarg) @nogc nothrow
{
const (local variable) const(int) idxidx = cast(int) cast((alias) object.size_t = ulongsize_t) (parameter) void* argarg;
auto (local variable) _error_ ctxctx = &g_p5ctx[idx];
ctx.ctx.tagtag = idx == 0 ? "t0"."t0".ptrptr : "t1"."t1".ptrptr;
ctx.ctx.displaydisplay = wl_display_connect(null); // private connection: nothing shared
if (ctx.(field) _error_ ctx.displaydisplay is null)
return null;
emitf("thread_connect", "tag=%s fd=%d", ctx.ctx.tagtag, wl_display_get_fd(ctx.ctx.displaydisplay));
bool (local variable) bool okok = setupWindow(ctx, idx == 0 ? "wsi-f17-t0"."wsi-f17-t0".ptrptr : "wsi-f17-t1"."wsi-f17-t1".ptrptr);
if ((local variable) bool okok)
(local variable) bool okok = dispatchFrames(ctx, 20, 3_000_000) && (template function) 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))atomicLoad(ctx.(field) _error_ ctx.framesframes) >= 20;
emitf("thread_done", "tag=%s ok=%d frames=%d", ctx.ctx.tagtag, cast(int) ok,
atomicLoad(ctx.ctx.framesframes));
teardownCtx(ctx, true);
if ((local variable) bool okok)
int core.atomic.atomicOp!("+=", int, int)(ref shared(int) val, int mod) pure nothrow @nogc @safePerforms the binary operation 'op' on val using 'mod' as the modifier.
atomicOp!"+="((shared global) shared(int) app.g_p5Okg_p5Ok, 1);
return null;
}
int int app.probeConnectionPerThread() nothrow @nogcprobeConnectionPerThread() @nogc nothrow
{
pthread_t[2] (local variable) ulong[2] tsts;
int core.sys.posix.pthread.pthread_create(ulong*, scope const(core.sys.posix.sys.types.pthread_attr_t*), extern (C) void* function(void*), void*) nothrow @nogcpthread_create(&(local variable) ulong[2] tsts[0], null, &void* app.connectionPerThread(void* arg) nothrow @nogcconnectionPerThread, cast(void*) 0);
int core.sys.posix.pthread.pthread_create(ulong*, scope const(core.sys.posix.sys.types.pthread_attr_t*), extern (C) void* function(void*), void*) nothrow @nogcpthread_create(&(local variable) ulong[2] tsts[1], null, &void* app.connectionPerThread(void* arg) nothrow @nogcconnectionPerThread, cast(void*) 1);
int core.sys.posix.pthread.pthread_join(ulong, void**) nothrow @nogcpthread_join((local variable) ulong[2] tsts[0], null);
int core.sys.posix.pthread.pthread_join(ulong, void**) nothrow @nogcpthread_join((local variable) ulong[2] tsts[1], null);
static __gshared char[96] (__gshared global) char[96] app.probeConnectionPerThread.dd;
int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf((__gshared global) char[96] app.probeConnectionPerThread.dd.(constant) char* char[96].ptr = &dptr, (__gshared global) char[96] app.probeConnectionPerThread.dd.(constant) ulong char[96].length = 96LUlength, "threads_completed=%d/2 model=connection_per_thread",
int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int) app.g_p5Okg_p5Ok));
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict(int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int) app.g_p5Okg_p5Ok) == 2 ? "ok" : "error", (__gshared global) char[96] app.probeConnectionPerThread.dd.(constant) char* char[96].ptr = &dptr);
return 0;
}
// -- probe 6: read-intent protocol violated -------------------------------------
private shared bool (shared global) shared(bool) app.g_preparedg_prepared;
private shared bool (shared global) shared(bool) app.g_holderDoneg_holderDone;
private shared bool (shared global) shared(bool) app.g_violatorDoneg_violatorDone;
private shared int (shared global) shared(int) app.g_violatorRetg_violatorRet;
private shared long (shared global) shared(long) app.g_violatorUsg_violatorUs;
extern (C) void* void* app.intentHolder(void* __param_0) nothrow @nogcintentHolder(void*) @nogc nothrow
{
auto (local variable) _error_ displaydisplay = g_ctx.(field) _error_ g_ctx.displaydisplay;
while (wl_display_prepare_read(display) != 0)
wl_display_dispatch_pending(display);
emit("thread=holder action=prepare_read_acquired");
void core.atomic.atomicStore!(MemoryOrder.seq, bool, bool)(ref shared(bool) val, bool newval) pure nothrow @nogc @trustedWrites '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((shared global) shared(bool) app.g_preparedg_prepared, true);
int core.sys.posix.unistd.usleep(uint) nothrow @nogc @trustedusleep(600_000); // hold the read intent while the violator strikes
wl_display_cancel_read(display);
emit("thread=holder action=cancel_read_returned");
void core.atomic.atomicStore!(MemoryOrder.seq, bool, bool)(ref shared(bool) val, bool newval) pure nothrow @nogc @trustedWrites '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((shared global) shared(bool) app.g_holderDoneg_holderDone, true);
return null;
}
extern (C) void* void* app.readViolator(void* __param_0) nothrow @nogcreadViolator(void*) @nogc nothrow
{
const (local variable) const(_error_) t0t0 = nowUs();
// The violation: read_events without this thread ever calling
// prepare_read. The documented contract pairs them strictly 1:1.
const (local variable) const(_error_) rr = wl_display_read_events(g_ctx.g_ctx.displaydisplay);
(template function) core.atomic.atomicStore(MemoryOrder ms = MemoryOrder.seq, T, V)(ref T val, V newval) if (!is(T == shared) && !is(V == shared))atomicStore((shared global) shared(long) app.g_violatorUsg_violatorUs, nowUs() - t0);
(template function) core.atomic.atomicStore(MemoryOrder ms = MemoryOrder.seq, T, V)(ref T val, V newval) if (!is(T == shared) && !is(V == shared))atomicStore((shared global) shared(int) app.g_violatorRetg_violatorRet, r);
emitf("thread=violator", "action=read_events_returned ret=%d took_us=%lld",
r, atomicLoad(g_violatorUs));
void core.atomic.atomicStore!(MemoryOrder.seq, bool, bool)(ref shared(bool) val, bool newval) pure nothrow @nogc @trustedWrites '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((shared global) shared(bool) app.g_violatorDoneg_violatorDone, true);
return null;
}
int int app.probePrepareReadViolation() nothrow @nogcprobePrepareReadViolation() @nogc nothrow
{
g_ctx.g_ctx.displaydisplay = wl_display_connect(null);
if (g_ctx.(field) _error_ g_ctx.displaydisplay is null)
{
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict("error", "no_compositor_in_child");
return 0;
}
pthread_t (local variable) ulong holderholder, (local variable) ulong violatorviolator;
int core.sys.posix.pthread.pthread_create(ulong*, scope const(core.sys.posix.sys.types.pthread_attr_t*), extern (C) void* function(void*), void*) nothrow @nogcpthread_create(&(local variable) ulong holderholder, null, &void* app.intentHolder(void* __param_0) nothrow @nogcintentHolder, null);
while (!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.g_preparedg_prepared))
int core.sys.posix.unistd.usleep(uint) nothrow @nogc @trustedusleep(1000);
int core.sys.posix.pthread.pthread_create(ulong*, scope const(core.sys.posix.sys.types.pthread_attr_t*), extern (C) void* function(void*), void*) nothrow @nogcpthread_create(&(local variable) ulong violatorviolator, null, &void* app.readViolator(void* __param_0) nothrow @nogcreadViolator, null);
// Timebox the violator: it may block forever inside read_events.
const (local variable) const(_error_) deadlinedeadline = nowUs() + 3_000_000;
while (!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.g_violatorDoneg_violatorDone) && nowUs() < deadline)
int core.sys.posix.unistd.usleep(uint) nothrow @nogc @trustedusleep(10_000);
static __gshared char[256] (__gshared global) char[256] app.probePrepareReadViolation.dd;
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.g_violatorDoneg_violatorDone))
{
int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf((__gshared global) char[256] app.probePrepareReadViolation.dd.(constant) char* char[256].ptr = &dptr, (__gshared global) char[256] app.probePrepareReadViolation.dd.(constant) ulong char[256].length = 256LUlength,
"read_events_without_prepare_blocked_3s holder_done=%d",
cast(int) 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.g_holderDoneg_holderDone));
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict("deadlock", (__gshared global) char[256] app.probePrepareReadViolation.dd.(constant) char* char[256].ptr = &dptr);
noreturn core.sys.posix.unistd._exit(int) nothrow @nogc @trusted_exit(0); // cannot join the stuck thread; verdict is flushed
}
int core.sys.posix.pthread.pthread_join(ulong, void**) nothrow @nogcpthread_join((local variable) ulong violatorviolator, null);
while (!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.g_holderDoneg_holderDone))
int core.sys.posix.unistd.usleep(uint) nothrow @nogc @trustedusleep(10_000);
int core.sys.posix.pthread.pthread_join(ulong, void**) nothrow @nogcpthread_join((local variable) ulong holderholder, null);
// Health check: does the connection still work after the violation?
(__gshared global) const(char)* app.g_watchdogDetailg_watchdogDetail = "health_roundtrip_hung_after_violation";
uint core.sys.posix.unistd.alarm(uint) nothrow @nogc @trustedalarm(4);
const (local variable) const(_error_) rtrt = wl_display_roundtrip(g_ctx.g_ctx.displaydisplay);
uint core.sys.posix.unistd.alarm(uint) nothrow @nogc @trustedalarm(0);
const (local variable) const(_error_) errerr = wl_display_get_error(g_ctx.g_ctx.displaydisplay);
int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf((__gshared global) char[256] app.probePrepareReadViolation.dd.(constant) char* char[256].ptr = &dptr, (__gshared global) char[256] app.probePrepareReadViolation.dd.(constant) ulong char[256].length = 256LUlength,
"read_events_no_prepare ret=%d took_us=%lld health_roundtrip=%d display_error=%d (nondeterministic)",
int core.atomic.atomicLoad!(MemoryOrder.seq, int)(ref return scope shared(const(int)) 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(int) app.g_violatorRetg_violatorRet), long core.atomic.atomicLoad!(MemoryOrder.seq, long)(ref return scope shared(const(long)) 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(long) app.g_violatorUsg_violatorUs), rt, err);
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict(err != 0 || rt < 0 ? "error" : "silent", (__gshared global) char[256] app.probePrepareReadViolation.dd.(constant) char* char[256].ptr = &dptr);
wl_display_disconnect(g_ctx.g_ctx.displaydisplay);
return 0;
}
// ---------------------------------------------------------------------------
int int app.runProbe(int n) nothrow @nogcrunProbe(int (parameter) int nn) @nogc nothrow
{
(__gshared global) int app.g_probeg_probe = (parameter) int nn;
void app.installCrashHandlers() nothrow @nogcinstallCrashHandlers();
emitf("probe_start", "n=%d", n);
switch ((parameter) int nn)
{
case 1:
int app.probeWindowOnWorker() nothrow @nogcprobeWindowOnWorker();
break;
case 2:
int app.probeConcurrentDispatch() nothrow @nogcprobeConcurrentDispatch();
break;
case 3:
int app.probePerThreadQueue() nothrow @nogcprobePerThreadQueue();
break;
case 4:
int app.probeRenderThread() nothrow @nogcprobeRenderThread();
break;
case 5:
int app.probeConnectionPerThread() nothrow @nogcprobeConnectionPerThread();
break;
case 6:
int app.probePrepareReadViolation() nothrow @nogcprobePrepareReadViolation();
break;
default:
void app.verdict(scope const(char)* result, scope const(char)* detail) nothrow @nogcThe one line the spec requires per run. Async-signal-safe on purpose
(snprintf into a static buffer + write(2)) so the signal handlers can
call it; the normal path uses it too so the format is identical.
verdict("error", "unknown_probe");
}
uint core.sys.posix.unistd.alarm(uint) nothrow @nogc @trustedalarm(0);
return 0;
}
int int D main(string[] args)main((alias) object.string = stringstring[] (parameter) string[] argsargs)
{
initInstrument("f17_wayland");
int (local variable) int probeprobe = 0;
foreach ((parameter) string aa; (parameter) string[] argsargs[1 .. $])
if ((local variable) string aa.(field) ulong string.lengthlength > 8 && (local variable) string aa[0 .. 8] == "--probe=")
(local variable) int probeprobe = (local variable) string aa[8] - '0';
// Capability gate in the parent, before any fork.
wl_display* (local variable) _error_ testtest = wl_display_connect(null);
if (test 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;
}
wl_display_disconnect(test);
if ((local variable) int probeprobe != 0)
return int app.runProbe(int n) nothrow @nogcrunProbe((local variable) int probeprobe);
// No argument: run the full matrix, each probe TWICE (the F17 spec's
// nondeterminism rule), each in a forked child so a wedged libwayland or
// a caught crash never poisons the next probe. Children always _exit(0).
foreach ((local variable) int nn; 1 .. 7)
foreach ((local variable) int runrun; 1 .. 3)
{
const (local variable) const(int) pidpid = int core.sys.posix.unistd.fork() nothrow @nogc @trustedfork();
if ((local variable) const(int) pidpid == 0)
{
int app.runProbe(int n) nothrow @nogcrunProbe((local variable) int nn);
noreturn core.sys.posix.unistd._exit(int) nothrow @nogc @trusted_exit(0);
}
int (local variable) int statusstatus;
int core.sys.posix.sys.wait.waitpid(int, int*, int) nothrow @nogcwaitpid((local variable) const(int) pidpid, &(local variable) int statusstatus, 0);
emitf("probe_child", "n=%d run=%d wait_status=%d", n, run, status);
}
emit("teardown all_probes_done");
return 0;
}