Commit08807ea2Recorded29 Oct 2026Repositorysigil-wm

Merge input reply backpressure and receiver lifecycle fixes

Changed
 Makefile                       |   3 +++
 README.md                      |   8 ++++++++
 docs/api.md                    |   6 ++++++
 docs/design.md                 |  14 ++++++++++++++
 scripts/test-input-reply-queue |  35 +++++++++++++++++++++++++++++++++++
 src/c/api.h                    |   3 +++
 src/c/host.c                   |  33 +++++++++++++++++++++++++++++++--
 src/sigil/wm/bridge.sgl        |  10 +++++++++-
 src/sigil/wm/input.sgl         |  20 ++++++++++++--------
 src/sigil/wm/runtime.sgl       |   6 +++++-
 test/input-queue-gate.c        | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test/input-reply-queue.sgl     | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 12 files changed, 347 insertions(+), 12 deletions(-)
Diff

A merge. Shown against its first parent, so this is the effect of merging rather than the work of the branch.

Makefilemodified
@@ -8,6 +8,9 @@ PROTOCOLS = $(shell pkg-config --variable=pkgdatadir wayland-protocols)
8
9
all: build/libsigil-wm.so build/environment-exec
10
+11
build/input-queue-gate.so: test/input-queue-gate.c build/libsigil-wm.so
+12
$(CC) $(CFLAGS) $(CPPFLAGS) -fPIC -shared $< $(LDFLAGS) $(LDLIBS) -o $@
+13
14
build/trace-records-test: test/trace-records.c src/c/trace-records.h
15
mkdir -p build
16
$(CC) -std=c11 -O2 -Wall -Wextra -Werror -D_POSIX_C_SOURCE=200809L $< -o $@
README.mdmodified
@@ -84,6 +84,14 @@ state. The socket permits arbitrary Sigil evaluation with the user's authority.
84
The real-client tests run through `scripts/test-examples`; they retain logs,
85
composed-output captures and PNGs under `build/receipts/`.
86
+87
`scripts/test-input-reply-queue` fills the 1024-command ring while a key, button
+88
or motion hook is returning its decision. A test-only native gate pauses
+89
consumption, and another scheduler task releases it to verify cooperative
+90
backpressure, accepted replies and subsequent input progress. It also checks
+91
replies before native readiness and after shutdown with an empty ring, and
+92
checks diagnostic rate limiting. Set `SIGIL` to a runtime with FFI
+93
loading support; logs are retained under `build/receipts/input-reply-queue/`.
+94
95
`scripts/test-lock-presentation` uses a test-only presentation barrier to check
96
that a blank lock owner cannot be acknowledged before actual output
97
presentation, that ordinary pixels remain covered, and that authenticated
docs/api.mdmodified
@@ -203,6 +203,12 @@ target or output changes. Its `moved?:` field distinguishes input motion from
203
scene refresh; coordinates use `x-milli:` and `y-milli:` fields. Observation
204
chooses no focus and can run independently of interception. Disabled observation
205
and absent motion hooks generate no ordinary motion callback to Sigil.
+206
Input replies retry cooperatively when the command ring is full or native
+207
startup is still in progress. The hook runs once; its copied disposition stays
+208
in FIFO order with earlier commands. A terminated receiver ends retry without
+209
raising a queue-full error. Rate-limited `INPUT_REPLY` lines distinguish
+210
`queue-full`, `receiver-starting` and `receiver-stopped` with depth and input kind.
+211
212
Intercepted input decisions have a 32 ms native observation budget and 10 ms
213
timer granularity. An intercepted key/button over that budget is logged and
214
keeps waiting for policy; it is never forwarded by default. Missing motion
docs/design.mdmodified
@@ -98,6 +98,20 @@ hook drops or fails; dropped presses suppress their releases. Buttons use the
98
same pairing rules. Physical and client-visible modifier state are tracked
99
separately. A hook exception drops a new press and reports an error. Replies
100
carrying stale focus/lock generations are rejected, with held-state cleanup.
+101
+102
Decision replies share the 1024-entry command ring with configuration, scene
+103
and focus commands. When it fills, Sigil retains the computed reply and sleeps
+104
cooperatively for 1 ms between enqueue attempts, without repeating the hook.
+105
Keeping the reply in this FIFO preserves earlier hook commands, including focus
+106
authorization. A reserved queue or priority lane could overtake those commands.
+107
Replies also wait while the native receiver is starting. Retry ends when the
+108
reply is accepted or the receiver has shut down; shutdown cannot leave an active
+109
intercepted input waiting for that reply. `wm_send` retains its boolean contract;
+110
`wm_input_reply` distinguishes accepted, full, starting and stopped results.
+111
`INPUT_REPLY` diagnostics include the case, wait/drop action, queue depth, input
+112
kind and native state. Repeated failures are limited to one line per second for
+113
each case and kind, with the suppressed attempt count included on the next line.
+114
115
Intercepted decisions are observed after 32 ms, with at most one 10 ms timer
116
interval of granularity. An unanswered intercepted press is logged and keeps
117
waiting for policy, without exposing a manager chord to a client. Ordinary
scripts/test-input-reply-queueadded
@@ -0,0 +1,35 @@
+1
#!/usr/bin/env bash
+2
set -euo pipefail
+3
cd -P "$(dirname "$0")/.."
+4
make -j2 all build/input-queue-gate.so
+5
test_runtime=$(mktemp -d /tmp/wm-input-queue.XXXXXX)
+6
trap 'rm -rf "$test_runtime"' EXIT
+7
chmod 700 "$test_runtime"
+8
export XDG_RUNTIME_DIR=$test_runtime WLR_BACKENDS=headless WLR_RENDERER=pixman WLR_HEADLESS_OUTPUTS=1
+9
unset WAYLAND_DISPLAY DISPLAY
+10
export SIGIL_WM_LIBRARY=$PWD/build/input-queue-gate.so
+11
mkdir -p build/receipts/input-reply-queue
+12
for kind in 30 31 32; do
+13
log=build/receipts/input-reply-queue/kind-$kind.log
+14
WM_INPUT_MODE=overflow WM_INPUT_KIND=$kind timeout --foreground 15 "${SIGIL:-sigil}" -L src eval -f test/input-reply-queue.sgl > "$log" 2>&1 || { cat "$log"; exit 1; }
+15
cat "$log"
+16
grep -q "^INPUT_REPLY_QUEUE=PASS kind=$kind " "$log"
+17
! grep -q 'event handler failed' "$log"
+18
[[ $(grep -c '^INPUT_REPLY case=queue-full action=wait depth=1024 ' "$log") == 1 ]]
+19
done
+20
for kind in 30 31 32; do
+21
log=build/receipts/input-reply-queue/starting-$kind.log
+22
WM_INPUT_MODE=starting WM_INPUT_KIND=$kind timeout --foreground 15 "${SIGIL:-sigil}" -L src eval -f test/input-reply-queue.sgl > "$log" 2>&1 || { cat "$log"; exit 1; }
+23
cat "$log"
+24
grep -q "^INPUT_STARTING=PASS kind=$kind " "$log"
+25
! grep -q 'event handler failed' "$log"
+26
[[ $(grep -c '^INPUT_REPLY case=receiver-starting action=wait depth=0 ' "$log") == 1 ]]
+27
done
+28
for kind in 30 31 32; do
+29
log=build/receipts/input-reply-queue/shutdown-$kind.log
+30
WM_INPUT_MODE=shutdown WM_INPUT_KIND=$kind timeout --foreground 15 "${SIGIL:-sigil}" -L src eval -f test/input-reply-queue.sgl > "$log" 2>&1 || { cat "$log"; exit 1; }
+31
cat "$log"
+32
grep -q "^INPUT_SHUTDOWN=PASS kind=$kind" "$log"
+33
! grep -q 'event handler failed' "$log"
+34
[[ $(grep -c '^INPUT_REPLY case=receiver-stopped action=drop depth=0 ' "$log") == 1 ]]
+35
done
src/c/api.hmodified
@@ -31,6 +31,9 @@ int wm_event_fd(void);
31
int wm_next(struct wm_message *event);
32
int wm_event_text(void *bytes, int capacity);
33
int wm_send(const struct wm_message *command);
+34
enum wm_send_result { WM_SEND_INVALID=-2, WM_SEND_STOPPED=-1, WM_SEND_FULL=0, WM_SEND_ACCEPTED=1, WM_SEND_STARTING=2 };
+35
/* Reply status preserves startup waits without changing wm_send's boolean contract. */
+36
int wm_input_reply(const struct wm_message *command,int input_kind);
37
int wm_submit(int64_t kind, int64_t token, const int64_t *words, int64_t count);
38
/* Pixel metadata: x,y,logical width/height,pixel width/height,stride,stack.
39
* stack is -1 bottom, 0 preserve, 1 top, 2 ordered insertion (options required).
src/c/host.cmodified
@@ -2470,14 +2470,43 @@ int wm_next(struct wm_message *event) {
2470
}
2471
return 0;
2472
}
+2473
static int send_owned_result(struct wm_message m,unsigned *depth,int *state) {
+2474
*state=wm_state();
+2475
if(*state!=2 || bridge.joined) {
+2476
*depth=atomic_load(&bridge.commands.write)-atomic_load(&bridge.commands.read);
+2477
return *state==1 && !bridge.joined ? WM_SEND_STARTING : WM_SEND_STOPPED;
+2478
}
+2479
if(!push(&bridge.commands,m)) { *depth=QUEUE_SIZE; return WM_SEND_FULL; }
+2480
wake(bridge.command_fd); return WM_SEND_ACCEPTED;
+2481
}
2482
static int send_owned(struct wm_message m) {
−2474
if(wm_state()!=2 || bridge.joined || !push(&bridge.commands,m)) return 0;
−2475
wake(bridge.command_fd); return 1;
+2483
unsigned depth;
+2484
int state;
+2485
return send_owned_result(m,&depth,&state)==WM_SEND_ACCEPTED;
2486
}
2487
int wm_send(const struct wm_message *m) {
2488
if(!m || m->word[0]>=90 || m->word[0]<=0) return 0;
2489
return send_owned(*m);
2490
}
+2491
int wm_input_reply(const struct wm_message *m,int kind) {
+2492
/* Repeated retries must not turn backpressure into logging pressure. */
+2493
static struct { uint64_t next,suppressed; } reports[3][3];
+2494
if(!m || m->word[0]!=11 || kind<30 || kind>32) return WM_SEND_INVALID;
+2495
unsigned depth;
+2496
int state,result=send_owned_result(*m,&depth,&state);
+2497
if(result==WM_SEND_ACCEPTED) return result;
+2498
unsigned reason=result==WM_SEND_FULL ? 0 : result==WM_SEND_STARTING ? 1 : 2;
+2499
uint64_t now=now_ms();
+2500
if(now>=reports[kind-30][reason].next) {
+2501
fprintf(stderr,"INPUT_REPLY case=%s action=%s depth=%u kind=%d state=%d suppressed=%llu\n",
+2502
result==WM_SEND_FULL ? "queue-full" : result==WM_SEND_STARTING ? "receiver-starting" : "receiver-stopped",
+2503
result==WM_SEND_STOPPED ? "drop" : "wait",depth,kind,state,
+2504
(unsigned long long)reports[kind-30][reason].suppressed);
+2505
reports[kind-30][reason].next=now+1000;
+2506
reports[kind-30][reason].suppressed=0;
+2507
} else reports[kind-30][reason].suppressed++;
+2508
return result;
+2509
}
2510
int wm_submit_traced(int64_t kind,int64_t token,const int64_t *words,int64_t count,const uint64_t *causes,int trace_count) {
2511
if(!words || count<1 || count>65536 || kind<1 || kind>15) return 0;
2512
if(trace_count<0 || trace_count>TRACE_CAUSES || (trace_count && !causes)) return 0;
src/sigil/wm/bridge.sglmodified
@@ -3,7 +3,7 @@
3
(import (sigil ffi) (sigil process) (sigil async) (sigil io) (sigil wm trace))
4
(export native-start! native-options! native-socket-name! native-decoration-default! native-state native-socket native-statistic native-join!
5
current-trace-correlation native-trace-enable! native-trace-dropped
−6
native-latency-mark! native-command! native-submit! native-pixels! native-event-text next-native-event wait-native-event!)
+6
native-latency-mark! native-command! native-input-reply! native-submit! native-pixels! native-event-text next-native-event wait-native-event!)
7
(begin
8
(define native (c-library (or (getenv "SIGIL_WM_LIBRARY") "./build/libsigil-wm.so")))
9
(define native-start! (c-function native "wm_start" '() ffi/int))
@@ -20,6 +20,7 @@
20
(define native-join! (c-function native "wm_join" '() ffi/int))
21
(define event-fd (c-function native "wm_event_fd" '() ffi/int))
22
(define send (c-function native "wm_send" (list ffi/pointer) ffi/int))
+23
(define input-reply (c-function native "wm_input_reply" (list ffi/pointer ffi/int) ffi/int))
24
(define next (c-function native "wm_next" (list ffi/pointer) ffi/int))
25
(define event-text (c-function native "wm_event_text" (list ffi/pointer ffi/int) ffi/int))
26
@@ -68,6 +69,13 @@
69
(pointer-set! storage ffi/uint64 120 (current-trace-correlation)))
70
(= (send storage) 1))))
71
+72
(define (native-input-reply! words kind)
+73
(unless (<= (length words) 16) (error "command too large"))
+74
(with-words words 128 (lambda (storage)
+75
(when (not (= (current-trace-correlation) 0))
+76
(pointer-set! storage ffi/uint64 120 (current-trace-correlation)))
+77
(input-reply storage kind))))
+78
79
(define (native-submit! kind token words)
80
(let ((causes (trace-causes)))
81
(with-words words (* 8 (max 1 (length words)))
src/sigil/wm/input.sglmodified
@@ -1,6 +1,6 @@
1
;;; Per-event decisions execute on the Sigil scheduler, outside native callbacks.
2
(define-library (sigil wm input)
−3
(import (sigil struct) (sigil list) (sigil wm runtime))
+3
(import (sigil struct) (sigil list) (only (sigil time) sleep) (sigil wm runtime))
4
(export input-event input-event? input-event-kind input-event-token input-event-device
5
input-event-time input-event-code input-event-state input-event-modifiers
6
input-event-focus input-event-generation input-event-origin input-event-x input-event-y input-event-keysyms
@@ -102,13 +102,17 @@
102
result)))
103
(disposition (car result))
104
(replacement (or (cadr result) event)))
−105
(unless (host-send! host
−106
(list 11 (input-event-token event) disposition
−107
(if (= (input-event-kind event) 32) 0 (input-event-code replacement))
−108
(inexact->exact (round (* 1000 (input-event-x replacement))))
−109
(inexact->exact (round (* 1000 (input-event-y replacement))))
−110
(input-event-modifiers replacement)))
−111
(error "input reply queue full"))))
+105
(let ((reply (list 11 (input-event-token event) disposition
+106
(if (= (input-event-kind event) 32) 0 (input-event-code replacement))
+107
(inexact->exact (round (* 1000 (input-event-x replacement))))
+108
(inexact->exact (round (* 1000 (input-event-y replacement))))
+109
(input-event-modifiers replacement))))
+110
;; Preserve command order while yielding until the native queue has space.
+111
(let retry ()
+112
(case (host-input-reply! host reply (input-event-kind event))
+113
((0 2) (sleep .001) (retry))
+114
((1 -1) #t)
+115
(else (error "invalid input reply")))))))
116
117
(define (install-input-dispatch! host)
118
(or (wm-host-input-handler host)
src/sigil/wm/runtime.sglmodified
@@ -3,7 +3,7 @@
3
(import (sigil math) (sigil struct) (sigil io) (sigil wm bridge) (sigil wm model) (sigil wm trace))
4
(export start-host! stop-host! run-host! poll-host! dispatch-host-event!
5
on-event! remove-handler! host-snapshot host-socket host-statistic host-running?
−6
host-send! host-submit! host-next-token!
+6
host-send! host-input-reply! host-submit! host-next-token!
7
wm-host? wm-host-key-hook set-wm-host-key-hook!
8
wm-host-button-hook set-wm-host-button-hook!
9
wm-host-motion-hook set-wm-host-motion-hook!
@@ -40,6 +40,10 @@
40
(unless (wm-host? host) (error "expected a host"))
41
(native-command! words))
42
+43
(define (host-input-reply! host words kind)
+44
(unless (wm-host? host) (error "expected a host"))
+45
(native-input-reply! words kind))
+46
47
(define (host-submit! host kind token words)
48
(unless (wm-host? host) (error "expected a host"))
49
(native-submit! kind token words))
test/input-queue-gate.cadded
@@ -0,0 +1,105 @@
+1
/* SPDX-License-Identifier: BSD-3-Clause */
+2
#include <unistd.h>
+3
#include <wayland-server-core.h>
+4
static ssize_t gated_read(int fd,void *buffer,size_t count);
+5
static const char *gated_socket(struct wl_display *display);
+6
#define read(...) gated_read(__VA_ARGS__)
+7
#define wm_send unrecorded_send
+8
#define wm_input_reply unrecorded_input_reply
+9
#define wl_display_add_socket_auto gated_socket
+10
#include "../src/c/host.c"
+11
#undef wl_display_add_socket_auto
+12
#undef wm_input_reply
+13
#undef wm_send
+14
#undef read
+15
+16
static atomic_bool hold_commands,commands_held;
+17
static atomic_int starting_kind;
+18
static unsigned reply_attempts,reply_refused,reply_accepted;
+19
+20
int wm_send(const struct wm_message *message) {
+21
int result=unrecorded_send(message);
+22
if(message && message->word[0]==11) {
+23
reply_attempts++;
+24
if(result) reply_accepted++;
+25
else reply_refused++;
+26
}
+27
return result;
+28
}
+29
+30
int wm_input_reply(const struct wm_message *message,int kind) {
+31
int result=unrecorded_input_reply(message,kind);
+32
reply_attempts++;
+33
if(result==WM_SEND_ACCEPTED) reply_accepted++;
+34
else reply_refused++;
+35
return result;
+36
}
+37
+38
int wm_test_start_gate(int kind) {
+39
atomic_store(&starting_kind,kind);
+40
return 1;
+41
}
+42
+43
/* A copied startup observation exercises retries before the ready event. */
+44
static const char *gated_socket(struct wl_display *display) {
+45
int kind=atomic_load(&starting_kind);
+46
if(kind) {
+47
emit((struct wm_message){.word={kind,777,1,now_ms(),kind==30 ? 30 : kind==31 ? 272 : 0,1,0,0,1,1}});
+48
uint64_t deadline=now_ms()+3000;
+49
while(atomic_load(&starting_kind) && now_ms()<deadline) {
+50
struct timespec delay={.tv_nsec=500000};
+51
nanosleep(&delay,NULL);
+52
}
+53
}
+54
return wl_display_add_socket_auto(display);
+55
}
+56
+57
int wm_test_reply_stat(int index) {
+58
switch(index) {
+59
case 0: return reply_attempts;
+60
case 1: return reply_refused;
+61
case 2: return reply_accepted;
+62
default: return -1;
+63
}
+64
}
+65
+66
/* Pausing command consumption makes saturation independent of thread timing. */
+67
static ssize_t gated_read(int fd,void *buffer,size_t count) {
+68
if(fd==bridge.command_fd && atomic_load(&hold_commands)) {
+69
atomic_store(&commands_held,true);
+70
uint64_t deadline=now_ms()+3000;
+71
while(atomic_load(&hold_commands) && now_ms()<deadline) {
+72
struct timespec delay={.tv_nsec=500000};
+73
nanosleep(&delay,NULL);
+74
}
+75
atomic_store(&commands_held,false);
+76
}
+77
return read(fd,buffer,count);
+78
}
+79
+80
int wm_test_gate(int hold) {
+81
atomic_store(&hold_commands,hold!=0);
+82
if(!hold) return 1;
+83
struct wm_message command={.word={14,3}};
+84
if(!wm_send(&command)) return 0;
+85
uint64_t deadline=now_ms()+1000;
+86
while(!atomic_load(&commands_held)) {
+87
if(now_ms()>=deadline) return 0;
+88
struct timespec delay={.tv_nsec=500000};
+89
nanosleep(&delay,NULL);
+90
}
+91
return 1;
+92
}
+93
+94
int wm_test_queued(int kind) {
+95
if(!atomic_load(&commands_held)) return -1;
+96
unsigned first=atomic_load(&bridge.commands.read),last=atomic_load(&bridge.commands.write);
+97
unsigned count=0;
+98
for(unsigned i=first;i!=last;i++)
+99
if(!kind || bridge.commands.entries[i%QUEUE_SIZE].word[0]==kind) count++;
+100
return count;
+101
}
+102
+103
int wm_test_depth(void) {
+104
return atomic_load(&bridge.commands.write)-atomic_load(&bridge.commands.read);
+105
}
test/input-reply-queue.sgladded
@@ -0,0 +1,116 @@
+1
(import (sigil async) (sigil io) (sigil ffi) (sigil process) (sigil time)
+2
(sigil wm host) (sigil wm input)
+3
(only (sigil wm runtime) host-send! wm-host-stopped? set-wm-host-motion-hook!))
+4
(define (check value message) (unless value (error message)))
+5
(define (now-ms) (inexact->exact (round (* 1000 (/ (current-jiffy) (jiffies-per-second))))))
+6
(define (await predicate)
+7
(let loop ((n 0))
+8
(unless (predicate)
+9
(check (< n 600) "input reply queue timeout")
+10
(sleep .005) (loop (+ n 1)))))
+11
(define fixture (c-library (getenv "SIGIL_WM_LIBRARY")))
+12
(define gate (c-function fixture "wm_test_gate" (list ffi/int) ffi/int))
+13
(define queued (c-function fixture "wm_test_queued" (list ffi/int) ffi/int))
+14
(define depth (c-function fixture "wm_test_depth" '() ffi/int))
+15
(define reply-stat (c-function fixture "wm_test_reply_stat" (list ffi/int) ffi/int))
+16
(define start-gate (c-function fixture "wm_test_start_gate" (list ffi/int) ffi/int))
+17
(define mode (or (getenv "WM_INPUT_MODE") "overflow"))
+18
(define kind (string->number (or (getenv "WM_INPUT_KIND") "30")))
+19
(when (equal? mode "starting") (start-gate kind))
+20
(define host (start-host!))
+21
(define ready? #f)
+22
(define completed '())
+23
(define observed '())
+24
(define saved #f)
+25
(define filled? #f)
+26
(define released? #f)
+27
(define start 0)
+28
(on-event! host (lambda (event)
+29
(case (event-type event)
+30
((ready) (set! ready? #t))
+31
((input-completed) (set! completed (cons (event-ref event id:) completed))))))
+32
(define (hook event)
+33
(set! observed (cons event observed))
+34
(unless saved
+35
(set! saved event)
+36
(cond
+37
((equal? mode "starting")
+38
(set! start (now-ms))
+39
(go (sleep .075) (start-gate 0) (set! released? #t)))
+40
((equal? mode "shutdown")
+41
(begin
+42
(check (stop-host! host) "stop command refused")
+43
(await (lambda () (not (host-running? host))))
+44
(check (= (depth) 0) "shutdown command queue not empty")
+45
(display "SHUTDOWN_REPLY queued=0 reply-kind=") (display kind) (newline)))
+46
(else
+47
(begin
+48
(check (= (gate 1) 1) "native command gate failed")
+49
(let loop () (when (set-seat-capabilities! host 3) (loop)))
+50
(check (= (queued 0) 1024) "unexpected command queue capacity")
+51
(check (= (queued 14) 1024) "unexpected queued command kinds")
+52
(display "QUEUE_FULL capacity=1024 commands=14:1024 reply-kind=")
+53
(display (input-event-kind event)) (newline)
+54
(set! start (now-ms))
+55
(set! filled? #t)
+56
(go (sleep .075) (gate 0) (set! released? #t))))))
+57
(drop))
+58
(set-key-hook! host hook)
+59
(set-button-hook! host hook)
+60
(when (= kind 32) (set-motion-hook! host hook 0 0 '(0 0 800 600)))
+61
(when (and (= kind 32) (equal? mode "starting")) (set-wm-host-motion-hook! host hook))
+62
(with-async
+63
(go (run-host! host))
+64
(await (lambda () ready?))
+65
(set-key-hook! host hook)
+66
(set-button-hook! host hook)
+67
(check (configure-keyboard! host 1 "us" 25 600) "keyboard setup refused")
+68
(when (= kind 32) (set-motion-hook! host hook 0 0 '(0 0 800 600)))
+69
(sleep .05)
+70
(let ((first (case kind
+71
((30) (emulate-key! host 1 30 #t))
+72
((31) (emulate-button! host 1 272 #t))
+73
((32) (emulate-motion! host 1 20 20)))))
+74
(check first "input emulation refused")
+75
(await (lambda () saved))
+76
(cond
+77
((equal? mode "starting")
+78
(await (lambda () (memv first completed)))
+79
(check released? "startup reply did not yield to readiness")
+80
(check (= (length observed) 2) "startup hook repeated or input lost")
+81
(check (> (reply-stat 1) 0) "starting receiver never refused reply")
+82
(check (< (reply-stat 0) 200) "startup reply spun without sleeping")
+83
(check (= (reply-stat 2) 2) "startup reply dropped before readiness")
+84
(display "INPUT_STARTING=PASS kind=") (display kind)
+85
(display " elapsed-ms=") (display (- (now-ms) start)) (newline))
+86
((equal? mode "shutdown")
+87
(await (lambda () (wm-host-stopped? host)))
+88
(check (= (reply-stat 0) 1) "shutdown reply retried without a receiver")
+89
(display "INPUT_SHUTDOWN=PASS kind=") (display kind) (newline))
+90
((equal? mode "probe")
+91
(await (lambda () released?))
+92
(let ((second (emulate-key! host 1 31 #t)))
+93
(check second "following key refused")
+94
(sleep 1.1)
+95
(check (not (memv first completed)) "lost bound input unexpectedly completed")
+96
(check (= (length observed) 1) "following bound key escaped lost decision")
+97
(display "BOUND_LOST kind=") (display kind)
+98
(display " retained-ms=") (display (- (now-ms) start))
+99
(display " following-key-blocked=yes\n")
+100
(check (host-send! host (list 11 (input-event-token saved) 0 (input-event-code saved) 0 0 0))
+101
"recovery reply refused")
+102
(await (lambda () (and (memv first completed) (memv second completed))))
+103
(display "BOUND_RECOVERED=PASS explicit-reply\n")))
+104
(else
+105
(await (lambda () (memv first completed)))
+106
(check filled? "input hook never filled queue")
+107
(check released? "reply did not wait for queue space")
+108
(let ((second (emulate-key! host 1 31 #t)))
+109
(await (lambda () (memv second completed))))
+110
(check (= (length observed) 2) "input hook repeated or following key blocked")
+111
(check (> (reply-stat 1) 0) "reply never encountered backpressure")
+112
(check (= (reply-stat 2) 2) "input reply silently lost")
+113
(display "INPUT_REPLY_QUEUE=PASS kind=") (display kind)
+114
(display " elapsed-ms=") (display (- (now-ms) start)) (newline)))
+115
(unless (equal? mode "shutdown") (check (stop-host! host) "stop refused"))))
+116
(for-each (lambda (index) (check (= (host-statistic host index) 0) "native resources or events retained")) '(1 2 3 4))