setGlobals left the library's exports, but a test drives the page's
switches through it without a page (gexdev tests/page-switches). It is
now its own module, which app.js imports and the library does not
export.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
start()'s options, its handle, its errors, the Linux program's library,
and what is not the API (the test page's build option, the globals,
gex.mjs). The eject README's copy of the table, which had fallen behind
(no uid, wisp, pageHosts, glLazy, launch), now points here.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
A typo in an option name (onlog, contaner) was ignored, and a wrong
value (args as a string, size 'WxH' mistyped) failed later or not at
all. start() now rejects at once naming the option, with the nearest
name for a typo; resize() checks its size the same way. setGlobals is
no longer exported from the library: only start() and fromLaunch are.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
The page-thread desktop (DomDesktop) used to read the whole scene and
reconcile it on every animation frame. Each frame built readScene's
records, a Set of live keys and a key string per surface. It now does
what the scene compositor already does: when no block has been claimed,
freed or published to since the last reconcile, every publish is acked,
nothing came from the page (a memory, a pixel buffer, an exit, a
resize), and every window got its pixels, the frame skips the reconcile.
A window whose pixels have not arrived yet keeps the next frame a full
one, as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
Every net_status, net_avail, net_recv and net_peek used to be a round
trip to the page, once per socket per poll. Now each open stream gets a
receive ring in the network channel's shared memory (net-bridge.mjs
Rings, up to 64 slots of 256 KiB). The rings live in a growable
SharedArrayBuffer that starts empty and grows four rings at a time as
streams open, so a page whose guest never opens a socket holds none of
it. WispNetwork writes what arrives into the ring, keeps whatever does
not fit, and moves it in when a reader asks (REFILL). It also writes the
stream's state there. NetProxy answers status, poll, recv and peek from
the ring. A stream opened once every ring is taken, one already closed,
and every stream on an engine without growable shared memory still go
through the page. check-net-rings.mjs runs a WispNetwork on a stand-in
Wisp client against a NetProxy in a worker thread.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
options.mjs holds each debug and tuning switch once: its start-message
name, global, URL key, environment variable and kind. Pages parse their
URL with pageOptions, runtime.js fills extra and opts with startOptions,
worker.mjs sets the globals with workerOptions and run-node.mjs reads the
environment with envOptions, in place of the copies each kept. A switch
is now added in one place. check-options.mjs follows every row from URL
or environment to a Worker's globals.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
GEX_RELAY_ALL (?relayall), GEX_EAGER_BLOCKS (?eagerblocks),
GEX_NO_MODULE_STORE, GEX_UNITS_EAGER, GEX_FORK_MODULES (?forkmods) and
GEX_PER_MISS (?permiss, the shim's gex_set_per_miss): nothing set them
but a person, and each kept a path besides the default alive. Small
modules are not relayed, dense modules wait for their first miss, the
module store is used where it can be, units are instantiated at their
first call, a fork hands its child its modules, and a miss takes up to 16
stored units.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
eject-site.mjs --rest-paths takes "PATH HEAD" lines (HEAD in MiB): the
file goes to the rest bundle whole, and its first HEAD MiB stay in the
bundle at its first-read place (site-bundle.mjs placeHeads), named in
lazy.json as the entry's sixth field. The page adds the lazy file over
the bundled head (gex.mjs), and the kernel takes a bundled copy that is
shorter than the lazy file and a whole number of chunks long as the
file's first chunks, already fetched (tmpfs.rs seed_head): what the boot
reads of a game's music, read at process start, comes from the bundle
and the rest bundle is not asked for it.
Tests: tests/eject/site-rest-heads.mjs; gex-kernel
a_bundled_head_seeds_a_lazy_file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
Since the compositor's channel has pixel buffers, WlProxy.publish copies
each surface's damaged rows into the block's buffer out of the memory it
reads the records from. hostx publishes the scene its Wayland server
module wrote into that module's memory, but the X windows' pixels are in
the X server's memory: every Xwayland window was copied from the wrong
memory, and drew black. The KDE desktop, and any X11 program in the
browser, showed nothing.
publish now takes the pixels' memory apart from the records' (the same by
default), and hostx passes the X server's. check-wl-pixels-memory.mjs
publishes a record from one memory whose pixels are in another.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
Every Worker loaded a zstd decoder before its kernel started, whatever
the site: an engine memory reservation each, on a site with no rest
bundle as well. A desktop session's Workers then ran out ("could not
allocate memory" as a program started, Konsole's shell among them).
The page now says when a lazy file is in a rest bundle, and only then
does a Worker without the lazy-file owner load a decoder.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
Each Worker loaded a zstd decoder of its own for the rest bundle before
its kernel started: an engine memory reservation per Worker. A desktop
session's Workers ran the page out of them, every decoder after that
failed to load, and no file in the rest bundle could be read (a program
started from the desktop never came up).
Where the page has its lazy-file owner (lazy-cache-worker.js, shared
memory and a Cache Storage that holds), a Worker asks it for a rest
bundle file as for any lazy file, and loads no decoder. The owner
fetches the frames by range, decodes them with its one decoder and
keeps the last few decoded, for every Worker. Without an owner, a
Worker decodes for itself as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
The lazy files' list may give each a mode after its time; without it an
executable read on demand (a program only a later click starts) was
0644 and would not run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
(cherry picked from commit 8f929d92360f5ba46ee50de20f57b6aafd3f8e07)
The lazy files as one uncompressed tar, in read order then by path, its
stream cut into independent zstd frames of one size (--rest-frame MiB,
default 8) with an index (rest.bin.json); a lazy file is rest.bin#OFFSET,
read by fetching and decoding the frames over its range, the last few kept.
Untested end to end yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
(cherry picked from commit c71ddb40624f8af042754ee507b2cb6d83d3fcee)
A site's first bundle holds what the recorded run read up to its
first usable frame, in the order it read them, so the page can start on
the files it needs first. Each file's first use takes the next number of
one counter; read_set lists by it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
A recording whose loader is Wine's x86_64-unix one ejects as a wine64
project. It differs from a wine32 one in four ways:
- it ships the 64-bit NT trampolines' units, both the memory64 one and
the plain one, since a memory64 program runs beside Wine's own
processes;
- the runtime carries the memory64 shim and seed;
- the launch file says whether the page had the D3D device;
- resources default to whole.
The rest of the project, build and site is a wine32 one's.
Unit keys are matched for either guest memory width (rsrc key_with_mem,
the ABI hash per width), so identities and resource rekeying find
memory64 units. The launch file carries the bundle's user xattrs (the
memory64 mark on a program), which the site's rebuilt bundle drops; the
app library sets them with the tree's own. The recorder reads a Worker's
relayed lines ("[gex-console-error] ..."), so uncovered addresses are
no longer lost, and the session records the page's d3d.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
The host map crosses postMessage as a Map; run-node parses
GEX_PAGE_HOSTS with page-host.mjs's own pageHosts, and says network and
tcp apart. Nothing after a request that is not answered (a POST, a bad
one) is read; a HEAD of a body of unknown length says no length. The
Node lane's files are streamed, one byte range if asked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
start({pageHosts: {host: path}}) names hosts the page answers itself,
instead of the network proxy, which cannot reach the page's own origin. A
guest speaks plain HTTP/1.1 to one: GET and HEAD, keep-alive and pipelined
requests, with each body streamed from fetch. Anything else, a POST or a
WebSocket upgrade, gets 405 and the connection ends. An ejected page can
serve its guest any files this way, such as a package repository beside it.
shims/browser/js/page-host.mjs is the stream, shaped like a Wisp client's.
WispNetwork holds one for a page-served host, and needs no proxy when there
are only those. So do a Worker's own network and the system Worker's. The
single-thread build shares the page's. NodeNetwork does the same for
run-node's GEX_PAGE_HOSTS=name=DIR,..., and with no GEX_NETWORK it refuses
every other host. check-page-host.mjs tests the HTTP side.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
The channel carried a 64 MiB SharedArrayBuffer for each of its 32
blocks, made on the page whether a block was ever claimed: 2 GiB of the
page's data size for the few MiB a session's surfaces fill. Each guest's
proxy now makes its own buffer on its first surface with pixels, sized to
what its surfaces need with a quarter to spare, and a larger one when they
outgrow it (up to 64 MiB), posting each to the compositor ('wl-pixels').
A record names its buffer by number; the compositor draws a surface once
that buffer has come, drops the one it replaces, and drops a process's
buffers when it exits. Only the damage is copied when the buffer has the
surface's last commit, so two threads publishing one process's surfaces
each copy whole what the other published. The single-thread build is
unchanged: it has no pixel buffers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
A D3D program's calls reach the page as a stream of records through the GL
device's ring (its own magic and version). With GEX_MESA_DIRK=1,
web/build-mesa-wasm.sh fetches dxvk at Mesa's pinned commit
(third_party/mesa/subprojects/dxvk) and links its D3D11/DXGI front end on a
gex_wgpu screen into the replay (web/mesa/gexd3d-mesa.cpp). A present files
the frame under the client's window like a GL client's: the compositor shows
it, newest first, and the frame report counts D3D presents and how far behind
a window is. d3d=1 on the page turns the device on; a window with no surface
shows nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
Every animation frame read the whole scene (readScene's records, a Map
and Set of surfaces), reconciled it, and built the output's signature
string before finding that nothing had changed. A frame now first checks,
against what the last acked frame drew from, each block's token and
generation and whether its publishes are acked, whether a message came
from the page, and whether a GL frame was presented; with none of them it
returns at once. A surface whose pixels could not be read yet keeps the
next frame a full one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
compositor.js and dom-compositor.js each had the evdev key map, the
modifier bits and the button codes, and the copies had drifted: the scene
compositor dropped the back and forward buttons. Both now import
input-map.js. A back or forward press on the scene's canvas no longer lets
its release navigate the page's history.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
The per-unit `[unit]` and `[cold]` lines and a link group's hold are
`GEX_LOG=unit` output; the page's start-up marks, the runtime rings and
Mesa's load time are `GEX_LOG=debug`. The fill's end and its late units
are still said by default. The wait census, whose report is debug output,
no longer runs a timer in every Worker otherwise.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
services.mjs was never wired to anything. The kernel import list was a
fallback for a caller without a shim Module, and every caller passes one.
#deliverFault, #guestView, DomDesktop.glStats and
MesaGlDevice.surfaceGeneration have no callers, and the per-block export
loop served a module shape no emitter produces.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
The repository that builds the wasm32 Debian system and the desktop demo's
guest side is named debdemo, not gex-debian. The READMEs, the web build's
messages and the comments say so; where they named a script inside it,
they name the repository.
No key inputs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
V8 counts every wasm memory an isolate holds at its whole length in the
size its next full collection is scheduled from. Past about 8 GiB the
headroom stops growing, and the isolate collects every few megabytes it
allocates. The compositor worker held every guest process's memory to
read wl_shm pixels in place: a memory64 game's alone, a Wine process's
at about 2 GiB each, and the global kernel's. Between them it made a
full collection 25 times a second.
Each arena block now has a fixed SharedArrayBuffer for pixels. A plain
SAB is not counted, and writeTexture refuses a growable one. The guest's
WlProxy copies each shm surface's damaged rows into it as it publishes,
and nothing for a commit already copied. It then points the record's
address and stride there. The compositor (both kinds) reads only that
buffer and keeps no memory when the channel has pixel buffers.
wl-memory carries no process memory when the channel has pixel buffers,
so a process's threads no longer re-send it one per worker. The global
kernel's memory goes once a page (an arena flag). The compositor needed
the kernel only to notify a process's wait queue after an input event.
It now marks the block in the arena and wakes a relay on the page
thread, which holds the kernel anyway and does the same notify.
The single-thread build has no pixel buffers and works as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
Comments named the test harness gex is developed with (its gate and bench
runner) and one still said a site's kind is w3; they say what they mean
without it.
No key inputs. wine32-dist.sh is in TOOLS_KEY and build-mesa-wasm.sh in
Mesa's stamp: the dist and Mesa rebuild.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
Six comments named tests that live in the development repository, not
here (an audio loopback measurement, a pipe race, the configure corpus,
a GL probe, the single-thread check, a benchmark). They say what the
test showed instead.
No key inputs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
The NT layer went, but its web launch stayed: eject-site.mjs still built a
site from build/web/nt.json, which no gex-eject writes any more, with its
options (--bench, --drop, --overlay, --input, --args, --wgsl-cache,
--no-direct), and app.js's launchBuild still read that launch. Both are
gone. eject-site.mjs takes a 32-bit Wine project and says so when it is
given anything else; launchBuild names a launch kind it does not know.
No key inputs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
- WINE32_KUSER (gex_abi), GEX_WINE32_KUSER (the provider header), the
`:wine32kuser` unit tag; the eject kind "wine32" and the recording format
"gex-wine32-session" (the old "w3" and "gex-w3-session" still read).
- The page mounts 32-bit Wine's dist at /wine32 (tools/wine-wasm/run.sh,
page-run.mjs, units.sh; a recording's /w3 paths still map) and reports
"== wine32 page:".
- Comments no longer point at docs that are not in the repository; the
eject ones point at crates/gex-eject/README.md.
Changes unit keys (gex-abi, gex-cgen's header and tag).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
A process that maps a late unit (one in the fill) while the fill is still
coming asks the host for it as for any unit. The host then waits for the
fill's compile of it instead of fetching it a second time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
- `{open, modules}`: an open preload archive of unit modules alone,
compiled during the load and let go after, so a boot's unit set comes
in one download beside the bundle rather than a request per unit as
each Worker first needs one.
- With compressed units the page fetches the decoder module once and
hands every Worker a Blob URL of it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
- cgenzst=1: a unit's module is fetched as KEY/c.wasm.zst. The host's prefetch
fetches, decodes (zstddec) and compiles it in a Worker of its own
(unit-worker.js). A Worker's synchronous load decodes it with its own decoder.
Both fall back to KEY/c.wasm on a 404.
- A preload archive marked `open` holds some of the units' files (every
unit's c.entries, providers/). Its files stay views of one shared buffer,
and a Worker still asks the server for the files it does not hold.
- The fork exit line says the zero pages it left alone beside what it
copied, not as part of it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
A unit's Module was compiled when its process published it, by every
process that mapped the library, each fetching the bytes with a
synchronous request first; the Module went only to that process's own
Workers. A desktop's start compiled 5.7 thousand units for 472 distinct
ones, 15 GiB of wasm, with 87 s of synchronous fetches and 32 s of
compiles summed over its Workers.
Now:
- Publishing a unit catch-up defers writes its entries and compiles
nothing; the unit is compiled at its first call in a Worker, unless
the Module has reached that Worker by then.
- The host fetches and compiles a published unit asynchronously, off
the guest's threads (`prefetchUnit`), and keeps every unit Module for
the page: handed to the pool and to each Worker that starts, and to
the Workers of the process that wanted it.
- The unit cache's small files (entries, indirect-call profiles,
provider files, and the ones it lacks) are kept for the page in shared
memory and handed to each Worker that starts: a forked child placing
its parent's units asks the server for none of them.
- A Worker that misses fetches only the module's bytes.
- A fork record's bulk (the parent's modules, its memory image and
maps) and a new thread's Memory go from the parent's Worker to the
child's over a MessageChannel of their own; the host passes the port.
A module or Memory arriving on the page's thread counts against its
isolate's external memory, and at a fork's four thousand modules that
ran one full collection after another there, 1.5 s with every process
waiting on that thread. The single-thread build, Node and ?memories=1
keep the one message.
- A process asking for a unit the host is fetching or keeps is handed
it when it comes, not compiled again.
- GEX_LOG=win logs the first map of each named X window
(`[win] map 0x.. "konsole"`), from the X requests the page relays,
and a Wayland toplevel's first paint and title.
The single-thread build, GEX_UNITS_EAGER, providers and the function JIT
keep compiling as they publish. GEX_LOG=unitstall times each first call,
catch-up, publish, synchronous fetch and compile ahead.
KDE site, Konsole launched from the desktop, base against this, two runs
each: desktop ready 135.3/136.6 s against 71.0/70.5 s from navigation;
Enter to Konsole's bash 25.2/25.4 s against 13.1/13.2 s; unit compiles
5691/5565 against 518/526; browser peak RSS 20245/20503 MiB against
17617/17634 MiB. Enter to Konsole's window ([win] map), warm launch:
15.3 s against 6.7/6.9 s; cold 16.1 s against 7.9 s.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
Changes unit keys; rebuild inputs: the gexcc install.
Comments only, plus code the removed NT layer left behind: the session
manifest's `nt` record (NtSession, NtImage), the native shim's unused
profile::note_pe, and gex-kernel's gex-gl dependency. "The NT layer's
split" is now "the 2 GiB split"; W3 and W64 in prose are 32-bit and
64-bit Wine. Keyed inputs (cgen providers, Wine dist, guest/gl, web/mesa,
the Xwayland crates, the mono provider) are left for a separate commit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
Changes unit keys; rebuild inputs: the Mesa key, the Wine dist keys.
32-bit Wine (Wine's own Unix side as wasm32) and 64-bit native Wine run
Windows programs; the NT layer, gex's own Windows personality, goes.
Deleted: crates/gex-kernel/src/nt, the native and browser shims' nt.rs,
gexc's NT run path (nt_run.rs, --wine-prefix, cgen-prefix), gex-eject's
PE projects (pe.rs) and gex-rt, whose only job was running them,
tests/nt and tests/providers, and the tools/nt-gen-* generators with
their lists. tests/nt's programs move to the dev repo, which the Wine
legs already run them from.
Kept, because 32-bit Wine uses them: the trampoline unit and its
providers, aot_only, web/nt.html and nt-page.js (the Wine page),
tools/nt-stage-wine.sh (the prefix the wine32 dist stages from, now
without its unit build), and in the browser shim NATIVE_LEFT, the
in-place serve of an AOT-only run and the process end a provider asks
for, now in lib.rs. gex_provider_alloc takes Linux pages, and the
nt_qpc import returns 0: it binds only to a PE ntdll. --no-cgen is
accepted and does nothing. web/test/browser-harness.mjs is the old
tests/nt harness, for the Wine page's runner.
pewasm is gone too: the PE loader comparison test and its label
helper, and every comment that measured gex against it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
No key inputs (unit keys); rebuild inputs: the Wine dist keys, the gexcc install.
Paths, env names (W3_*, W64_*, GEX_W3_* become WINE32_*, WINE64_*,
GEX_WINE32_*), page switches and exports, identifiers and comments.
Unit-keyed and recovery-keyed names stay (W3_KUSER, GEX_W3_KUSER), so
unit keys are unchanged; the Wine dist keys change with the scripts
they hash.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
A dlopen'd module with thread-local data was refused. Now each thread
that instantiates it gets a block of its own: allocated from per-thread
chunks and set up with the module's __wasm_init_tls, natively and in
each browser Worker. A fork's child keeps the forking thread's blocks
(the link map carries which modules the program started with, whose
blocks libc's registry handles). A thread's chunks are unmapped when it
exits.
A thread already running when another one dlopens caught up with the
function table only when it called through a slot it had not filled,
and a slot holding the resolve stand-in traps on a type mismatch for any
signature but the stand-in's. Every dlopen now bumps a generation, and
a thread that has not seen the latest catches up at its next system
call's return, so a pointer handed over through the kernel (a futex, a
pipe) is in its table before it can call it.
That leaves a pointer handed over lock-free, with no system call on the
receiving side before the call. A trap there cannot be retried, since a
wasm32 program's frames are gone by then. gex now names the cause:
"called a function from a dlopen this thread had not caught up with".
GEX_DL_CHECKED_CALLS=1 (the page's dlchecked=1), off by default and
costing nothing when off, rewrites every wasm32 module of the program
so that an indirect call checks its index against the thread's
caught-up slot count and catches up on a miss
(gex_dylink::checked_calls).
tests/gexcc/dltls.sh: a startup library's and a dlopen'd library's
thread-locals in main, in a thread started before the dlopen and two
after, and in a fork's child. tests/gexcc/dlspin.sh: the lock-free
handoff, a known gap unchecked and passing checked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
web/public/mesa-wgpu-{replay,smoke}.html and web/public/gl_cts (tests copy
them into the served tree), and tools/frame-stats.py, surface-diff.py and
icall-hints.py. NOTICE drops the CTS data with them; gl44_coverage.py
stays, since gen_gl44_manifest.py imports it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598