Commit2157d1d1Recorded29 Oct 2026Repositorysigil-wm

Constrain popups and retry failed client frame presentation

Changed
 Makefile                       |  15 +++++++++++++
 README.md                      |  13 +++++++++++
 scripts/test-foot-blink        |  68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 scripts/test-popup-constraints |  23 +++++++++++++++++++
 scripts/test-surface-frames    |  39 ++++++++++++++++++++++++++++++++
 src/c/deadlines.h              |   5 ++++-
 src/c/host.c                   |  36 ++++++++++++++++++++++++++---
 src/c/output-gamma.h           |  10 ++++-----
 test/damage-client.c           | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test/deadlines.c               |   1 +
 test/frame-fault.c             |  41 +++++++++++++++++++++++++++++++++
 test/popup-client.c            | 199 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test/popup-constraints.c       | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test/surface-frames.c          | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 14 files changed, 821 insertions(+), 9 deletions(-)
Diff
Makefilemodified
@@ -219,6 +219,21 @@ build/backend-pointer.so: test/backend-pointer.c
219
build/geometry-client: test/geometry-client.c build/xdg-shell-client-protocol.h build/xdg-shell-client-protocol.c build/wlr-layer-shell-unstable-v1-client-protocol.h build/wlr-layer-shell-unstable-v1-client-protocol.c
220
$(CC) $(CFLAGS) -Ibuild $(shell pkg-config --cflags wayland-client) $< build/xdg-shell-client-protocol.c build/wlr-layer-shell-unstable-v1-client-protocol.c $(shell pkg-config --libs wayland-client) -Wl,-rpath,$(shell pkg-config --variable=libdir wayland-client) -o $@
221
+222
build/popup-client: test/popup-client.c build/xdg-shell-client-protocol.h build/xdg-shell-client-protocol.c build/wlr-layer-shell-unstable-v1-client-protocol.h build/wlr-layer-shell-unstable-v1-client-protocol.c
+223
$(CC) $(CFLAGS) -Ibuild $(shell pkg-config --cflags wayland-client) $< build/xdg-shell-client-protocol.c build/wlr-layer-shell-unstable-v1-client-protocol.c $(shell pkg-config --libs wayland-client) -Wl,-rpath,$(shell pkg-config --variable=libdir wayland-client) -o $@
+224
+225
build/popup-constraints: test/popup-constraints.c build/libsigil-wm.so
+226
$(CC) $(CFLAGS) -Isrc/c $< -Lbuild -lsigil-wm -Wl,-rpath,'$$ORIGIN' -o $@
+227
+228
build/damage-client: test/damage-client.c build/xdg-shell-client-protocol.h build/xdg-shell-client-protocol.c
+229
$(CC) $(CFLAGS) -Ibuild $(shell pkg-config --cflags wayland-client) $< build/xdg-shell-client-protocol.c $(shell pkg-config --libs wayland-client) -Wl,-rpath,$(shell pkg-config --variable=libdir wayland-client) -o $@
+230
+231
build/surface-frames: test/surface-frames.c build/libsigil-wm.so
+232
$(CC) $(CFLAGS) -Isrc/c $< -Lbuild -lsigil-wm -Wl,-rpath,'$$ORIGIN' -o $@
+233
+234
build/frame-fault.so: test/frame-fault.c
+235
$(CC) $(CFLAGS) $(CPPFLAGS) -fPIC -shared $< $(LDFLAGS) $(LDLIBS) -ldl -o $@
+236
237
build/security-context-v1-client-protocol.h:
238
mkdir -p build
239
wayland-scanner client-header $(PROTOCOLS)/staging/security-context/security-context-v1.xml $@
README.mdmodified
@@ -61,6 +61,19 @@ frame or client events. `scripts/test-deadlines` checks every deadline class,
61
independent repeating timers, real resource expiry, lock isolation, repeated
62
key streams, coalesced motion and zero blanked-host maintenance ticks.
63
+64
Popups use their root parent's output as the constraint area, including layer
+65
surfaces and nested popups. Initial configuration, repositioning and policy
+66
configuration apply the client's flip, slide and resize rules in root-surface
+67
coordinates. `scripts/test-popup-constraints` checks delivered geometry against
+68
two adjacent outputs with nonzero layout and window-geometry origins.
+69
+70
Failed scene commits retain damage and client frame callbacks until an output
+71
commit succeeds. Ordinary scene failures retry after 16 ms through the native
+72
deadline timer; gamma failures retain their existing ownership and retry rules.
+73
`scripts/test-surface-frames` checks tiny redraws, callbacks and actual presented
+74
pixel phases, including one rejected commit. `scripts/test-foot-blink` samples
+75
cursor phases in a nested compositor, directly and through a private tmux.
+76
77
Run the stacking example from a Wayland session:
78
79
```sh
scripts/test-foot-blinkadded
@@ -0,0 +1,68 @@
+1
#!/usr/bin/env bash
+2
set -euo pipefail
+3
cd "$(dirname "$0")/.."
+4
if [[ ${1:-} != --inside ]]; then
+5
exec guix time-machine -C channels.scm -- shell -m draw-manifest.scm foot -- bash scripts/test-foot-blink --inside
+6
fi
+7
make -j2 all build/surface-frames build/frame-fault.so
+8
unset TMUX TMUX_PANE
+9
unset SIGIL_WM_FOOT_MODERN
+10
foot_version=$(foot --version)
+11
if [[ $foot_version =~ ([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
+12
if (( BASH_REMATCH[1] > 1 || (BASH_REMATCH[1] == 1 && BASH_REMATCH[2] >= 28) )); then
+13
export SIGIL_WM_FOOT_MODERN=1
+14
fi
+15
else
+16
printf 'unrecognized foot version: %s\n' "$foot_version" >&2
+17
exit 2
+18
fi
+19
out=$PWD/build/receipts/foot-blink
+20
mkdir -p "$out"
+21
parent_runtime=$(mktemp -d /tmp/sigil-foot-parent.XXXXXXXX)
+22
child_runtime=$(mktemp -d /tmp/sigil-foot-child.XXXXXXXX)
+23
chmod 700 "$parent_runtime" "$child_runtime"
+24
parent_pid=
+25
cleanup() {
+26
if [[ -n $parent_pid ]]; then kill "$parent_pid" 2>/dev/null || true; wait "$parent_pid" 2>/dev/null || true; fi
+27
rm -rf "$parent_runtime" "$child_runtime"
+28
}
+29
trap cleanup EXIT
+30
XDG_RUNTIME_DIR=$parent_runtime WLR_BACKENDS=headless WLR_RENDERER=pixman WLR_HEADLESS_OUTPUTS=1 \
+31
SIGIL_WM_REPL=$parent_runtime/repl timeout 60 bash scripts/run-sigil -L src -L examples eval -f examples/tiling.sgl > "$out/parent.log" 2>&1 &
+32
parent_pid=$!
+33
for ((attempt=0;attempt<400;attempt++)); do
+34
if grep -q '^WAYLAND_SOCKET=' "$out/parent.log"; then break; fi
+35
sleep 0.025
+36
done
+37
parent_display=$(sed -n 's/^WAYLAND_SOCKET=//p' "$out/parent.log" | head -1)
+38
[[ -n $parent_display ]] || { cat "$out/parent.log"; exit 2; }
+39
for terminal in direct tmux; do
+40
for clip in 0 1; do
+41
for failure in ${SIGIL_WM_FOOT_FAILURES:-clean}; do
+42
unset SIGIL_WM_FRAMES_TMUX
+43
if [[ $terminal == tmux ]]; then export SIGIL_WM_FRAMES_TMUX=1; fi
+44
unset SIGIL_WM_FRAME_FAILURE
+45
rm -f "$child_runtime/fail"
+46
if [[ $failure == busy ]]; then export SIGIL_WM_FRAME_FAILURE=$child_runtime/fail; fi
+47
rc=0
+48
XDG_RUNTIME_DIR=$child_runtime WAYLAND_DISPLAY=$parent_runtime/$parent_display WLR_BACKENDS=wayland WLR_RENDERER=pixman \
+49
SIGIL_WM_FRAMES_FOOT=1 LD_PRELOAD=$PWD/build/frame-fault.so \
+50
timeout 20 build/surface-frames "$out/$terminal-$clip-$failure.samples" "$clip" > "$out/$terminal-$clip-$failure.log" 2>&1 || rc=$?
+51
cat "$out/$terminal-$clip-$failure.log"
+52
test "$rc" == 0
+53
if grep -q '^ err:' "$out/$terminal-$clip-$failure.log"; then exit 2; fi
+54
if [[ $failure == busy ]]; then
+55
awk '
+56
/^FRAME_COMMIT_EBUSY / { pending=1; failures++ }
+57
/^FRAME_COMMIT_ACCEPTED / { pending=0 }
+58
/^FRAME_CALLBACK / && pending { print "FOOT_CALLBACK_BEFORE_ACCEPTED=FAIL"; failed=1 }
+59
END { if (failures != 1) failed=1; exit failed }
+60
' "$out/$terminal-$clip-$failure.log"
+61
fi
+62
done
+63
done
+64
done
+65
XDG_RUNTIME_DIR=$parent_runtime bash scripts/msg "$parent_runtime/repl" '(stop-host! host)' > "$out/quit.log"
+66
wait "$parent_pid"
+67
parent_pid=
+68
printf 'FOOT_NESTED_BLINK=PASS clipped-and-unclipped sampled-on-off-phases\n'
scripts/test-popup-constraintsadded
@@ -0,0 +1,23 @@
+1
#!/usr/bin/env bash
+2
set -euo pipefail
+3
cd "$(dirname "$0")/.."
+4
if [[ ${1:-} != --inside ]]; then
+5
exec guix time-machine -C channels.scm -- shell -m manifest.scm -- bash scripts/test-popup-constraints --inside
+6
fi
+7
make -j2 all build/popup-client build/popup-constraints
+8
mkdir -p build/receipts/popup-constraints
+9
runtime=$(mktemp -d /tmp/sigil-popup-constraints.XXXXXXXX)
+10
chmod 700 "$runtime"
+11
trap 'rm -rf "$runtime"' EXIT
+12
export XDG_RUNTIME_DIR=$runtime WLR_BACKENDS=headless WLR_RENDERER=pixman WLR_HEADLESS_OUTPUTS=2
+13
unset WAYLAND_DISPLAY DISPLAY
+14
result=0
+15
for role in toplevel layer; do
+16
for output in 0 1; do
+17
rc=0
+18
timeout 30 build/popup-constraints "$role" "$output" > "build/receipts/popup-constraints/$role-$output.log" 2>&1 || rc=$?
+19
cat "build/receipts/popup-constraints/$role-$output.log"
+20
if ((rc)); then result=$rc; fi
+21
done
+22
done
+23
exit "$result"
scripts/test-surface-framesadded
@@ -0,0 +1,39 @@
+1
#!/usr/bin/env bash
+2
set -euo pipefail
+3
cd "$(dirname "$0")/.."
+4
if [[ ${1:-} != --inside ]]; then
+5
exec guix time-machine -C channels.scm -- shell -m manifest.scm -- bash scripts/test-surface-frames --inside
+6
fi
+7
make -j2 all build/damage-client build/surface-frames build/frame-fault.so
+8
out=$PWD/build/receipts/surface-frames
+9
mkdir -p "$out"
+10
runtime=$(mktemp -d /tmp/sigil-surface-frames.XXXXXXXX)
+11
chmod 700 "$runtime"
+12
trap 'rm -rf "$runtime"' EXIT
+13
export XDG_RUNTIME_DIR=$runtime WLR_BACKENDS=headless WLR_RENDERER=pixman WLR_HEADLESS_OUTPUTS=1
+14
unset WAYLAND_DISPLAY DISPLAY
+15
result=0
+16
for clip in 0 1; do
+17
for failure in clean busy; do
+18
unset SIGIL_WM_FRAME_FAILURE
+19
if [[ $failure == busy ]]; then export SIGIL_WM_FRAME_FAILURE=$runtime/fail; fi
+20
rc=0
+21
LD_PRELOAD=$PWD/build/frame-fault.so timeout 20 build/surface-frames "$out/$clip-$failure.samples" "$clip" > "$out/$clip-$failure.log" 2>&1 || rc=$?
+22
cat "$out/$clip-$failure.log"
+23
awk '
+24
/^FRAME_PRESENT / && /presented=1/ { split($3, item, "="); presented[item[2]]=1 }
+25
/^DAMAGE_PRESENT_PHASE=/ { split($2, item, "="); phases[item[2]]=1 }
+26
END {
+27
for (sequence in phases) if (!presented[sequence]) {
+28
print "DAMAGE_UNPRESENTED_SEQUENCE=" sequence; failed=1
+29
}
+30
exit failed
+31
}
+32
' "$out/$clip-$failure.log" || rc=2
+33
if [[ $failure == busy ]]; then
+34
test "$(grep -c '^FRAME_COMMIT_EBUSY ' "$out/$clip-$failure.log")" == 1 || rc=2
+35
fi
+36
if ((rc)); then result=$rc; fi
+37
done
+38
done
+39
exit "$result"
src/c/deadlines.hmodified
@@ -10,7 +10,10 @@ static uint64_t native_deadline(struct server *s,uint64_t now) {
10
if(atomic_load(&bridge.input_resync)) earlier_deadline(&next,now);
11
if(s->plan) earlier_deadline(&next,s->plan_deadline);
12
struct output *output;
−13
wl_list_for_each(output,&s->outputs,link) earlier_deadline(&next,output->gamma_retry_at);
+13
wl_list_for_each(output,&s->outputs,link) {
+14
earlier_deadline(&next,output->gamma_retry_at);
+15
earlier_deadline(&next,output->scene_retry_at);
+16
}
17
earlier_deadline(&next,s->scene_update_deadline);
18
if(s->lock_state==1) earlier_deadline(&next,s->lock_deadline);
19
if(s->has_pending_motion) earlier_deadline(&next,(uint64_t)s->motion_last+s->motion_period);
src/c/host.cmodified
@@ -160,7 +160,7 @@ struct output {
160
bool configured_enabled,gamma_pending,gamma_external,nearest_filter;
161
uint16_t *gamma_table;
162
size_t gamma_size;
−163
uint64_t gamma_token,gamma_retry_at;
+163
uint64_t gamma_token,gamma_retry_at,scene_retry_at;
164
int x,y;
165
uint64_t configuration_generation;
166
};
@@ -1296,7 +1296,24 @@ struct popup {
1296
struct wlr_scene_tree *tree;
1297
struct wl_listener commit,destroy,reposition;
1298
};
+1299
static void constrain_popup(struct popup *p) {
+1300
struct server *s=p->server;
+1301
struct view *root=find_view(s,window_for_surface(s,p->native->parent));
+1302
if(!root) return;
+1303
int x,y;
+1304
wlr_scene_node_coords(&root->tree->node,&x,&y);
+1305
struct wlr_output *output=root->layer ? root->layer->output :
+1306
wlr_output_layout_output_at(s->layout,x+root->committed.width/2.0,y+root->committed.height/2.0);
+1307
if(!output) return;
+1308
struct wlr_box box;
+1309
wlr_output_layout_get_box(s->layout,output,&box);
+1310
/* wlroots resolves nested popup offsets from the root surface origin. */
+1311
box.x-=x-root->committed.x;
+1312
box.y-=y-root->committed.y;
+1313
wlr_xdg_popup_unconstrain_from_box(p->native,&box);
+1314
}
1315
static void popup_request(struct popup *p) {
+1316
constrain_popup(p);
1317
struct wlr_box box=p->native->scheduled.geometry;
1318
p->deadline=now_ms()+bridge.request_deadline;
1319
EVENT(80,p->id,p->parent,box.x,box.y,box.width,box.height);
@@ -1354,6 +1371,7 @@ static bool configure_popup(struct server *s,int64_t *w) {
1371
wl_list_for_each(p,&s->popups,link) {
1372
if(p->id!=(uint64_t)w[1] || !p->native->base->initialized) continue;
1373
p->native->scheduled.geometry=(struct wlr_box){w[2],w[3],w[4],w[5]};
+1374
constrain_popup(p);
1375
wlr_xdg_surface_schedule_configure(p->native->base);
1376
p->deadline=0;
1377
return true;
@@ -1555,8 +1573,14 @@ static void output_frame(struct wl_listener *listener,void *data) {
1573
}
1574
wlr_scene_output_for_each_buffer(o->scene,output_buffer_filter,o);
1575
lock_arm(o);
−1558
if(o->gamma_pending) output_commit_gamma(o);
−1559
else wlr_scene_output_commit(o->scene,NULL);
+1576
bool committed=o->gamma_pending ? output_commit_gamma(o) : wlr_scene_output_commit(o->scene,NULL);
+1577
if(!committed) {
+1578
/* Retain damage and callbacks until a replacement frame is accepted. */
+1579
wlr_damage_ring_add_whole(&o->scene->damage_ring);
+1580
if(!o->gamma_pending) o->scene_retry_at=now_ms()+16;
+1581
return;
+1582
}
+1583
o->scene_retry_at=0;
1584
struct timespec now;
1585
clock_gettime(CLOCK_MONOTONIC,&now);
1586
wlr_scene_output_send_frame_done(o->scene,&now);
@@ -2214,6 +2238,12 @@ static int timer_tick(void *data) {
2238
data_expire(s);
2239
output_controls_expire(s);
2240
output_gamma_expire(s);
+2241
struct output *output;
+2242
wl_list_for_each(output,&s->outputs,link) {
+2243
if(!output->scene_retry_at || now_ms()<output->scene_retry_at) continue;
+2244
output->scene_retry_at=0;
+2245
if(output->native->enabled) wlr_output_schedule_frame(output->native);
+2246
}
2247
output_requests_expire(s);
2248
pointer_hints_expire(s);
2249
struct popup *p,*next;
src/c/output-gamma.hmodified
@@ -68,16 +68,15 @@ static void output_gamma_expire(struct server *s) {
68
if(o->native->enabled) wlr_output_schedule_frame(o->native);
69
}
70
}
−71
static void output_commit_gamma(struct output *o) {
+71
static bool output_commit_gamma(struct output *o) {
72
/* A page flip must finish before another buffer/gamma commit. Never issue
73
* a second commit from this frame callback after a runtime failure. */
−74
if(o->native->frame_pending) return;
+74
if(o->native->frame_pending) return false;
75
o->gamma_retry_at=0;
76
struct wlr_gamma_control_v1 *control=output_gamma_control(o);
77
if(control && !control->table) {
78
o->gamma_pending=false;
−79
wlr_scene_output_commit(o->scene,NULL);
−80
return;
+79
return wlr_scene_output_commit(o->scene,NULL);
80
}
81
struct wlr_output_state state;
82
wlr_output_state_init(&state);
@@ -93,7 +92,7 @@ static void output_commit_gamma(struct output *o) {
92
* or revoke a client's ownership for an unrelated backend fault. */
93
wlr_output_state_finish(&state);
94
o->gamma_retry_at=now_ms()+16;
−96
return;
+95
return false;
96
}
97
o->gamma_pending=false;
98
if(control) wlr_gamma_control_v1_send_failed_and_destroy(control);
@@ -109,4 +108,5 @@ static void output_commit_gamma(struct output *o) {
108
* Retry on the next backend frame rather than racing the page flip. */
109
o->gamma_retry_at=now_ms()+16;
110
}
+111
return committed;
112
}
test/damage-client.cadded
@@ -0,0 +1,134 @@
+1
/* SPDX-License-Identifier: BSD-3-Clause */
+2
#define _POSIX_C_SOURCE 200809L
+3
#include <errno.h>
+4
#include <poll.h>
+5
#include <stdbool.h>
+6
#include <stdint.h>
+7
#include <stdio.h>
+8
#include <stdlib.h>
+9
#include <string.h>
+10
#include <sys/mman.h>
+11
#include <time.h>
+12
#include <unistd.h>
+13
#include <wayland-client.h>
+14
#include "xdg-shell-client-protocol.h"
+15
+16
static struct wl_display *display;
+17
static struct wl_compositor *compositor;
+18
static struct wl_shm *shm;
+19
static struct xdg_wm_base *shell;
+20
static struct wl_surface *surface;
+21
static int width = 150, height = 150;
+22
static unsigned commits, callbacks;
+23
static bool configured, failed;
+24
static double milliseconds(void) {
+25
struct timespec now;
+26
clock_gettime(CLOCK_MONOTONIC, &now);
+27
return now.tv_sec * 1000.0 + now.tv_nsec / 1e6;
+28
}
+29
static void require(bool ok, const char *message) {
+30
if (ok) return;
+31
fprintf(stderr, "damage client: %s\n", message);
+32
exit(2);
+33
}
+34
static void done(void *data, struct wl_callback *callback, uint32_t time) {
+35
(void)time;
+36
unsigned sequence = (uintptr_t)data;
+37
wl_callback_destroy(callback);
+38
callbacks++;
+39
if (sequence != callbacks) failed = true;
+40
printf("DAMAGE_FRAME_DONE=%u\n", sequence);
+41
fflush(stdout);
+42
}
+43
static const struct wl_callback_listener frame_listener = {.done = done};
+44
static void draw(bool initial) {
+45
char path[] = "/tmp/sigil-damage-client.XXXXXX";
+46
int fd = mkstemp(path);
+47
unlink(path);
+48
require(fd >= 0, "shm file");
+49
size_t size = (size_t)width * height * 4;
+50
require(ftruncate(fd, size) == 0, "shm size");
+51
uint32_t *pixels = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+52
require(pixels != MAP_FAILED, "shm map");
+53
uint32_t colour = commits % 2 ? 0xffeeeeee : 0xff222222;
+54
for (int y = 0; y < height; y++)
+55
for (int x = 0; x < width; x++)
+56
pixels[y * width + x] = x >= 20 && x < 28 && y >= 20 && y < 36 ? colour : 0xff222222;
+57
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size);
+58
struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height, width * 4, WL_SHM_FORMAT_XRGB8888);
+59
wl_shm_pool_destroy(pool);
+60
munmap(pixels, size); close(fd);
+61
if (!initial && commits == 3) {
+62
const char *barrier = getenv("SIGIL_WM_FRAME_FAILURE");
+63
if (barrier) { FILE *file = fopen(barrier, "w"); require(file != NULL, "failure barrier"); fclose(file); }
+64
}
+65
wl_surface_attach(surface, buffer, 0, 0);
+66
if (initial) wl_surface_damage_buffer(surface, 0, 0, width, height);
+67
else wl_surface_damage_buffer(surface, 20, 20, 8, 16);
+68
commits++;
+69
wl_callback_add_listener(wl_surface_frame(surface), &frame_listener, (void *)(uintptr_t)commits);
+70
wl_surface_commit(surface);
+71
wl_buffer_destroy(buffer);
+72
printf("DAMAGE_COMMIT=%u colour=%06x damage=%s\n", commits, colour & 0xffffff, initial ? "whole" : "20,20,8,16");
+73
fflush(stdout);
+74
}
+75
static void configure(void *data, struct xdg_surface *xdg, uint32_t serial) {
+76
(void)data;
+77
xdg_surface_ack_configure(xdg, serial);
+78
if (!configured) { configured = true; draw(true); }
+79
else wl_surface_commit(surface);
+80
}
+81
static const struct xdg_surface_listener surface_listener = {.configure = configure};
+82
static void top_configure(void *data, struct xdg_toplevel *top, int32_t w, int32_t h, struct wl_array *states) {
+83
(void)data; (void)top; (void)states;
+84
if (w) width = w;
+85
if (h) height = h;
+86
}
+87
static void close_top(void *data, struct xdg_toplevel *top) { (void)data; (void)top; failed = true; }
+88
static const struct xdg_toplevel_listener top_listener = {.configure = top_configure, .close = close_top};
+89
static void ping(void *data, struct xdg_wm_base *base, uint32_t serial) { (void)data; xdg_wm_base_pong(base, serial); }
+90
static const struct xdg_wm_base_listener shell_listener = {.ping = ping};
+91
static void global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
+92
(void)data; (void)version;
+93
if (!strcmp(interface, "wl_compositor")) compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 4);
+94
else if (!strcmp(interface, "wl_shm")) shm = wl_registry_bind(registry, name, &wl_shm_interface, 1);
+95
else if (!strcmp(interface, "xdg_wm_base")) {
+96
shell = wl_registry_bind(registry, name, &xdg_wm_base_interface, 1);
+97
xdg_wm_base_add_listener(shell, &shell_listener, NULL);
+98
}
+99
}
+100
static void removed(void *data, struct wl_registry *registry, uint32_t name) { (void)data; (void)registry; (void)name; }
+101
static const struct wl_registry_listener registry_listener = {.global = global, .global_remove = removed};
+102
int main(void) {
+103
setvbuf(stdout, NULL, _IOLBF, 0);
+104
display = wl_display_connect(NULL);
+105
require(display != NULL, "connect");
+106
struct wl_registry *registry = wl_display_get_registry(display);
+107
wl_registry_add_listener(registry, &registry_listener, NULL);
+108
require(wl_display_roundtrip(display) >= 0 && compositor && shm && shell, "registry");
+109
surface = wl_compositor_create_surface(compositor);
+110
struct xdg_surface *xdg = xdg_wm_base_get_xdg_surface(shell, surface);
+111
xdg_surface_add_listener(xdg, &surface_listener, NULL);
+112
struct xdg_toplevel *top = xdg_surface_get_toplevel(xdg);
+113
xdg_toplevel_add_listener(top, &top_listener, NULL);
+114
wl_surface_commit(surface);
+115
double end = milliseconds() + 5000, next = milliseconds() + 200;
+116
while (!failed && milliseconds() < end && callbacks < 12) {
+117
require(wl_display_dispatch_pending(display) >= 0, "dispatch pending");
+118
require(wl_display_flush(display) >= 0 || errno == EAGAIN, "flush");
+119
struct pollfd fd = {.fd = wl_display_get_fd(display), .events = POLLIN};
+120
int result = poll(&fd, 1, 10);
+121
require(result >= 0 || errno == EINTR, "poll");
+122
if (fd.revents & POLLIN) require(wl_display_dispatch(display) >= 0, "dispatch");
+123
if (configured && commits < 12 && milliseconds() >= next) {
+124
if (callbacks != commits) { fprintf(stderr, "DAMAGE_MISSING_CALLBACK commit=%u callbacks=%u\n", commits, callbacks); failed = true; }
+125
else draw(false);
+126
next = milliseconds() + 120;
+127
}
+128
}
+129
bool passed = !failed && commits == 12 && callbacks == commits;
+130
xdg_toplevel_destroy(top); xdg_surface_destroy(xdg); wl_surface_destroy(surface);
+131
wl_display_flush(display); wl_display_disconnect(display);
+132
printf("DAMAGE_CALLBACKS=%s commits=%u callbacks=%u\n", passed ? "PASS" : "FAIL", commits, callbacks);
+133
return passed ? 0 : 1;
+134
}
test/deadlines.cmodified
@@ -52,6 +52,7 @@ static void selection(void) {
52
struct data_request d={.deadline=1011}; wl_list_insert(&s.data_requests,&d.link);
53
assert(native_deadline(&s,now)==1011); wl_list_remove(&d.link);
54
struct output o={.id=7}; wl_list_insert(&s.outputs,&o.link);
+55
o.scene_retry_at=1012; assert(native_deadline(&s,now)==1012); o.scene_retry_at=0;
56
struct output_control_request c={.output=7,.deadline=1012}; wl_list_insert(&s.output_controls,&c.link);
57
assert(native_deadline(&s,now)==1012); c.resolved=true;
58
assert(native_deadline(&s,now)==0); wl_list_remove(&c.link); wl_list_remove(&o.link);
test/frame-fault.cadded
@@ -0,0 +1,41 @@
+1
/* SPDX-License-Identifier: BSD-3-Clause */
+2
#define _GNU_SOURCE
+3
#include <dlfcn.h>
+4
#include <errno.h>
+5
#include <stdio.h>
+6
#include <stdlib.h>
+7
#include <unistd.h>
+8
#include <wlr/interfaces/wlr_output.h>
+9
#include <wlr/types/wlr_compositor.h>
+10
+11
bool wlr_output_commit_state(struct wlr_output *output, const struct wlr_output_state *state) {
+12
const char *barrier = getenv("SIGIL_WM_FRAME_FAILURE");
+13
if ((state->committed & WLR_OUTPUT_STATE_BUFFER) && barrier && unlink(barrier) == 0) {
+14
fprintf(stderr, "FRAME_COMMIT_EBUSY output=%s sequence=%u\n", output->name, output->commit_seq + 1);
+15
errno = EBUSY;
+16
return false;
+17
}
+18
typedef bool (*commit_fn)(struct wlr_output *, const struct wlr_output_state *);
+19
commit_fn commit = (commit_fn)dlsym(RTLD_NEXT, "wlr_output_commit_state");
+20
if (!commit) abort();
+21
bool accepted = commit(output, state);
+22
if (accepted && (state->committed & WLR_OUTPUT_STATE_BUFFER))
+23
fprintf(stderr, "FRAME_COMMIT_ACCEPTED output=%s sequence=%u\n", output->name, output->commit_seq);
+24
return accepted;
+25
}
+26
void wlr_output_send_present(struct wlr_output *output, struct wlr_output_event_present *event) {
+27
fprintf(stderr, "FRAME_PRESENT output=%s sequence=%u presented=%d\n", output->name, event->commit_seq, event->presented);
+28
typedef void (*present_fn)(struct wlr_output *, struct wlr_output_event_present *);
+29
present_fn present = (present_fn)dlsym(RTLD_NEXT, "wlr_output_send_present");
+30
if (!present) abort();
+31
present(output, event);
+32
}
+33
void wlr_surface_send_frame_done(struct wlr_surface *surface, const struct timespec *when) {
+34
if (!wl_list_empty(&surface->current.frame_callback_list))
+35
fprintf(stderr, "FRAME_CALLBACK sequence=%u callbacks=%d\n", surface->current.seq,
+36
wl_list_length(&surface->current.frame_callback_list));
+37
typedef void (*done_fn)(struct wlr_surface *, const struct timespec *);
+38
done_fn done = (done_fn)dlsym(RTLD_NEXT, "wlr_surface_send_frame_done");
+39
if (!done) abort();
+40
done(surface, when);
+41
}
test/popup-client.cadded
@@ -0,0 +1,199 @@
+1
/* SPDX-License-Identifier: BSD-3-Clause */
+2
#define _POSIX_C_SOURCE 200809L
+3
#include <fcntl.h>
+4
#include <stdbool.h>
+5
#include <stdint.h>
+6
#include <stdio.h>
+7
#include <stdlib.h>
+8
#include <string.h>
+9
#include <sys/mman.h>
+10
#include <unistd.h>
+11
#include <wayland-client.h>
+12
#include "xdg-shell-client-protocol.h"
+13
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
+14
+15
struct surface {
+16
struct wl_surface *wl;
+17
struct xdg_surface *xdg;
+18
struct xdg_popup *popup;
+19
int width, height;
+20
bool child;
+21
bool frame_pending;
+22
int frame_phase;
+23
};
+24
static struct wl_display *display;
+25
static struct wl_compositor *compositor;
+26
static struct wl_shm *shm;
+27
static struct xdg_wm_base *shell;
+28
static struct zwlr_layer_shell_v1 *layers;
+29
static struct zwlr_layer_surface_v1 *layer;
+30
static struct surface parent, popup;
+31
static int phase, configured_phase, completed_phase = -1, checks, failures;
+32
static bool running = true;
+33
+34
static void require(bool ok, const char *message) {
+35
if (ok) return;
+36
fprintf(stderr, "popup client: %s\n", message);
+37
exit(2);
+38
}
+39
static struct xdg_positioner *positioner(void) {
+40
struct xdg_positioner *p = xdg_wm_base_create_positioner(shell);
+41
xdg_positioner_set_size(p, 160, 100);
+42
xdg_positioner_set_anchor_rect(p, phase == 2 ? 0 : 140, phase == 2 ? 0 : 140, 1, 1);
+43
xdg_positioner_set_anchor(p, phase == 2 ? XDG_POSITIONER_ANCHOR_TOP_LEFT : XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT);
+44
xdg_positioner_set_gravity(p, phase == 2 ? XDG_POSITIONER_GRAVITY_TOP_LEFT : XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);
+45
uint32_t adjust = XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y;
+46
if (phase != 1) adjust |= XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y;
+47
xdg_positioner_set_constraint_adjustment(p, adjust);
+48
return p;
+49
}
+50
static void open_popup(void);
+51
static void frame(void *data, struct wl_callback *callback, uint32_t time) {
+52
(void)time;
+53
struct surface *s = data;
+54
wl_callback_destroy(callback);
+55
s->frame_pending = false;
+56
if (!s->child) {
+57
if (!popup.wl) open_popup();
+58
} else if (s->frame_phase <= completed_phase) return;
+59
else if ((phase = (completed_phase = s->frame_phase) + 1) < 3) {
+60
struct xdg_positioner *p = positioner();
+61
xdg_popup_reposition(popup.popup, p, phase);
+62
xdg_positioner_destroy(p);
+63
} else running = false;
+64
}
+65
static const struct wl_callback_listener frame_listener = {.done = frame};
+66
static void draw(struct surface *s) {
+67
char path[] = "/tmp/sigil-popup-client.XXXXXX";
+68
int fd = mkstemp(path);
+69
unlink(path);
+70
require(fd >= 0, "shm file");
+71
int margin_x = s->child || layer ? 0 : 17;
+72
int margin_y = s->child || layer ? 0 : 11;
+73
int width = s->width + margin_x * 2, height = s->height + margin_y * 2;
+74
size_t size = (size_t)width * height * 4;
+75
require(ftruncate(fd, size) == 0, "shm size");
+76
uint32_t *pixels = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+77
require(pixels != MAP_FAILED, "shm map");
+78
for (size_t i = 0; i < size / 4; i++) pixels[i] = s->child ? 0xffeeaa22 : 0xff00cc44;
+79
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size);
+80
struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height, width * 4, WL_SHM_FORMAT_XRGB8888);
+81
wl_shm_pool_destroy(pool);
+82
munmap(pixels, size);
+83
close(fd);
+84
if (s->xdg) xdg_surface_set_window_geometry(s->xdg, margin_x, margin_y, s->width, s->height);
+85
wl_surface_attach(s->wl, buffer, 0, 0);
+86
wl_surface_damage_buffer(s->wl, 0, 0, width, height);
+87
if (!s->frame_pending && (!s->child || configured_phase > completed_phase)) {
+88
s->frame_pending = true;
+89
s->frame_phase = configured_phase;
+90
wl_callback_add_listener(wl_surface_frame(s->wl), &frame_listener, s);
+91
}
+92
wl_surface_commit(s->wl);
+93
wl_buffer_destroy(buffer);
+94
}
+95
static void configured(void *data, struct xdg_surface *xdg, uint32_t serial) {
+96
xdg_surface_ack_configure(xdg, serial);
+97
draw(data);
+98
}
+99
static const struct xdg_surface_listener surface_listener = {.configure = configured};
+100
static void popup_configure(void *data, struct xdg_popup *role, int32_t x, int32_t y, int32_t width, int32_t height) {
+101
(void)data; (void)role;
+102
popup.width = width; popup.height = height;
+103
/* Both outputs share this parent-relative box, including the nonzero output origin. */
+104
bool inside = x >= -480 && y >= -290 && x + width <= 160 && y + height <= 190;
+105
bool adjustment = configured_phase == 0 ? x == -20 && y == 40 :
+106
configured_phase == 1 ? x == 0 && y == 90 : x == -160 && y == -100;
+107
checks |= 1 << configured_phase;
+108
failures += !inside || !adjustment;
+109
printf("POPUP_%s=%s phase=%d geometry=%d,%d,%d,%d inside=%d expected-adjustment=%d\n",
+110
layer ? "LAYER" : "TOPLEVEL", inside && adjustment ? "PASS" : "FAIL", configured_phase,
+111
x, y, width, height, inside, adjustment);
+112
fflush(stdout);
+113
}
+114
static void popup_done(void *data, struct xdg_popup *role) {
+115
(void)data; (void)role;
+116
require(false, "unexpected dismissal");
+117
}
+118
static void repositioned(void *data, struct xdg_popup *role, uint32_t token) {
+119
(void)data; (void)role;
+120
require(token == (uint32_t)phase, "reposition token");
+121
configured_phase = token;
+122
}
+123
static const struct xdg_popup_listener popup_listener = {
+124
.configure = popup_configure, .popup_done = popup_done, .repositioned = repositioned,
+125
};
+126
static void open_popup(void) {
+127
popup = (struct surface){.wl = wl_compositor_create_surface(compositor), .child = true};
+128
popup.xdg = xdg_wm_base_get_xdg_surface(shell, popup.wl);
+129
xdg_surface_add_listener(popup.xdg, &surface_listener, &popup);
+130
struct xdg_positioner *p = positioner();
+131
popup.popup = xdg_surface_get_popup(popup.xdg, parent.xdg, p);
+132
if (layer) zwlr_layer_surface_v1_get_popup(layer, popup.popup);
+133
xdg_popup_add_listener(popup.popup, &popup_listener, &popup);
+134
xdg_positioner_destroy(p);
+135
wl_surface_commit(popup.wl);
+136
}
+137
static void top_configure(void *data, struct xdg_toplevel *top, int32_t width, int32_t height, struct wl_array *states) {
+138
(void)data; (void)top; (void)states;
+139
if (width) parent.width = width;
+140
if (height) parent.height = height;
+141
}
+142
static void top_close(void *data, struct xdg_toplevel *top) { (void)data; (void)top; running = false; }
+143
static const struct xdg_toplevel_listener top_listener = {.configure = top_configure, .close = top_close};
+144
static void layer_configure(void *data, struct zwlr_layer_surface_v1 *role, uint32_t serial, uint32_t width, uint32_t height) {
+145
(void)data;
+146
parent.width = width; parent.height = height;
+147
zwlr_layer_surface_v1_ack_configure(role, serial);
+148
draw(&parent);
+149
}
+150
static void layer_closed(void *data, struct zwlr_layer_surface_v1 *role) { (void)data; (void)role; running = false; }
+151
static const struct zwlr_layer_surface_v1_listener layer_listener = {.configure = layer_configure, .closed = layer_closed};
+152
static void ping(void *data, struct xdg_wm_base *base, uint32_t serial) { (void)data; xdg_wm_base_pong(base, serial); }
+153
static const struct xdg_wm_base_listener shell_listener = {.ping = ping};
+154
static void global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
+155
(void)data;
+156
if (!strcmp(interface, "wl_compositor")) compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 4);
+157
else if (!strcmp(interface, "wl_shm")) shm = wl_registry_bind(registry, name, &wl_shm_interface, 1);
+158
else if (!strcmp(interface, "xdg_wm_base")) {
+159
shell = wl_registry_bind(registry, name, &xdg_wm_base_interface, version < 3 ? version : 3);
+160
xdg_wm_base_add_listener(shell, &shell_listener, NULL);
+161
} else if (!strcmp(interface, "zwlr_layer_shell_v1")) layers = wl_registry_bind(registry, name, &zwlr_layer_shell_v1_interface, 1);
+162
}
+163
static void removed(void *data, struct wl_registry *registry, uint32_t name) { (void)data; (void)registry; (void)name; }
+164
static const struct wl_registry_listener registry_listener = {.global = global, .global_remove = removed};
+165
int main(int argc, char **argv) {
+166
require(argc == 2, "usage: popup-client toplevel|layer");
+167
display = wl_display_connect(NULL);
+168
require(display != NULL, "connect");
+169
struct wl_registry *registry = wl_display_get_registry(display);
+170
wl_registry_add_listener(registry, &registry_listener, NULL);
+171
require(wl_display_roundtrip(display) >= 0 && compositor && shm && shell, "registry");
+172
parent.wl = wl_compositor_create_surface(compositor);
+173
parent.width = parent.height = 150;
+174
struct xdg_toplevel *top = NULL;
+175
if (!strcmp(argv[1], "layer")) {
+176
require(layers != NULL, "layer shell");
+177
layer = zwlr_layer_shell_v1_get_layer_surface(layers, parent.wl, NULL, ZWLR_LAYER_SHELL_V1_LAYER_TOP, "popup-test");
+178
zwlr_layer_surface_v1_add_listener(layer, &layer_listener, NULL);
+179
zwlr_layer_surface_v1_set_size(layer, 150, 150);
+180
} else {
+181
require(!strcmp(argv[1], "toplevel"), "unknown role");
+182
parent.xdg = xdg_wm_base_get_xdg_surface(shell, parent.wl);
+183
xdg_surface_add_listener(parent.xdg, &surface_listener, &parent);
+184
top = xdg_surface_get_toplevel(parent.xdg);
+185
xdg_toplevel_add_listener(top, &top_listener, NULL);
+186
}
+187
wl_surface_commit(parent.wl);
+188
while (running && wl_display_dispatch(display) >= 0) {}
+189
require(checks == 7, "missing popup configurations");
+190
xdg_popup_destroy(popup.popup);
+191
xdg_surface_destroy(popup.xdg);
+192
wl_surface_destroy(popup.wl);
+193
if (top) { xdg_toplevel_destroy(top); xdg_surface_destroy(parent.xdg); }
+194
if (layer) zwlr_layer_surface_v1_destroy(layer);
+195
wl_surface_destroy(parent.wl);
+196
wl_display_flush(display);
+197
wl_display_disconnect(display);
+198
return failures ? 1 : 0;
+199
}
test/popup-constraints.cadded
@@ -0,0 +1,109 @@
+1
/* SPDX-License-Identifier: BSD-3-Clause */
+2
#include <errno.h>
+3
#include <poll.h>
+4
#include <signal.h>
+5
#include <stdbool.h>
+6
#include <stdint.h>
+7
#include <stdio.h>
+8
#include <stdlib.h>
+9
#include <string.h>
+10
#include <sys/wait.h>
+11
#include <time.h>
+12
#include <unistd.h>
+13
#include "api.h"
+14
+15
static pid_t child;
+16
static void cleanup(void) {
+17
if (child > 0) { kill(child, SIGKILL); waitpid(child, NULL, 0); }
+18
}
+19
static void require(bool ok, const char *message) {
+20
if (ok) return;
+21
fprintf(stderr, "popup constraints: %s\n", message);
+22
exit(2);
+23
}
+24
static void send_words(int64_t *words, size_t count) {
+25
struct wm_message command = {0};
+26
require(count <= 16, "command size");
+27
memcpy(command.word, words, count * sizeof(*words));
+28
require(wm_send(&command) == 1, "command refused");
+29
}
+30
#define SEND(...) send_words((int64_t[]){__VA_ARGS__}, sizeof((int64_t[]){__VA_ARGS__}) / sizeof(int64_t))
+31
static double seconds(void) {
+32
struct timespec now;
+33
clock_gettime(CLOCK_MONOTONIC, &now);
+34
return now.tv_sec + now.tv_nsec / 1e9;
+35
}
+36
int main(int argc, char **argv) {
+37
require(argc == 3, "usage: popup-constraints toplevel|layer 0|1");
+38
int selected = atoi(argv[2]);
+39
require(selected == 0 || selected == 1, "output selection");
+40
atexit(cleanup);
+41
setvbuf(stdout, NULL, _IOLBF, 0);
+42
require(wm_options(32, 2000) == 1 && wm_start() == 1, "start");
+43
int outputs = 0, configured = 0, result = 2;
+44
int64_t chosen = 0;
+45
bool running = true, layer_configured = false;
+46
int64_t admitted_popup = 0;
+47
double deadline = seconds() + 20;
+48
while (running && seconds() < deadline) {
+49
struct pollfd fd = {.fd = wm_event_fd(), .events = POLLIN};
+50
require(poll(&fd, 1, 50) >= 0 || errno == EINTR, "event poll");
+51
struct wm_message event;
+52
while (wm_next(&event)) {
+53
int64_t *w = event.word;
+54
switch (w[0]) {
+55
case 2: {
+56
if (outputs == selected) chosen = w[1];
+57
int64_t output[] = {w[1], 1, 100 + outputs * 640, 60, 1000, 0, -1, 640, 480, 60000};
+58
require(wm_submit(2, ++outputs, output, 10) == 1, "output configuration");
+59
break;
+60
}
+61
case 24:
+62
require(w[3] == 1, "output rejected");
+63
if (++configured == 2) {
+64
require(outputs == 2 && chosen, "two outputs");
+65
SEND(25, chosen);
+66
require(setenv("WAYLAND_DISPLAY", wm_socket(), 1) == 0, "socket name");
+67
child = fork();
+68
require(child >= 0, "fork");
+69
if (!child) { execl("build/popup-client", "popup-client", argv[1], NULL); _exit(127); }
+70
}
+71
break;
+72
case 14: SEND(2, w[1], 150, 150); break;
+73
case 19:
+74
if (w[14] && !layer_configured) {
+75
layer_configured = true;
+76
SEND(24, w[1], chosen, 150, 150);
+77
}
+78
break;
+79
case 11: {
+80
int64_t plan[] = {1, 1, w[1], 200, w[1], 580 + selected * 640, 350, 150, 150, 1, 1, 0, 0, 0};
+81
require(wm_submit(1, 10, plan, 14) == 1, "parent placement");
+82
break;
+83
}
+84
case 80:
+85
if (admitted_popup != w[1]) {
+86
admitted_popup = w[1];
+87
SEND(20, w[1], w[3], w[4], w[5], w[6]);
+88
}
+89
break;
+90
case 13: {
+91
int status;
+92
require(child > 0 && waitpid(child, &status, 0) == child, "client wait");
+93
child = 0;
+94
result = WIFEXITED(status) ? WEXITSTATUS(status) : 2;
+95
SEND(1);
+96
break;
+97
}
+98
case 7: require(w[2] == 0, "native failure"); running = false; break;
+99
case 23: require(false, "plan rejected"); break;
+100
case 98: require(false, "command rejected"); break;
+101
}
+102
}
+103
}
+104
require(!running, "timeout waiting for client");
+105
require(wm_join() == 1, "join");
+106
require(wm_stat(1) == 0 && wm_stat(2) == 0 && wm_stat(3) == 0 && wm_stat(7) == 0, "retained resources");
+107
printf("POPUP_CONSTRAINTS=%s role=%s output=%d\n", result == 0 ? "PASS" : "FAIL", argv[1], selected);
+108
return result;
+109
}
test/surface-frames.cadded
@@ -0,0 +1,137 @@
+1
/* SPDX-License-Identifier: BSD-3-Clause */
+2
#include <errno.h>
+3
#include <poll.h>
+4
#include <signal.h>
+5
#include <stdbool.h>
+6
#include <stdint.h>
+7
#include <stdio.h>
+8
#include <stdlib.h>
+9
#include <string.h>
+10
#include <sys/wait.h>
+11
#include <time.h>
+12
#include <unistd.h>
+13
#include "api.h"
+14
+15
static pid_t child;
+16
static void cleanup(void) { if (child > 0) { kill(child, SIGKILL); waitpid(child, NULL, 0); } }
+17
static void require(bool ok, const char *message) {
+18
if (ok) return;
+19
fprintf(stderr, "surface frames: %s\n", message);
+20
exit(2);
+21
}
+22
static void command(int kind, int64_t id, int64_t a, int64_t b) {
+23
struct wm_message m = {.word = {kind, id, a, b}};
+24
require(wm_send(&m) == 1, "command refused");
+25
}
+26
static double seconds(void) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); return now.tv_sec + now.tv_nsec / 1e9; }
+27
int main(int argc, char **argv) {
+28
require(argc == 3, "usage: surface-frames SAMPLE-PATH CLIP");
+29
bool foot = getenv("SIGIL_WM_FRAMES_FOOT") != NULL;
+30
bool modern = getenv("SIGIL_WM_FOOT_MODERN") != NULL;
+31
setvbuf(stdout, NULL, _IOLBF, 0);
+32
atexit(cleanup);
+33
require(wm_options(foot ? 8 : 0, 2000) == 1 && wm_decoration_default(2) == 1 && wm_start() == 1, "start");
+34
bool running = true, injected = false;
+35
int result = 2;
+36
double deadline = seconds() + 12;
+37
while (running && seconds() < deadline) {
+38
struct pollfd fd = {.fd = wm_event_fd(), .events = POLLIN};
+39
require(poll(&fd, 1, 50) >= 0 || errno == EINTR, "event poll");
+40
struct wm_message event;
+41
while (wm_next(&event)) {
+42
int64_t *w = event.word;
+43
switch (w[0]) {
+44
case 1: {
+45
int64_t keyboard[] = {1, 25, 600, 2, 'u', 's'};
+46
require(wm_submit(3, 3, keyboard, 6) == 1, "keyboard configuration");
+47
command(14, 3, 0, 0);
+48
break;
+49
}
+50
case 2: {
+51
int64_t output[] = {w[1], 1, 0, 0, 1000, 0, -1, 320, 240, w[5] ? 0 : 60000};
+52
require(wm_submit(2, 1, output, 10) == 1, "output configuration");
+53
break;
+54
}
+55
case 4: {
+56
int64_t output[] = {w[1], 1, 0, 0, 1000, 0, -1, w[2], w[3], 0};
+57
require(wm_submit(2, 1, output, 10) == 1, "nested output configuration");
+58
break;
+59
}
+60
case 6:
+61
if (!child) {
+62
size_t length = strlen(argv[1]);
+63
require(length < 1024, "sample path");
+64
int64_t samples[1028] = {60, 1, 30, 30};
+65
if (foot) { samples[0] = 120; samples[2] = 15; samples[3] = 18; }
+66
for (size_t i = 0; i < length; i++) samples[i + 4] = (unsigned char)argv[1][i];
+67
require(wm_submit(15, w[1], samples, length + 4) == 1, "sample recording");
+68
require(setenv("WAYLAND_DISPLAY", wm_socket(), 1) == 0, "socket name");
+69
child = fork();
+70
require(child >= 0, "fork");
+71
if (!child) {
+72
if (foot) execlp("foot", "foot", "--config=/dev/null",
+73
"--override=cursor.blink=yes", "--override=cursor.blink-rate=120",
+74
"--override=pad=0x0", "--override=font=JetBrains Mono:size=12",
+75
modern ? "--override=colors-dark.background=222222" : "--override=colors.background=222222",
+76
modern ? "--override=colors-dark.foreground=eeeeee" : "--override=colors.foreground=eeeeee",
+77
modern ? "--override=colors-dark.cursor=222222 eeeeee" : "--override=cursor.color=222222 eeeeee",
+78
"--", "sh", "-c",
+79
"if [ -n \"$SIGIL_WM_FRAMES_TMUX\" ]; then exec tmux -S \"$XDG_RUNTIME_DIR/blink-tmux\" -f /dev/null new-session \"printf '\\033[2J\\033[H\\033[?25h'; sleep 3\"; else printf '\033[2J\033[H\033[?25h'; sleep 3; fi", NULL);
+80
else execl("build/damage-client", "damage-client", NULL);
+81
_exit(127);
+82
}
+83
}
+84
break;
+85
case 14: command(2, w[1], 150, 150); break;
+86
case 75: command(18, w[1], 2, 0); break;
+87
case 22:
+88
if (foot && !injected && getenv("SIGIL_WM_FRAME_FAILURE")) {
+89
FILE *barrier = fopen(getenv("SIGIL_WM_FRAME_FAILURE"), "w");
+90
require(barrier != NULL, "foot failure barrier");
+91
fclose(barrier);
+92
injected = true;
+93
}
+94
break;
+95
case 11: {
+96
int64_t plan[] = {1, 1, w[1], 200, w[1], 10, 10, 150, 150, 1, 1, 0, 15, atoi(argv[2])};
+97
require(wm_submit(1, 2, plan, 14) == 1, "placement");
+98
break;
+99
}
+100
case 13: {
+101
int status;
+102
require(child > 0 && waitpid(child, &status, 0) == child, "wait client");
+103
child = 0;
+104
result = WIFEXITED(status) ? WEXITSTATUS(status) : 2;
+105
command(1, 0, 0, 0);
+106
break;
+107
}
+108
case 7: require(w[2] == 0, "native failure"); running = false; break;
+109
case 23: require(false, "plan rejected"); break;
+110
case 24: require(w[3] == 1, "submission rejected"); break;
+111
case 98: require(false, "command rejected"); break;
+112
}
+113
}
+114
}
+115
require(!running && wm_join() == 1, "timeout or join");
+116
FILE *file = fopen(argv[1], "r");
+117
require(file != NULL, "missing sample log");
+118
char line[256];
+119
unsigned phases = 0, previous = 0, colour, sequence, index, width, height;
+120
while (fgets(line, sizeof(line), file)) {
+121
char status[8];
+122
require(sscanf(line, "FRAME %u %u %u %u %x %7s", &index, &sequence, &width, &height, &colour, status) == 6 &&
+123
!strcmp(status, "PASS"), "bad sample");
+124
if (colour != 0x222222 && colour != 0xeeeeee) continue;
+125
if (colour == previous) continue;
+126
if (!foot) require(colour == (phases % 2 ? 0xeeeeee : 0x222222), "phase order");
+127
previous = colour;
+128
phases++;
+129
printf("DAMAGE_PRESENT_PHASE=%u output-sequence=%u colour=%06x\n", phases, sequence, colour);
+130
}
+131
fclose(file);
+132
bool passed = !result && (foot ? phases >= 4 : phases == 12);
+133
printf("%s=%s phases=%u expected=%s\n", foot ? "FOOT_BLINK" : "DAMAGE_PRESENTATION",
+134
passed ? "PASS" : "FAIL", phases, foot ? "at-least-4" : "12");
+135
require(wm_stat(1) == 0 && wm_stat(2) == 0 && wm_stat(3) == 0 && wm_stat(7) == 0, "retained resources");
+136
return result ? result : passed ? 0 : 1;
+137
}