Skip to content

wasm-gui: a WebAssembly Linux desktop (M1–M6) rendered by the native Rust client#104

Open
NathanFlurry wants to merge 28 commits into
mainfrom
wasm-gui-desktop
Open

wasm-gui: a WebAssembly Linux desktop (M1–M6) rendered by the native Rust client#104
NathanFlurry wants to merge 28 commits into
mainfrom
wasm-gui-desktop

Conversation

@NathanFlurry

Copy link
Copy Markdown
Member

Experiment under experiments/wasm-gui/: a graphical Linux desktop built by cross-compiling real, standard X11 software to wasm32-wasip1 with our own toolchain and running it inside the real secure-exec V8 sidecar, rendered by a native Rust app on crates/secure-exec-client (per SPEC §1a: no wasmer / node:wasi / TS client / Command::new in the execute+render path).

What works (all wasm guests in one VM, over host_net AF_UNIX)

  • M1–M5: the standard X.Org window manager twm decorates a real libX11 client window; multi-client compositing. Tests: test-m4b.sh, test-m5-multiclient.sh, test-m5-twm.sh.
  • M6.4 — robust multi-app desktop: twm concurrently manages a real libX11 window and a stock xclock (live analog face), past 30s, 3/3 deterministic. test-m6-desktop.sh. Proof: ~/tmp/gui-progress/m6-desktop-robust.png.
  • M6.1 (input): host-driven input via XTEST reaches a real libX11 client (ButtonPress → window repaints). test-m6-input.sh.
  • M6.2 (fonts): real X core fonts served by the server; libX11 locale DB install so Xt apps build a fontset; Xft stack cross-compiled (expat + fontconfig + libXft).

Core sidecar fixes (benefit all of secure-exec)

  • sync-RPC fairness: net.poll blocked the single service thread up to 50ms, starving other guests; lowered to 3ms so it round-robins.
  • WASM execution budget: long-running guests (the X server) were killed at the 30s default fuel budget; configurable via limits.resources.maxWasmFuel.

Notes

  • Cross-compiled from source: Xvfb, twm, xclock, libX11/libxcb/libXt/libXaw/libXft/fontconfig/freetype/pixman/… (see third_party/, scripts/).
  • The live winit window blit needs a machine with a display to verify (the dev box is headless); the input delivery path is verified headlessly via XTEST.
  • Remaining M6: xterm (needs a kernel-PTY-spawn shim + a wasm shell); then M7 (JWM), M8 (GTK DE). See experiments/wasm-gui/SPEC.md.

🤖 Generated with Claude Code

Root cause: net.poll did a blocking recv_timeout(up to 50ms) on the single sync-RPC service thread,
so while servicing one chatty guest's poll (e.g. an Xt app's main loop) no other guest's sync RPC
could run — starving the WM and other clients. Lowering the cap to 3ms lets the service thread
round-robin quickly across guests. Result: in the 3-client desktop (twm+xclock+xwin) the event-driven
xwin client now renders fully (green+white rects, decorated) where before it lost draws; twm+xclock
(2-client) renders the clock face. M5-twm + M5-multiclient still pass (no regression).
Open: xclock still fails specifically in the 3-client case (xclock+xwin together) — investigating.
…indow)

Second root cause found + fixed: the X server (long-running WASM guest) was killed at the 30s default
wall-clock fuel budget (DEFAULT_WASM_EXECUTION_TIMEOUT_MS) -> 'WebAssembly fuel budget exhausted' ->
desktop collapsed past 30s. Host now sets limits.resources.maxWasmFuel=1h on the trusted VM.
Combined with the net.poll fairness cap (3ms), the 3-client desktop now renders robustly: twm
CONCURRENTLY decorates a real libX11 window AND a stock xclock (live analog face), past 30s, 3/3
deterministic. test-m6-desktop.sh strengthened to assert the xclock face too. Proof:
~/tmp/gui-progress/m6-desktop-robust.png. SPEC.md M6 updated.
New: guest-xclient/xtest-agent.c (XTEST/libXtst input injector) + xinput-target.c (repaints on
KeyPress=green/ButtonPress=orange) + build-xclient.sh (reusable libX11-client build) + test-m6-input.sh.
The host launches the agent which synthesizes a ButtonPress; the target client repaints orange ==
the injected event was delivered through the wasm X server to a real libX11 client. Proof:
~/tmp/gui-progress/m6-input-button.png. Host --inject <pid>=<cmd> wired for dynamic stdin injection
(currently blocked by a separate WASM-guest stdin-delivery gap; argv path proves the chain). The live
winit blit still needs a display to verify (headless box). SPEC.md updated.
@railway-app

railway-app Bot commented Jun 20, 2026

Copy link
Copy Markdown

🚅 Environment secure-exec-pr-104 in rivet-frontend has no services deployed.

@NathanFlurry NathanFlurry force-pushed the wasm-gui-desktop branch 12 times, most recently from e127565 to b693106 Compare June 21, 2026 01:02
…M1-M7 suite green)

- M6.2 Xft antialiased text (expat+fontconfig+libXft; freetype memory-stream patch; C-locale DB)
- M6-INTERACTIVE: runnable cross-platform (winit+softbuffer, macOS+Linux) live desktop with
  host-driven cursor/click/keyboard/drag via x11rb XTEST to the X server host-backed AF_UNIX socket
  (twm drag fix: NoGrabServer+OpaqueMove)
- M7: JWM 2.4.6 cross-compiled to wasm as the desktop shell (panel/taskbar/window list/live clock)
- PNG proof exporter (fb2png.py + export-proof-png.sh); fresh proof in ~/tmp/gui-progress/
- scripts: run-desktop, test-m6-{input,drag,xft}, test-m7-jwm, prepare-{xftfonts,jwm}, export-proof-png
- full suite 8/8 green: M4b, M5 multiclient+twm, M6 desktop+input+drag+xft, M7 JWM

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NathanFlurry and others added 3 commits June 20, 2026 18:14
Allocate a PTY pair and place its ends in two distinct processes: master fd into the parent
(terminal emulator) fd table, slave fd into the child (shell) fd table. open_pty places both ends in
one process; a terminal needs them split. This is the kernel primitive behind the forthcoming
__pty_spawn host import for M6.3 (xterm/terminal). Read/write/poll already route through PTYs, so the
terminal reads/writes the master and the shell's dup'd 0/1/2 slave just works.

Unit test open_pty_split_wires_master_in_parent_and_slave_in_child verifies bidirectional I/O
(master<->slave) across the two processes plus rollback on a bad child pid. Full kernel suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the kernel open_pty_split primitive into the child-process spawn path:
- configure_child_stdio() factored from the two spawn variants; adds a 'pty' stdio mode that
  allocates a split PTY (master in parent, slave in child), dups the slave onto the child's
  stdin/stdout/stderr, and returns ptyMasterFd in the spawn response.
- __pty_read/__pty_write sync-RPC handlers let the terminal (parent) drive the master fd, mirroring
  __kernel_stdin_read / __kernel_stdio_write semantics.
- ActiveProcess.pty_master_fd tracks the master for reaping.
- arch-guard: allowlist node_import_cache.rs for the process-wide materialize-timeout env read.

A wasm terminal can now spawn a wasm shell over a real kernel PTY: shell stdin reads the slave,
shell stdout writes the slave (write_process_stdout -> fd_write(1) -> pty), terminal reads/writes the
master. Sidecar suite + architecture guards green. (WASM import shims + e2e test next.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e bindings (M6.3)

Complete the host->guest PTY syscall surface so a wasm terminal can drive a kernel PTY:
- node_import_cache.rs: host_net.pty_spawn (launches a child over child_process.spawn stdio 'pty',
  returns the master fd), pty_read/pty_write (drive the master via __pty_read/__pty_write). full tier.
- v8-bridge.source.js: _ptyReadRaw/_ptyWriteRaw facades; v8_runtime.rs maps them to __pty_read/write;
  wasm.rs runner switch adds the two cases.
- bump NODE_IMPORT_CACHE_ASSET_VERSION 70->71.

Whole stack builds. pty_spawn reuses the existing engine-level child_process bridge (de-risked:
_childProcessSpawnStart is an unconditional binding). C terminal/shell guests + e2e proof next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NathanFlurry NathanFlurry force-pushed the wasm-gui-desktop branch 8 times, most recently from 9853eb2 to f1c842d Compare June 21, 2026 03:39
@NathanFlurry NathanFlurry force-pushed the wasm-gui-desktop branch 9 times, most recently from 2fc31c1 to bcf22e2 Compare June 21, 2026 05:43
…t cycles)

Builds on the PTY stdin pump: pty-shell.c is now a real line-oriented interpreter
loop (prompt -> read line from PTY slave stdin -> respond), and pty-term.c drives a
multi-command interactive session over the kernel PTY:
  echo hello -> hello ; ping -> pong ; exit -> bye (clean shutdown).
test-m6-3-pty.sh asserts PTY_CHILD_REPLY_OK + PTY_CHILD_PING_OK + PTY_CHILD_EXIT_OK
+ PTY_SESSION_OK, proving repeated bidirectional terminal I/O (terminal->shell stdin
AND shell stdout->terminal) across several cycles, not a one-shot echo.

A real shell (dash/bash) needs fork/exec/job-control, which wasi lacks; this
interpreter is faithful to what a terminal emulator drives over the PTY primitive.

SPEC.md M6.3 status updated. test-m6-3-pty.sh green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NathanFlurry and others added 19 commits June 20, 2026 23:04
…led to wasm

st, cross-compiled from source to wasm32-wasip1 (scripts/build-st.sh), runs as a
wasm guest in secure-exec: it spawns the wasm shell (/pty-shell.wasm) over a real
kernel PTY and renders the shell's terminal output via Xft to the wasm X server.
scripts/test-m6-3-st.sh asserts the framebuffer shows the shell's prompt as
antialiased Xft text (215 glyph-core + 258 AA-edge px); proof PNG m6-3-st.png.

Integration (third_party/st):
- wasmpty.c: st's forkpty/openpty + select/read/write PTY backend replaced by the
  host_net pty_spawn/pty_read/pty_write primitive (the path proven by test-m6-3-pty.sh).
- st.c: ttynew -> stwasm_spawn('/pty-shell.wasm'); ttyread -> non-blocking stwasm_read
  (0 = no data this tick, not EOF); ttywriteraw -> stwasm_write; ttyresize/ttyhangup/
  stty neutralized; termios.h dropped (wasi has none); tcsendbreak stubbed.
- x.c: the pselect(xfd, ttyfd) loop becomes a non-blocking poll loop (ttyread via
  pty_read + XPending + 8ms nanosleep idle throttle), since neither fd is a libc fd.
- config.h: DejaVu Sans Mono Xft font (shipped via fontconfig); STWASM_SHELL.
- link adds expat + zlib (fontconfig deps) + wasi-emulated mman/process-clocks.

Host: run_xdemo gained --pty-shell to install the child shell into the VM so a
terminal-emulator client can pty_spawn('/pty-shell.wasm').

Separable gap (documented in SPEC.md): typing into st THROUGH X does not work yet
because this Xvfb cannot compile an XKB keymap (xkbcomp is exec'd; wasi has no
fork/exec), so the server has no keyboard device. st's keypress->ttywrite->pty_write
path is wired; the terminal<->shell bidirectional I/O is proven by test-m6-3-pty.sh.

.gitignore: ignore third_party/st.tar.gz (the patched source tree is tracked).
build-st.sh fetches upstream st-0.9.2 if the tree is absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…map, no xkbcomp)

The wasm X server now has a functional keyboard device, so host-driven KeyPress/
XTEST events are delivered AND translated to characters for real libX11 clients.
test-m6-keyboard.sh asserts an injected key repaints the input-target green
(green=39888).

Root cause fixed: wasi has no fork/exec, so Xvfb cannot run xkbcomp to compile a
keymap at runtime; its keyboard device never activated ('XTest keyboard not
activated') and all key events were dropped. Fix: compile a US keymap on the host
(scripts/prepare-xkb.sh -> /xkb/default.xkm), install it in the VM (--vm-tree), and
patch the server's XkbCompileKeymap to load it directly via fmemopen instead of
forking xkbcomp (patches/xserver-keymap-no-xkbcomp.patch; third_party/xserver is
not VCS-tracked so the change is persisted as a patch file). KEY GOTCHA: VFS-backed
files don't support per-access fseek/fread streaming under wasi (same limitation
freetype hit) and XkmReadFile fseeks to each section offset, so the .xkm must be
slurped into memory and read via fmemopen.

Also:
- host: --inject 'host=focus' sets X input focus (window-under-pointer + PointerRoot)
  so keys reach a client when no WM owns focus.
- st: build with XIM disabled + core XLookupString (no XIM server exists under wasi;
  an XIC makes XFilterEvent swallow every KeyPress before kpress runs).
- scripts/prepare-xkb.sh, scripts/test-m6-keyboard.sh.

Note (SPEC.md): live typing into st specifically is still blocked by the synchronous
child-driving model (st's ttyread drives the interactive PTY child, whose read(0)
spins in-isolate waiting for input, so the drive never returns and st can't process
the keystroke that would feed it). That is the core 'drive child isolates
concurrently' execution-engine limitation, separate from the keyboard/terminal work;
terminal<->shell I/O is proven by test-m6-3-pty.sh and the X keyboard by
test-m6-keyboard.sh. Rebuild note: link-xvfb.sh must be followed by wasm-opt
--fpcast-emu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…very, not deadlock)

Flushed logging + sidecar tracing disproved the earlier 'child-drive deadlock' and
'XIM filtering' theories for why typing into st doesn't work: st's poll loop runs
fine (poll_descendant ENTERs 150+x and returns; the apparent block was a
stderr-buffering artifact), and the server's input focus IS st's window (confirmed
via get_input_focus: VIEWABLE + KeyPressMask). The real, still-open issue is that the
server delivers only early events (Expose/VisibilityNotify) to st's host_net X
connection and never late ones (FocusIn/KeyPress), even with XSync forcing round-trips
- while an identical simple client (xinput-target) receives keys via the same flow.
So it is st-connection-specific X-over-host_net event delivery, needing server-side
event-routing instrumentation. Docs-only correction (SPEC.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… test)

Demonstrates and locks in robust concurrency: host --xdemo --concurrent launches all
X clients simultaneously (no per-client settle/ordering gating), and
scripts/test-m2-3-concurrent.sh starts twm + xclock + xftdemo at once -- twm reaches
its event loop and decorates both concurrently-launched windows, xftdemo opens its
Xft font, 0 clients crash, and the framebuffer shows the twm-managed desktop (proof
~/tmp/gui-progress/m2-3-concurrent.png). 5/5 reliability runs: 0 failures, consistent
render.

The enabling fix was M6.4's net.poll fairness (JAVASCRIPT_NET_POLL_MAX_WAIT 50ms->3ms)
which removed the sync-RPC-bridge starvation that made concurrent libX11 init flaky;
this commit adds the concurrent launch path (host main.rs --concurrent) + the
regression test to lock it in. The WM is slightly slower to finish decorating under
concurrent contention but is reliable, not flaky. SPEC.md 'Robust concurrency' marked
done.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sses DeliverFocusedEvent)

10 server-side instrumentation passes this session narrowed the st live-typing gap:
keyboard events do NOT flow through DeliverFocusedEvent (0 calls in both the working
xinput-target run and the st run), and TryClientEvents/DeliverToWindowOwner core
type-2 checks never fire either -- core KeyPress is synthesized late (EventToCore at
write time) and live events travel as XI internal device events via
DeliverGrabbedEvent/DeliverDeviceEvents. WriteToClient tracing is confounded by event
batching (count=N*32)/XI2. Server focus IS st's window (confirmed get_input_focus),
st selects KeyPressMask, and an identical simple client (xinput-target, real core
KeyPress via XNextEvent) works -- so the difference is st-connection/window specific.
SPEC.md records where the next investigation should start (DeliverGrabbedEvent +
DeliverDeviceEvents + the mieq/ProcessInputEvents drain, NOT DeliverFocusedEvent).
Docs-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the keyboard path

12 instrumentation passes total: keyboard events bypass TryClientEvents,
DeliverFocusedEvent, AND DeliverDeviceEvents (each 0 calls for internal key/button
events ET_KeyPress=2..ET_ButtonRelease=5 in BOTH the working green run and the st
run), yet green delivers KeyPress to xinput-target. So this server's keyboard
delivery is atypical (DeliverGrabbedEvent / XKB-direct / XTEST-specific). SPEC.md
updated with the eliminations + the next-session starting points
(DeliverGrabbedEvent + ProcXTestFakeInput + mieqProcessDeviceEvent). Docs-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ting (+ tracing caveat)

13 instrumentation passes culminated in the real finding: st live-typing is a
TIMING/LOAD-sensitivity issue, not an event-routing bug. KEY META-FINDING: hot-path
ErrorF instrumentation in the X server event pipeline perturbs the very timing that
is the bug -- with traces added, even the working xinput-target stops receiving
KeyPress (green=0/orange=40000), which invalidates the earlier dew/dfe/dde "0 type-2
delivery" conclusions (the key just wasn't delivered in time in the slowed runs).
Untraced, test-m6-keyboard passes 3/3 reliably (green=39888): core KeyPress delivery
to a LIGHT client works fine. st fails because it is HEAVY -- Xft rendering + driving
the PTY child produce dense request/sync-RPC traffic that starves the wasm X server's
input-event (mieq) processing over the single service thread (same single-bridge
scheduling class as M2.3/M6.4 net.poll, now for input). Next session: low-overhead
counters (not hot-path ErrorF) + input-vs-request scheduling fairness in the server
main loop, NOT a delivery-function fix. SPEC.md + memory updated. Docs-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…target diagnostic)

~16 passes. Decisive isolation this turn via a minimal control client
(guest-xclient/xpoll-target.c): it uses st's EXACT non-blocking XPending+nanosleep
poll loop (no Xft, no PTY) and receives KeyPress fine (green=39888) under twm
click-to-focus -- so the poll loop is NOT the cause. A hardcoded NOPTY st build (no
PTY driving) still got zero input -- so PTY driving is NOT the cause. With focus
CONFIRMED on st's window (get_input_focus: st win, VIEWABLE, KeyPressMask set), st
receives ZERO input events (no ButtonPress AND no KeyPress, only Expose/Visibility),
while xpoll-target under identical conditions gets them. So st live-typing is
st-window/connection specific: st uses XCreateWindow with a custom visual+colormap
(CWColormap, for Xft) + a heavy libxcb connection, vs xpoll-target's
XCreateSimpleWindow on the default visual. Next: compare window attrs/visual and check
whether st's heavy libxcb reply traffic buries input events in the xcb event queue.
Adds xpoll-target.c (tracked diagnostic). SPEC.md updated. Docs + diagnostic only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ents NOT (XI2/core synthesis)

~22 passes. Decisive symptom this turn: with st self-focusing (XSetInputFocus on its
own window), st receives WINDOW events (Expose, VisibilityNotify, MapNotify, and now
FocusIn/FocusOut) but ZERO DEVICE events -- no KeyPress AND no ButtonPress -- even
with focus CONFIRMED on st (FocusIn received) and a button injected over its on-screen
location. Minimal clients (xpoll-target/xinput-target) receive both via the identical
path. So st live-typing is specifically a DEVICE-EVENT delivery problem for st's
connection -- focus, poll loop, PTY driving, window creation, visual, and XIM are all
ruled out. Device events flow via XI2 with core events synthesized for core clients;
next session: examine the XI2->core synthesis / per-client device-event selection for
st's connection. Focus + keymap both work (xinput-target green via the same path).
SPEC.md updated with the crystallized symptom. Docs-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…wed to terminal machinery

~25 passes. Built guest-xclient/xftpoll-target.c (the working poll loop + XftFontOpenName
+ setlocale/XSetLocaleModifiers, exactly like st's main) and proved it ALONE + focus +
key => green=40000. So Xft font init is NOT the cause and locale/IM modifiers are NOT
the cause. Combined with the prior definitive control (xpoll-target works alone+focus),
the eliminated set is now: WM, focus, poll loop, PTY driving, visual/colormap, window
creation, XIM, Xft font init, locale. Remaining suspects are st's terminal machinery the
minimal clients lack: XftDraw rendering to the offscreen pixmap (xw.buf) + XCopyArea, the
GC created on the ROOT window, selection setup (selinit), xsetenv, or terminal-core init.
Adds xpoll-target.c (no Xft) and xftpoll-target.c (Xft+locale) as working baselines for
continuing the bisection. SPEC.md updated with results + next steps. Docs + diagnostics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…minal-core/run-flow

~28 passes. Extended xftpoll-target.c to replicate st's ENTIRE X-protocol behavior:
st's exact XCreateWindow (explicit attrs, CWColormap, GC-on-root, 644x408 at 0,0),
XftFontOpenName, setlocale+XSetLocaleModifiers, offscreen-pixmap XftDraw + XCopyArea,
AND host_net.pty_spawn of /pty-shell.wasm -- and it STILL receives KeyPress (green).
Conversely st with selinit() removed still gets ZERO device events. So NONE of st's
X-protocol behavior is the cause. Eliminated (16 total): WM, focus (incl. self
XSetInputFocus), poll loop, PTY driving, visual/colormap, exact window creation,
GC-on-root, XIM, Xft init, Xft RENDER/pixmap rendering, locale, PTY child spawn,
selection setup, window size/position. The cause is st's terminal-core/run() flow --
the only thing the now-near-identical working baseline (xftpoll-target.c) lacks. Next:
bisect by REMOVING pieces from st (tnew/twrite, run() do-while, xsetenv, handler[]) or
use non-perturbing server-side counters. xftpoll-target.c (works, near-st) committed as
the reference baseline. SPEC.md updated. Docs + diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…; X path fully ruled out)

~33 passes. Split tests pinned st live-typing precisely:
(a) st with its run() loop replaced by the minimal working-baseline loop (no
    ttyread/draw) STILL gets no KeyPress => NOT the loop body, it is st's INIT.
(b) A probe right after xinit() (before xsetenv/selinit/run/ttynew) gets FocusIn but
    no KeyPress => xinit itself breaks device-event delivery.
(c) Removing cursor / WM-protocols / _NET_WM_PID / resettitle / xhints(input=1) from
    xinit did NOT restore keys => none of those.
(d) 500 round-trip requests added to the working baseline did NOT break it => NOT
    request count.
So the remaining suspects are xinit's xloadfonts (4 Xft faces) and xloadcols (256+
colors) -- the only xinit ops the working near-st baseline (xftpoll-target.c) lacks and
that aren't generic request volume. Next: stub xloadfonts/xloadcols and re-probe; bisect
which Xft/Fc/Render call triggers it. Everything else in st's X path is ruled out (~18
eliminated suspects). SPEC.md updated; xftpoll-target.c kept as the working reference.
Docs + diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n exhausted)

~34 passes. Extended the working baseline xftpoll-target.c to perform EVERY operation
st's xinit does -- st's exact XCreateWindow + GC-on-root, FOUR Xft faces (xloadfonts),
256 XftColor allocs (xloadcols), offscreen-pixmap render, AND pty_spawn -- and it STILL
receives KeyPress (green). So bisection-by-addition is exhausted: every individual st
operation replicated, none breaks input. The cause must be operation ORDER (st loads
fonts/colors BEFORE creating its window; the baseline creates the window first) or a
subtle cumulative interaction. Remaining methodologies (documented in SPEC.md): st-side
REMOVAL bisection (stub xloadfonts/xloadcols, reorder), X PROTOCOL TRACE diff (st vs
xftpoll-target byte streams around the inject), or non-perturbing server-side counters.
~19 suspects eliminated; xftpoll-target.c kept as the working reference. Docs + diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ol-trace remains

~35 passes. Order ruled out too: the baseline xftpoll-target.c was made to load
fonts+colors BEFORE XCreateWindow (st's exact order) and STILL receives keys. So the
baseline now replicates st's ENTIRE X behavior -- every operation, in st's order -- and
works, while st fails even right after xinit with focus CONFIRMED (FocusIn arrives, so
KeyPressMask is provably set). CONCLUSION: the cause is NOT identifiable by replicating
st's X calls; the entire X-call layer is excluded (~20 suspects eliminated across the
session). The only remaining methodology is an X PROTOCOL-TRACE DIFF (capture bytes the
server sends to st vs xftpoll-target around the inject, via an in-VM socket proxy or a
libxcb raw-read hook, and diff). Documented in SPEC.md as the precise frontier. The
working reference baseline xftpoll-target.c is committed. Docs + diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ivers KeyPress correctly)

~37 passes; the decisive finding. A non-perturbing per-client counter in the server's
WriteToClient (zero-I/O increments + rare /data dump) proved the server DOES write the
KeyPress to st's connection: st gets keypress_writes=1, EXACTLY like the working
xinput-target (=1). The only difference is xinput-target SURFACES it (green) and st does
NOT. So the ENTIRE server side -- routing, focus, XI2/core synthesis, keymap,
device-event delivery -- is CORRECT and the bytes reach st's socket. st-typing is a
CLIENT-SIDE libxcb/Xlib event-surfacing bug in st: it surfaces early events
(Expose/Visibility) but XPending/XNextEvent stops surfacing the later KeyPress. This
moots the server-side/protocol-trace avenues. Next is client-side only: instrument st's
XPending/_XEventsQueued/xcb_poll_for_event vs the working xftpoll-target baseline.
SPEC.md + memory updated. Docs only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s flush/wire/recv

~38 passes. Forcing a socket read in st's loop (XEventsQueued QueuedAfterReading) does
NOT surface the KeyPress -- the bytes aren't reaching st's libxcb read at all. The prior
server counter measured WriteToClient (BUFFERING), not the flush/wire send. So the
boundary is now: server-flush (FlushClient -> Xtranssock -> net_send), host_net wire
delivery (net_send/net_recv for st's heavy connection), or libxcb recv -- NOT st's read
logic (forced reads don't help). st recvs EARLY events (Expose) but not the LATER
KeyPress. Next: count net_send-to-st (server) + net_recv-on-st (client) to pin
flush-vs-wire-vs-recv; suspect a host_net per-socket buffering/poll-readiness stall on
st's connection after heavy traffic. Layer-by-layer wire investigation, multi-session.
SPEC.md updated. Docs only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es/sends correctly)

~39 passes. A server-side FlushClient byte counter (non-perturbing /data dump) shows the
server FLUSHES/net_sends to st: client 1 (st) flushed_bytes=2596 over the run. So the
server both buffers AND writev/net_sends data to st's socket -- not just buffers. Combined
with the forced-read result (XEventsQueued(QueuedAfterReading) doesn't surface the
KeyPress), the failure is st's CLIENT-SIDE host_net RECV: st recvs EARLY bytes (Expose
surfaces) but not the LATER KeyPress bytes the server sent. So the bug is in host_net
net_recv / net_poll readability for st's heavy X connection (bytes are on the wire / in
the kernel socket but st's recv stops pulling after the initial burst). Next: count
net_recv bytes on st's client socket (runner host_net.net_recv, keyed by fd) vs the 2596
sent; check net_poll readability for st's X fd after heavy traffic (POLLIN/POLLOUT area).
The X server, focus, keymap, and st's X-call/read logic are all excluded. SPEC.md updated.
Docs only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l/refill for st's connection

~39 passes. Traced the client recv path end to end: st libxcb -> net_recv (runner) ->
drains a runner-LOCAL buffer (dequeueHostNetBytes) refilled by pollHostNetSocket (ONE
net.poll RPC -> ONE chunk into socket.readChunks) -> sidecar net.poll handler -> kernel
recv_buffer (UNBOUNDED VecDeque, level-triggered POLLIN when non-empty). Kernel buffering
is correct, so the suspect is the net.poll RPC / runner local-buffer refill for st's
heavy connection (one-chunk-per-poll under-draining, or a readiness edge st misses).
Measurement caveat recorded: the FlushClient counter (st flushed_bytes=2596) is cumulative
to its last /data dump and may not include the late keypress flush -- re-run dumping AFTER
the inject to confirm the keypress is net_sent. Next: per-fd byte counters in runner
net_recv + sidecar net.poll dumped after the inject (server-sent vs client-recv for st's X
fd), and check net.poll level-vs-edge for an already-drained socket. Everything above the
host_net wire is excluded. SPEC.md updated. Docs only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…X-server flush-timing frontier

~40 passes. Traced the FULL recv path: the X11 socket is a real HOST UnixStream under the
VM shadow (not the kernel socket_table). Chain: X server FlushClient -> _XSERVTransWritev
-> net_send -> server's accepted host UnixStream -> OS socketpair -> st's connected host
UnixStream -> st's reader thread (spawn_unix_socket_reader: blocking read loop -> UNBOUNDED
mpsc channel) -> ActiveUnixSocket.poll -> net.poll RPC -> runner net_recv -> libxcb ->
XPending. EVERY component is structurally correct. st surfaces EARLY events (Expose, and
FocusIn from its own XSetInputFocus) but not the LATE host-injected keypress; the server
delivers it. The single unresolved link is X-server output-flush TIMING for the late
keypress to an IDLE client (st sends no requests to trigger a flush; the keypress comes
from another client's XTEST), and the flush counter is cumulative to its dump so it can't
confirm the late keypress was sent. Decisive next test (multi-session): inject-synchronized
counters (dump AFTER the inject, gated on a sentinel) at FlushClient + st's reader thread +
net.poll to pin server-flush vs reader vs poll for the LATE keypress specifically.
Everything except this timing question is verified correct. SPEC.md updated. Docs only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant