Every guest thread's last system call (x8, x0-x2) and guest backtrace, for a
hang: a thread blocked in a system call has left its registers in its context.
The threads are kept in a registry (id, context, guest stack) as they are made.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
cache_op was empty. DCZID_EL0 says DC ZVA is implemented (64-byte blocks),
so bionic's memset zeroes everything past its first block with it, and that
memory was left as it was: libpng's freshly memset png_struct had stale
callbacks in it ("Can't set both read_data_fn and write_data_fn"), and
Doodle Jump's first PNG load crashed. DC_ZVA now zeroes the block, as the
browser shim does, and IC_INVALIDATE retires the translations of the page
(a guest JIT rewrote its code), as unmapping executable memory already did.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The across-lanes long sums, which clang emits for a popcount (cnt, then
uaddlv), were undecoded. They widen with extend_low (and extend_high in the
128-bit form) and then reduce as addv does, at twice the lane width.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
fmul, fmulx, fmla and fmls on s and d registers with one lane of a vector
(`fmul s1, s3, v2.s[0]`, from libDoodleJump.so's matrix setup) were left
undecoded, an illegal instruction. They are the vector by-element forms with a
scalar arrangement, which the lifter already masks to the one element.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- diag.rs: every executable mapping of a file is recorded, and a raised signal, an illegal instruction, a brk or a
jump into nothing prints the guest's backtrace (pc, x30, then the x29 frame chain) as library+offset (symbol+delta).
- rt_tgsigqueueinfo is a raised signal too (bionic's abort() raises SIGABRT with it).
- The load bias is taken from the executable segment the mapping's offset starts: a small library's code can share
its first file page with the read-only segment before it, which gave liblog a bias 16 KiB off.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
gex_embed_provide(soname, table, n, missing): when a library of that name is mapped executable, its functions are
bound to the host's -- the table's by name, and, given a `missing`, every other function it exports (a stub library:
android-wasm's native/guest builds libandroid, libEGL, libGLES*, libOpenSLES, ... as one `udf` each), with
"soname!symbol" as the user value. gex_embed_missing logs it once and returns 0. Without `missing`, only the named
functions are bound (liblog: its sink, __aw_log_write, among real guest code).
bionictest, with APP_LIBS naming a directory of an app's arm64-v8a libraries: Doodle Jump 3.11.40's libDoodleJump.so
(8 MB) dlopened against the guest image -- liblog, libandroid, libEGL, libOpenSLES and libGLESv1_CM loaded and bound,
its constructors run, a JNI method found.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
What an Android app's arm64 libraries need under gex in another program's memory, with android-wasm's guest image
(native/guest: linker64, libc, libm, libdl, libz built for arm64 from the AOSP tree).
- vm.rs: the guest's memory map. A mapping without an address is a block of the host's heap of its own; MAP_FIXED
lands inside a block (the linker reserves a span and maps segments into it); a block goes back once nothing in it
is mapped. No protection, except that a range turning executable is handed to the translator, and one that stops
being executable has its translations retired (jit::unmap_exec: the ranges, the known set, the directory).
- elf.rs: images mapped with their protections, symbol tables read (.symtab and .dynsym: linker64's .symtab names are
prefixed __dl_), and gex_embed_boot: linker64 run on the calling thread from a kernel-shaped initial stack whose
AT_ENTRY is a return pad, so the call returns once libc and its dependencies are loaded and initialized. The thread
is the guest's main thread after. gex_embed_dlopen/dlsym/dlerror call the linker's __loader_* functions.
- overlay.rs: when libc.so is mapped executable, its malloc family is bound to the host's dlmalloc and every scudo_*
entry point to a no-op (scudo reserves gigabytes at start), before any of its code runs; and __aw_thread_attach is
found, so every other host thread is adopted into bionic on its first call in (thread.rs).
- call.rs: calls keep the thread pointer the callee set (the linker's, __aw_thread_attach's).
- sys.rs and c/gex_embed_sys.c: system calls. Memory, threads, signals and the process in Rust; files, time, futexes
and ids by the host's libc, with arm64 Linux's flags, structs and errno numbers translated. Guest fds are host fds.
Every C function the Rust side calls takes fixed-width types: a `long` is 32 bits there, and a call whose wasm type
disagrees with the definition is linked as a trap.
- Guest stacks start zeroed, as a kernel's do: linker64's start reads fields of a soinfo on its stack nothing wrote.
- A trap in guest code prints every register.
test/bionictest.c (with GUEST naming the image): linker64 booted; libm's sin called, bit-exact; libc's snprintf with
AArch64 varargs and a double; guest malloc is the host's heap; getauxval; guest stdio writes a file the host reads;
a second pthread adopted into bionic calls strlen and snprintf. Passes, as embedtest does.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A staticlib for wasm32-unknown-unknown that an emscripten host (android-wasm's dotnet.native.wasm) links beside its
own objects, with js/gex_embed.js as a --js-library and include/gex_embed.h as its C API. The host keeps its memory
layout: gex's block is a static in the host's .bss, and Rust allocates with the host's malloc.
- A miss translates under one lock (translate_runtime_in over the executable range), appends the module to a log in
linear memory, installs it on this Worker and publishes its directory entries. Other Workers install from the log
when they next miss; the table guard sends them there.
- The imports translated code calls are static functions, so each Worker hands every module the host's own
function by its table index: calls are wasm to wasm.
- gex_embed_call runs a guest function by the AAPCS64 on the calling thread, which gets a context and a guest stack
the first time. x30 is a return pad per nesting depth; its miss returns to the caller.
- gex_embed_thunk binds a host function to a guest address (Intrinsic::Replace).
- A first few system calls: write, exit, getpid/gettid, clock_gettime.
build.sh builds with the host's exact wasm features and build-std (1.97.1 by default: 1.96.0 has no rust-src here),
and rewrites the archive's target_features without LLVM 22's implied bulk-memory-opt, which the frozen emsdk's
binaryen can't be passed (--enable-bulk-memory-opt).
test/: AArch64 functions (guest.s) called from an emscripten program with android-wasm's link flags, under node --
loops, a host call, a nested guest-host-guest call, system calls, float and stack arguments, four threads catching
up on each other's translations, exact ldxr/stxr and LSE counters, and (`high`) code above 2 GiB. All pass.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
gex_abi::set_hosted_layout(base, floor) moves everything translated code reads at a constant address -- the fixed
words, the probe cache, the patched-page bits and the dispatch directory -- into one 5 MiB block at `base`
(gex_abi::hosted), for a host that owns the memory (an emscripten program: data from 0x400, dlmalloc above). The
directory covers all 4 GiB there, since the host's allocator places code anywhere. The fixed layout and its output
are unchanged: every constant stays, and the accessors (fcache(), exclusive_scratch(), split_lock(), ...) answer
with it unless hosted.
Hosted, dispatch checks a slot is inside the table before calling it (module::emit_table_guard), and calls the miss
stub otherwise: the host's threads are Workers with their own copies of the tables, and the directory they share
can name a slot a Worker hasn't grown its table to yet. Inline dispatch, dense tables and the null guard are off
hosted; the memory import declares no minimum; knobs() has a bit for it.
gex_xlate::translate_runtime_in reads guest memory through a window (GuestWindow { bytes, base }) rather than a
slice indexed from zero, which on wasm32 can't start at address zero or reach past 2 GiB. translate_runtime is the
window from zero.
tests/hosted.rs: under a hosted base, with code at 0x9000_0000, nothing of the fixed layout's runtime region is
named, the hosted directory and scratch word are, the seed's dispatch carries a guard per table, and the layout
can't change after a translation.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Wine's session programs' memories started at 512 MiB: the native core's
data window sat at the top of the band to 0x20000000, Wine's unpinned
images top-down below it, and the arena stepped over the whole band. The
images and the window now sit at the bottom of the game DLLs' part
(0x19000000), which those programs never load, and the island ends a guard
above them, so a session program starts at about 408 MiB. Wine's ntdll
places the images in the same range.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
The recorder keeps the volatile keys wineboot writes as [gex-record] reg
lines in a page run with WINEGEX_RECORD_HARDWARE=1; the build writes them
into the prefix's system.reg and user.reg and starts the page with
WINEGEX_NO_WINEBOOT=1. The 64-bit Wine pin moves to the commit that adds
both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
README: 64-bit Wine beside 32-bit, the page and ahead-of-time runs, the
rest of tools/ and tests/ in the layout, the page's JavaScript API
(web/src/README.md), and how the Node test scripts run.
NOTICE: the MIT permission text that musl's and Emscripten's code needs
beside their copyright lines, and where the Apache-2.0 text for the
Khronos-generated files is.
No key inputs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
Up to four zeroed memories, 768 MiB together, waited for a fork child
whatever the page did next, their pages touched and so resident. A page
that never forks again (a Wine game once wineboot has exited) held them
for its whole life. They now go 15 s after the last exit; a page that
forks keeps forking well inside that.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
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
An execve now drops the old program's bytes only after it has killed
their installs, and a killed install is skipped, so the catch-up no
longer finds them gone. The two JS fallbacks that dropped its error (a
thread gone, or the exec generation moved since adoption) go. adoptGen
stays: the Worker's exit uses it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
A fault delivered in place inside another's dispatch holds that level's
frames, as large as the code that faulted makes them, so the nesting cap
alone did not bound the host's stack: a deep enough nest ended in a host
stack overflow inside the shim, reported as a runtime crash. A nested
delivery now also checks that the stack has room for one more level and,
short, is delivered from the run loop as one past the cap is (said once).
A staged fault that a call-out's loop would deliver inside such a nest,
with no room left and frames under it that cannot be left, ends the
process as one whose stack overflowed (exit 253). run-node takes
GEX_WORKER_STACK_MB, a Worker stack as small as a browser's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
A lazy file's chunks were kept for good once fetched: a game reading its
gigabytes of data once held all of them in the tmpfs. Past 256 MiB a file's
chunks read longest ago go (it is never written; a chunk read again is
fetched again), never one the read being served needs, nor one seeded
from a bundle, which the file's URL does not carry.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
A Worker replaying the function table's log takes a provider's record as
the sign that its bytes are there, but a wasm32 program's bytes were
pushed after its record, and an execve dropped the old program's bytes
before killing its installs: in either window the replay found the bytes
gone ("published but cannot be loaded here"). The bytes now come before
the record, and the installs die before the bytes go; a provider whose
install fails and has been killed since is skipped. A Worker whose thread
went with the old program (out of guest code, so the exec did not wait
for it) no longer serves what is staged, which is the new program's, and
posts its end without reading the new program's state for its stats.
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
As the Wine page's start(): a name it does not know is an error with
the nearest name, and args and env of the wrong kind are named. The
Wine page's other options are accepted and do nothing, so one page can
start either kind of build.
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
The four JavaScript files a Linux program's eject writes used to be raw
strings inside web.rs: runtime/web/eject.mjs, app.mjs, run.mjs and
serve.mjs. They are now files beside index.html, pulled in with
include_str!, so node --check and an editor see them as JavaScript.
Their bytes are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
64-bit Wine now maps a program that can move at its dll-bases pin, so a
game's program whose base is beyond the process's space keeps one address,
and its unit, whatever the environment. The Wine pin moves to that commit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
WlProxy.publish used to build a string key, a rectangle object and a Map
entry for every surface, a list of objects for the pixel copy, and new
views of the guest's memory and the pixel buffer, all on every scheduler
turn. pollInput made a view each time, even with nothing to read. Both
also built console.timeStamp strings. Undrawn damage now lives in typed
arrays, the pixel copy reads its surfaces from typed arrays, the views
are kept while their buffer stays the same, and a narrow damaged row is
copied a word at a time rather than through a view per row. Arena
contents and pixel buffers are unchanged: an old-against-new run of
random publishes, draws and input matched word for word.
check-wl-publish.mjs covers merging undrawn damage, copying damaged
pixels and input order.
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
The shim hashes a pending module's bytes when it queues it, giving both
its content key and the unit cache's key (the dense base word zeroed),
in one pass. It publishes them with the queue's front, and
gex_jit_pending_key reads them, so #installTranslation(Sync) no longer
hashes the bytes in JS once or twice per install. gex_jit_installed
uses the same key where it hashed again. GEX_CHECK_INSTALL hashes the
bytes in JS too and reports any difference.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
The thread is marked gone only once the execve stops the old program's
threads, but the module's bytes go before that: in between, a thread Worker
catching up found them gone and still threw (bridge-share exec, 1 run in
8). The catch-up's error is also dropped when the process's exec
generation moved since the thread was adopted.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
A thread Worker that adopts its thread just before another thread's
execve could still be replaying the function table's log when the exec
released the old program's module. The replay then threw "published but
cannot be loaded here" and the process died. This shows up more often
now that a trap goes straight back into the guest. With the thread gone
(gex_thread_gone), the error is dropped. The shim stops the thread at
its first dispatch, which is what the execve waits for.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
An archive whose table sits at its end (a PBG4 .dat) reads its first
bytes and its last before the title. --rest-paths 'PATH HEAD tail' puts
the file's first HEAD MiB and its last, partial chunk in the bundle, and
the kernel holds both as fetched (tmpfs.rs seed_head), told apart from a
plain head by the copy's length.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
The game's Wine runs under WINEGEX_NO_DESKTOP with the Wayland driver: no
explorer, so no rpcss, and RunServices no longer starts one. Windowed
programs keep "explorer". Wine pins move to the commits that add it.
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
Every Worker opened the 'gex-fetch' BroadcastChannel when it imported
fetch-note.mjs, and every open channel is handed every other one's
messages: each lazy chunk, unit fetch and install was cloned to and woke
every Worker of the page (30 to 60 in the Win7 shell or FL), not only the
page that listens. A process's Worker now says its notes to its host on
its own port, in order, and the host says them on the channel for the
page; a realm with no host (the single-thread build, a page's own
Worker) opens the channel when it first says something, as before.
check-fetch-note.mjs checks both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
gex_after_trap counted the log and late bindings as due only with units
or the function JIT on, but JS replays them in every mode: a wasm32
program's module, and a thread's fresh TLS after another thread's execve,
come through the log with units off. The trap went straight back into the
guest, missed again as a catch-up, and livelocked (wasm32 execthreads).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598