wined3d with a WebGPU adapter, shaders through a new vkd3d-shader WGSL
target, webgpu.h over dawn_wire to the page (or native Dawn on the native
host). The WGSL target runs vkd3d's HLSL shader tests on Dawn/Metal: 247
of 283 files clean, 96.6% of 6474 checks. Aero needs DWM besides D3D;
the two routes are written down for later.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_f0a06c8a084b4b2ca4f7f291183d507d
feat/inplace-fork (6fe60515) is kept unmerged: correct, but snapshots
740 ms plus restores 226 ms for apt-get update's 11 forks, no win against
pooled Workers on the timed apt pair (edger, inside noise).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
The leg's and the recorder's servers answer ranges (a split bundle's lazy
files are read 1 MiB at a time). On a split site the leg checks lazy.json
against the store, says how many lazy reads the run made to the window, and
with --placed has Paint's Ctrl+P read printing's DLLs from the store.
docs/eject.md: "The bundle split".
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
A caption drag moves Paint by the drag, a border drag past its edge grows
it and one inside shrinks it, and a click on Paint leaves its owned dialog
on top; the first window is where Wine put it, not cascaded.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
Conflicts: lib.rs stderr reporting: W's stderr_capture() refactor kept, with C's
rule (warn only when nothing went through; escape non-printables; name stdout or
stderr) moved into it. system-worker.js: W's addMemory(kernel) plus C's dropMemory.
Session mirror, wakeups in place, the USD through the door, the in-door
word and poisoning (clients killed with the server), the door context
bug, mspaint click timings per step, and the SP plan.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
(cherry picked from commit 7d8cda37fa44dec54a5c08b334339d9e5912877e)
The server memory is created and owned by the runtime with the kernel
module's lifecycle, never a guest mapping, so sharing it is a build
option for eject. The server lock is a robust word waited on through
step 2's slots (SP suspends through JSPI). The pump serves proxied
syscalls from both of its waits, so a door caller waiting on the pump
under the server lock cannot deadlock.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
(cherry picked from commit d8461354092b53ff1d25e84a7a9b941c039a0b54)
- fitsLeg: foot from the launcher must open inside the output, at a
terminal's size in CSS pixels at any dpr.
- resizeLeg: a corner of foot and of the xterm, one on the canvas with room,
is dragged by CDP pointer events, and the size the client drew must grow.
For foot, a title double-click must maximize and a second restore.
- {"glcycles": N}: glxgears from the launcher N times, with the card's used
memory (nvidia-smi) after each exit; it must not grow per client.
docs/gexcc.md §9.2: the GL forget, foot's size, resize and maximize.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
The user's no-SW direction covers every gex page. A PE (Noita) site now
ships its page as plain files, packs only the bundle and the units (which
nt.html fetches and decodes itself, pack=), and its launcher checks
isolation and WebGPU, unregisters an older build's worker and opens nt.html
on the build: no install, no reload, no progress bar of its own. sw.js is
deleted.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
The lock-free syscall class (docs/kernel-locking.md step 9): read, write,
readv, writev, pread64, pwrite64, lseek and rt_sigprocmask, decided by
number (syscalls::free_class). The page makes the whole call from its
Worker, blocking included, with the same code the kernel runs (FdIo,
sigprocmask). A call that waits:
- parks in the thread's own run state;
- makes its first scheduler turn itself (pick_mine): it finishes the call,
or registers the slot on the stream's queue.
The idle "why" is per thread and kernel-free (wait_why, HAVE_NET,
FileHandle::is_global_socket). waitq::enter and publish_timer read the
process's signal state, not the kernel. Only the process's end stays the
kernel's (exit_here).
Also deletes 8 sites that read the pid or the ABI (my_pid, compat_abi).
Sites: 183 -> 175.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
Each thread's pc, wait, parked call and what that call still owes live
behind an Arc<Mutex<Run>> of their own, which the thread's Worker holds
(MY_RUN). The kernel's per-call leftovers (restore_sigmask, write_blocked,
the accept/recvmsg/transfer retries) are taken out as the call parks, so an
idle turn that finds its wait over finishes the call itself: finish_parked,
the answer into its context, SIGPIPE, the sigsuspend mask and delivery
through sigs::finish. A call the kernel used to redo after its wait is made
again from its instruction instead (in place for a wasm32 program or a
provider). A safepoint's handler frame resumes without schedule. The locked
pick stays for a blocking call's first turn (under its own take), the
compositor pump and the deadlock watch; exit stays the kernel's.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
Conflicts: proc.rs keeps both the process wait queue and the /proc info;
run.sh's PAGE_PROGRAMS has the signal programs and clock-monotonic;
runtime.js passes the kernel with wl-memory. EpollSet::with_watches (a
fork's copy) is now built on new(): its own queue, each watch chained
again as a waiter collects it.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
Conflicts: wl-bridge.mjs (the publish word stays at 2, the output scale
moves to 3), compositor-worker.js (both the publish flag and the turn
count), runtime.js (master's output scale under the compositor-feature
guard; hostxcount with the kernel argument to addMemory).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
The ejected mspaint site ships no sw.js and never reloads. nt.html's pack=
names the site's pack.json; a packed file (the bundle, the units) is
fetched as FILE.zst?v=BUILD and decoded in the page with zstddec,
straight into the buffer that is then moved to pid 1. The page itself is
plain files (80.7 MB download for mspaint, from 75.4 MB). The launcher
goes straight to nt.html when it is cross-origin isolated, and says which
headers to send when it is not; a W3 site gets serve.mjs and _headers.
pid 1's Worker lets go of the moved bundle by moving it on to a closed
MessagePort: transfer(0) detaches V8's view but a buffer that arrived by
postMessage stays resident until the Worker collects, which an idle guest
Worker never does (Chromium on gooner: 513 MiB renderer RSS either way
with a 400 MiB buffer, 112 MiB moved on).
Memory, renderer PSS on worldmachine (mspaint, mem probe): peak 3.76 GiB
with this, from 4.33 GiB with the service worker (s-ej3) and 6.98 GiB for
the s-demo6 page; at the title 3.24 GiB from 3.86 GiB. The bundle's
459 MiB second copy and the service worker's 236 MiB of decoded chunks
are gone.
tests/eject/w3-page.mjs: no sw.js, no registration, one load each of
the launcher and nt.html.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
Signals is the process's ProcSig (dispositions behind an RwLock, the
shared pending word, the interval timers with an atomic earliest
deadline) and the running thread's ThreadSig (pending and mask as atomic
words). Switching threads swaps an Arc; save_thread/load_thread are gone.
Each page Worker holds its own thread's ThreadSig (sigs.rs). A safepoint
asks whether a signal would do something, collects the inbox, fires due
timers and builds the handler frame with no kernel lock and no hints:
SIGNAL_SEEN, LOCAL_SIGNALS, TIMERS_ARMED and the 64th-safepoint sweep are
deleted, with the locked check and the locked delivery. The handler's
start reaches schedule through HANDLER_AT. waitq's idle turn asks the
signal state instead of the hints.
- The proc table's signal sequence moves after the inbox bit is set
(it moved before, which the sweep hid).
- A delivery claims its signal with one fetch_and before building the
frame, and collecting raises each signal once with its sender: two
raises let a lock-free deliverer take a process-directed signal twice.
- thread-signals' page failure was the test's 100 ms sleep (a page starts
a thread's Worker later); it hands off under the mutex now.
close-while-reading waits for its readers. New signal-storm: targeted
pthread_kill to spinning threads, a shared signal taken exactly once,
masks respected. All three in PAGE_PROGRAMS.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
glxgears ran at about 45 fps with no window whenever it was not the first
X client to use its window id: one typed in the xterm after one from the
launcher, or the reverse. The window manager kept the set of windows it had
Composite-redirected and never removed one. The X server gives a new client
the id a gone client had, so the new window was taken for already redirected
and never was, and rootless Xwayland makes no wl_surface for a window that
is not redirected. Traced on edger with ?wltrace=1: the second client
connected, the window manager got its events, and no surface was ever made.
DestroyNotify now drops the id from the redirected set and from the pending
pairings. Unit test: a_window_id_used_again_is_redirected_again (fails
without the fix).
The launcher no longer starts the host's Mesa before a GL item (startGl).
Every program, launched or typed, starts it with its first GL request
(runtime.js glLazy), so a launch runs the same command a shell would, in
the session's environment. The other difference was setsid and the log
file, which only keep the program off pid 1's terminal.
web/build-all.sh: the Xwayland/hostwl rebuild key now covers
crates/gex-wayland and crates/gex-xwm, which hostwl is built from. Before,
a change to either alone shipped the old hostwl.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
Every pool the page's Xwayland made stayed allocated for good. Xwayland
drops a window buffer unused for a second and makes a new pool, so an xterm
printing a line a second kept about 0.9 MB per line.
A pool now counts its references: the server's descriptor, its mappings
(gex_shm_unmap in web/xwayland/gexos.c now calls shmUnmap; before it was a
no-op), and the compositor's handles. shims/hostwl gives a handle back
(host_release) once Client::fds_in_use no longer names it: no wl_shm_pool,
no wl_buffer, and no surface showing pixels from it. A surface keeps its
shown pool, because the buffer is released at commit and may be destroyed
while still on screen. With no reference left, hostx frees the pixels once
the page has acked a scene published after the drop (WlProxy.generation and
drawnGeneration), so a frame already on its way is never read from freed
memory. Keymap pools are never freed: the server does not map them through
shmAddr, so nothing would count their last reader.
The leg is a default leg of tests/demo/desktop.mjs and also the step
{"pools"}. The xterm prints a line a second for 12 s, and the X server's
live pools (?hostxcount=1 pools=N, now always on in the test) must not grow
by more than 2. On edger, with freeing disabled: 4 then 14, FAIL. With it:
4 then 4, at dpr 1 and 2. Unit test:
a_descriptor_is_in_use_while_a_surface_shows_its_pixels. The SP Xwayland
(web/build-all.sh --sp --x-only) builds.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
gex-eject:
- features.rs: user/eject.json "features" (compositor, gl, multiprocess,
translation); a feature off that no build can remove fails, naming why.
- w3.rs: gexc eject of a tools/eject/record-w3.mjs recording (w3-session.json:
the page's launch, the processes, units used and missed, wasm modules) into
a project (bundle, units, runtime wasm, recording); gexc eject build builds
the page with GEX_FEATURES_OFF and writes build/web (page, rootfs.tar,
units, w3.json). web/eject-site.mjs packs it (--strip --prune --dedupe); the
launcher starts nt.html as recorded. tests/eject/w3-page.mjs is the leg:
the site reaches its window with the features off really gone.
- gex.mjs: [gex-record] lines (units with keys, processes, wasm modules);
nt-page: __nt.session.
Memory (mspaint demo, worldmachine probe):
- The shim's exec, load and snapshot-restore paths zeroed whole mappings
with fill(0), committing every page of reservations nothing had touched
(78 MiB per Wine process at 10-88 MiB, more in two processes):
gex_kernel::zero_dirty_bytes writes only pages that are not zero.
- The page's bundle was a SharedArrayBuffer, resident all session beside
the kernel's copy (459 MiB): now an ArrayBuffer transferred to pid 1's
Worker, which frees it (ArrayBuffer.transfer(0)) once loaded.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
Bug 3 (the xterm not presenting until clicked off and back on). The page's X
server closes a wl_shm pool's descriptor as soon as it has sent it, and its
next pool gets the same number. The compositor was handed the number and
located each buffer at commit time, so a buffer of an older pool resolved to
whichever pool held the number now. Typing into the xterm reuses its two
buffers; with both pools on one number the window showed the frame before
the one drawn, until something made a fresh pool. A cursor pool on the reused
number was too small for the frame, and the commit was dropped. hostx now
mints a handle per descriptor sent, as SCM_RIGHTS gives the receiver its own
descriptor. Guest Wayland clients (foot, Wine, Unity) pass real kernel
descriptors and never took this path.
The leg (a default leg of tests/demo/desktop.mjs, also step {"truth"}) clicks
into the xterm, types at a person's pace, moves the pointer across the window
edge, waits, then compares the X server's copy of the window (xwd through
tests/demo/guest/xsig.py: per-cell dark-pixel counts and a hash of the whole
dark bitmap) with the compositor's output. On edger, without the fix: 0 of 5
rounds equal at dpr 1 (47 dark pixels off, the last character and the
cursor), 0 of 3 at dpr 2 (228). With it: 5 of 5 and 3 of 3 exact.
The layers were measured with ?hostxcount=1 (kept): once a second, the bytes
the X server read from and wrote to each client, its bytes to the
compositor, and its scenes published. With ?wltrace=1 that showed two cursor
pools at one address and xterm buffers located to nothing. Damage and the
present path were ruled out first: forcing full uploads changed nothing, and
every commit changed the composed picture.
Known gap, in docs/gexcc.md §9.2: pool memory is never freed (as before).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
Linux reports the slave side going away to the master as EIO. gex said
end-of-file, and only once the session leader was gone from the process
table: it was in the table as a zombie until its parent waited. A parent
that reads the master until EIO and then calls waitpid (Python's pty.fork,
any terminal emulator) therefore read for ever.
- Pty::master_read answers EIO, not 0, when every slave has closed.
- hangup_is_real asks ProcTable::alive (in the table and not exited)
rather than exists. The reopen window (a live leader between slave
descriptors) still reads EAGAIN.
Tests: the pty unit test the_master_reads_eio_once_every_slave_has_closed,
and tests/demo/guest/tty.py, where the master must read EIO within 15 s of
the shell exiting, before waitpid. docs/gexcc.md §9.2 also states the
epoll-across-fork gap: the child gets a copy of the set, not Linux's shared
instance, until W's kernel-locking step 2.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
The user, 2026-10-12: the plan is removal, not fast paths. §5 now says so,
with the progress metric (lock-taking call sites in shims/browser/src, 232
on master f3689d12, and the per-syscall census), what has landed (steps 0
to 5 and 6a), and the remaining steps ordered by the measured wait of the
diag/klock-6e census: clocks, then waits on Linux's wait-queue model (a
slot per thread registered on every watched object, epoll notified by its
objects, no global WAKE re-check), then the fd table, futexes, per-thread
signal state, the memory map, signals and the thread table, lifecycle, and
native. Each says which structures become concurrent and how.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
One page, web/desktop.html, built stripped by web/build-demo.sh into
web/dist-demo. It opens two windows: an xterm running bash (X11 through the
in-page Xwayland) and a launcher drawn by the page. The base rootfs is 8.3 MB
of zstd for bash and xterm. Launcher items are tar.zst overlays
(tools/gexcc/debian/mkoverlay.py) unpacked into the running kernel, and apt
gets our pool as lazy files. Runtime .wasm ships as .wasm.zst
(web/src/zfetch.js). Measurements are in §9.1.
Shared code this changes, for every page:
- kernel: /proc lists every process (ProcTable info and snapshot through
the global kernel), and each has stat, status, statm, cmdline and comm. A
pty leaves FIONBIO and the close-on-exec ioctls to the kernel, which
fixes libuv (nvim) on a terminal.
- gex-wayland and the shims: wl_output.scale and set_buffer_scale.
Records carry the buffer and client scales; scale 1 behaves as before.
- compositor: a device-pixel output and decorations scaled by the page's
scale (1 unless a page passes one). Nearest sampling for 1:1 draws. The
client's cursor becomes the canvas's CSS cursor on every page (before,
only in direct present), and no cursor is composited. The present path
is unchanged: the worker's own WebGPU canvas, driven by its
requestAnimationFrame.
- runtime.js: wlScale; glLazy starts Mesa on the first guest GL request.
- web/build-all.sh: --x-only and --gl-only, for build-demo.sh.
Fixes from the first users, and the open xterm presentation report, are in
§9.2. tests/demo/desktop.mjs drives the demo. Its cursor and presentation
legs run unless DEMO_LEGS=0.
Squashed from demo/desktop (a6b38999 plus the driver commit) on master
e4c2a2c3. That branch's history also committed web/public/gex-apt (56
.debs of build output) by mistake, and this commit does not carry it.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c