integ: merge merge/unity-seed 0ddc9390

# Conflicts:
#	web/src/runtime.js
This commit is contained in:
velzie committed 2026-10-09 08:29:42 -04:00
commit ff69f7e446
27 files changed
+1300 -40

No files matched your search

+5 -3
View File
@@ -3,8 +3,10 @@
//!
//! * `GEX_CGEN_RECOVERY_INPUTS`: what decides an image's recovery (its GIR
//! and summaries) -- the IR, the lifters and decoders, gex-xlate's recovery
//! (everything in it but the runtime translator's `lib.rs` and the
//! profile and validation code), gex-abi, and gex-ld (placing the image);
//! (everything in it but the runtime translator's `lib.rs` and
//! `fjit.rs` -- the function JIT drives recovery at run time, a unit
//! never calls it -- and the profile and validation code), gex-abi, and
//! gex-ld (placing the image);
//! * `GEX_CGEN_UNIT_INPUTS`: what decides the C and the module from a
//! recovery -- this crate (and its runtime). What the C takes from gex-abi
//! is hashed at run time (`image::abi_fingerprint`: the header and every
@@ -55,7 +57,7 @@ fn main() {
rec.extend(src(c));
}
rec.extend(src("gex-xlate").into_iter().filter(|p| {
!matches!(p.file_name().and_then(|n| n.to_str()), Some("lib.rs" | "profile.rs" | "validate.rs"))
!matches!(p.file_name().and_then(|n| n.to_str()), Some("lib.rs" | "profile.rs" | "validate.rs" | "fjit.rs"))
}));
// gex-abi is not hashed here: what the C takes from it is the generated
// header and the values `image::abi_fingerprint` lists, which the unit
+4
View File
@@ -272,6 +272,10 @@ pub trait Translator: Send + Sync {
fn image(&self, _process: u64, _program: &gex_ld::Program, _bytes: Option<&[u8]>) {}
/// Guest process `process` is gone (its last reference dropped).
fn forget(&self, _process: u64) {}
/// Guest process `process` unmapped or reprotected the code in
/// `ranges` (`Kernel::retired_code`): what the translator learned of
/// those addresses no longer describes them.
fn retire(&self, _process: u64, _ranges: &[(u64, u64)]) {}
}
static TRANSLATOR: std::sync::OnceLock<Box<dyn Translator>> = std::sync::OnceLock::new();
+73 -4
View File
@@ -87,6 +87,29 @@ impl Jit {
self.add_image_in(process, image, elf);
}
/// Process `process` unmapped or reprotected the code in `ranges`:
/// what was learned there (function starts, never-returning functions,
/// functions translated, read-only ranges) described the code that was
/// there, and a `dlclose` and a later `dlopen` put another library at
/// the same addresses. Kept, the old library's starts split the new
/// one's functions where it had none: HK's SDL loads libdbus, unloads
/// it, and maps libxcb over it; a libdbus start ended libxcb's
/// `_xcb_in_read_block` before its `cmovle` as a tail call, flags dead,
/// and it read the X setup reply out of an empty queue.
pub fn retire(&self, process: u64, ranges: &[(u64, u64)]) {
let mut st = self.st.lock().unwrap_or_else(|e| e.into_inner());
let Some(l) = st.procs.get_mut(&process) else { return };
for &(lo, hi) in ranges.iter().filter(|r| r.1 > r.0) {
let gone: Vec<u64> = l.starts.range(lo..hi).map(|(&a, _)| a).collect();
for a in gone {
l.starts.remove(&a);
}
l.noreturn.retain(|&a| a < lo || a >= hi);
l.translated.retain(|&a| a < lo || a >= hi);
l.readonly.retain(|&(s, e)| e <= lo || s >= hi);
}
}
/// Process `process` is gone.
pub fn forget(&self, process: u64) {
let mut st = self.st.lock().unwrap_or_else(|e| e.into_inner());
@@ -107,18 +130,21 @@ impl Jit {
/// starts (symbols, `.eh_frame`, init arrays, relocations; `elf` is the
/// file, for `.eh_frame` and ELF32 symbols), with their extents, which
/// bound functions and computed-goto tables as they do ahead of time.
pub fn add_image(&self, image: &gex_ld::Image, elf: Option<&[u8]>) {
self.add_image_in(0, image, elf);
/// Returns how many function starts it knows of the image.
pub fn add_image(&self, image: &gex_ld::Image, elf: Option<&[u8]>) -> usize {
self.add_image_in(0, image, elf)
}
fn add_image_in(&self, process: u64, image: &gex_ld::Image, elf: Option<&[u8]>) {
fn add_image_in(&self, process: u64, image: &gex_ld::Image, elf: Option<&[u8]>) -> usize {
let code = Code::with_elf(image, elf);
let (starts, _) = func::seeds(&code, elf, false);
let mut guard = self.st.lock().unwrap_or_else(|e| e.into_inner());
let st = guard.procs.entry(process).or_default();
let mut n = 0;
for (a, info) in starts {
if info.strong() {
st.starts.insert(a, info);
n += 1;
}
}
st.readonly.extend(
@@ -128,6 +154,7 @@ impl Jit {
.filter(|s| !s.writable)
.map(|s| (s.vaddr, s.vaddr + s.bytes.len() as u64)),
);
n
}
/// A module for the function at `addr` (see the module documentation),
@@ -159,6 +186,28 @@ impl Jit {
volatile: &dyn Fn(u64) -> bool,
page_gen: &dyn Fn(u64) -> u32,
class32: bool,
) -> Option<Vec<u8>> {
self.translate_skipping(process, mem, addr, executable, &|_| false, read_only, volatile, page_gen, class32)
}
/// [`Jit::translate_in`], leaving out of the batch the callees `skip`
/// names (served by another tier: the call reaches them through the
/// table). `skip` bounds the batch only, never instruction fetch: a
/// function another tier serves can have an entry in the middle of the
/// one being recovered, and fetch taking its bytes for non-code decodes
/// them as illegal.
#[allow(clippy::too_many_arguments)]
pub fn translate_skipping(
&self,
process: u64,
mem: &[u8],
addr: u64,
executable: &dyn Fn(u64) -> bool,
skip: &dyn Fn(u64) -> bool,
read_only: &dyn Fn(u64, u64) -> bool,
volatile: &dyn Fn(u64) -> bool,
page_gen: &dyn Fn(u64) -> u32,
class32: bool,
) -> Option<Vec<u8>> {
if gex_abi::PPC || (class32 && !gex_abi::X86) {
return None;
@@ -188,11 +237,31 @@ impl Jit {
if batch.len() >= limit {
break;
}
if batch.contains_key(&a) || (a != addr && st.translated.contains(&a)) || !executable(a) {
if batch.contains_key(&a) || (a != addr && (st.translated.contains(&a) || skip(a))) || !executable(a) {
continue;
}
let Learned { starts, noreturn, .. } = &mut *st;
// Recovery records `a` as a function start for good, which bounds
// every function recovered after it: a later recovery of the
// function around `a` ends there as at a tail call, with the
// flags dead. A root the table missed at can be the middle of a
// function (a return or a signal's resume through dispatch, a
// computed goto's label, a guessed slot target), so `a` stays a
// start only if something already made it one (a call, the
// image's symbols). The page's JIT, with no images to seed it,
// split libxcb's `_xcb_in_read_block` before a `cmovle` this way.
let before = starts.get(&a).cloned();
let f = func::recover_one(&code, a, starts, noreturn, &opts);
if !before.as_ref().is_some_and(|b| b.strong()) {
match before {
Some(b) => {
starts.insert(a, b);
}
None => {
starts.remove(&a);
}
}
}
if let Err(e) = f.verify().and_then(|_| block_local(&f)) {
if std::env::var_os("GEX_FJIT_STATS").is_some() {
eprintln!("[fjit] {a:#x}: recovered malformed ({e}); left to the fallback");
+4
View File
@@ -580,6 +580,10 @@ impl gex_kernel::ftab::Translator for FjitTranslator {
fn forget(&self, process: u64) {
self.0.forget(process);
}
fn retire(&self, process: u64, ranges: &[(u64, u64)]) {
self.0.retire(process, ranges);
}
}
/// The guest memory for this run (docs/mem64.md §5): `--mem64`/`--mem32`,
+9 -9
View File
@@ -51,21 +51,21 @@ push** (rebased onto upstream wink 0dd84167; nothing pushed):
- replay it with `replay-run.mjs gpu TRACE --query readback=1`;
- bisect the shader variants with `shaders.mjs`;
- then re-apply e204fa1b's two web files.
2. *Grass is still dots* at GL 4.0 after the -0.0 fix (U mp27; the sea is
back). Next: the geometry-shader emulation's output for the grass program
(its source is in the Muck trace; `shaders.mjs`). It may resolve with
item 1 at GL 4.1+, where Unity's 410 source compiles natively.
2. *Grass:* fixed at GL 4.2 by GL 4.1's texture limits with mesa-wink
60202246 (U mpg3: blades, 36.6-38.1 presents/s). gex's pin is 60202246
and `?gllimits=41` is the default since U's pin commit on merge/unity
(`?gllimits=0` turns it off; 43 stays per run, item 1).
3. *Noita's world fps* is bound by its job hand-off, not by GL: the main
thread is parked about 37% waiting on job threads, and KLock is 0.14%
(stream F's area). The page's GL submit is no longer the limit
(CLIENT_ARRAY: 55.5 vs 51.1 world fps on the old path, pewasm 47.2).
4. Remaining E1: GL draw/pointer calls are on the fast path, but the page
gain is below noise.
5. *The 4-slot bind-group repro on the rebased tree* (`$G/urep5.sh`, mods
tinyu) timed out at 1200 s with no output. Rerun it the way trep2.sh ran
the earlier ones (full trace, `--query stream=1`, timeout 1500, no
maxframes) before moving gex's pin; expect 0 validation errors from
frame 1343 on.
5. *The 4-slot bind-group repro on the rebased tree:* done (U,
2026-10-09, `scratch/unity/urep-pin.sh` on edger, run like trep2.sh:
full trace, `--query stream=1`, timeout 1500): mods/tinyu replays all
2,777 frames (3,403 batches) of Noita's trace with 0 validation errors
(5f5572e6 and +6d3a4cfd had 2,668 and 2,720 from frame 1343).
Tooling (unchanged):
+361
View File
@@ -1801,3 +1801,364 @@ Not yet:
applies the browser's flat-layout cuts natively, to test that.
- HK's UnityPlayer maps at 0x11752000 in the page. A unit for that
base is building on gooner (`scratch/unity/units/build-hk-page.log`).
- **Bisecting the HK fjit black screen** (`?fjitlibs=+name,-name`,
731faf46; `scratch/unity/page/hk-bis.sh NAME LIBS` gives the last
readback's mean brightness: the menu ~37-40, the failure 0):
| run | fjitlibs | result |
|---|---|---|
| hb1 | -UnityPlayer.so | black |
| hb2 | -UnityPlayer.so,-libc.so.6 | black |
| hb3 | +none.so (JIT on, no object) | menu |
| hb4 | +libxcb.so.1,+libX11.so.6 | menu (lit 59k from ~210 s; the last readback only black) |
The compositor's lit-pixel count (every 30 s in the log; ~5179 is
the title bar alone, ~59k the menu) separates two things the last
readback's brightness mixed up: hb1, hb2 and hp5 are never lit,
while hb4 draws the menu and only its final frame is dark. So the X
libraries are not the cause; hp1's libxcb crash (`_xcb_in_read_block`
copying `queue_len` = -8 bytes) stays open.
- **These rules leaked:** `?fjitlibs=` was checked only on tier-1
dispatch misses (`serve`), not on `gex.lookup` misses, so JIT code
calling through a pointer or a PLT slot translated any object
(hb1 and hb2 still JIT-compiled UnityPlayer and libc that way).
841b380b checks the rules at every translation and takes
`name@lo-hi` (hex offsets into the mapping, as traps print them).
- Next with the strict rules: hb6 `+libGL.so.1` (the guest GL;
rendering black while presents continue points at the GL stream),
hb7 `-libGL.so.1`. `hk-bis.sh` now prints the lit timeline too.
- Native xbig (`scratch/unity/xbn/run.sh`, big X replies across
many socket reads) passes with `--fjit`, `--fjit --mem64` and
`GEX_CGEN_EXP=WASMFLAT`; the page runs of it (xb1, xb2) never
printed (the old xbig printed only at the end; it now reports
progress).
- **hp6 (no fjit, `elfunits=UnityPlayer.so`) did not use the unit:**
UnityPlayer mapped at 0x10752000, not 0x11752000. The function
JIT's heap spans come from the arena cursor the guest's mappings
use, so its 16 MiB chunk moved every later mapping in hp1. hp6 is a
no-unit run: first frame 108 s, 100 presents 183 s, menu 15.7-16.0
presents/s (hp4: 108 s, 185 s, 15.2-15.6). 841b380b adds
`?pins=path@base+span` (the closure pins, `gex_pin_object`).
- **hp7 (no fjit, the UnityPlayer unit pinned at 0x11752000,
`?cgen=1&elfunits=UnityPlayer.so&pins=UnityPlayer.so@11752000+2674000`):**
first frame 55 s (hp6 108 s), 100 presents at 100 s (hp6 183 s),
the menu at 24.5-27.6 presents/s from ~180 s (hp6 15.7-16.0), lit
from ~150 s; readback `shots/hp7/c017.png` is the real menu. Native
gexc on edger (integ/2026-10-08d gate leg): 30.59 presents/s, menu
ready at 406.5 s. So the page with the unit is about 14% slower
than native at the menu (26.3 vs 30.59) and reaches it sooner.
`hk-page.sh` runs want the pin whenever `elfunits` is given.
- hb6 (`+libGL.so.1`, strict) died at ~133 s: thread 24 in
UnityPlayer (+0x1dcbe60, tier-1) branched to 0x13450100, a mono.wasm
export with no bridge (SIGSEGV, status 139). Not seen in any other
run; hb10 repeats it.
- The page keeps the guest's output (`shots/NAME/out.txt`), and
hk-page.sh passes `-logFile -`, so HK's player log is in it.
- The HK UnityPlayer unit for 0x11752000 (55,439 functions, 165 MiB
wasm, key 81cfd177) is what hp7 ran with.
- **The fjit crash (hb6-hb21, strict `?fjitlibs=`).** Every run that
JIT-compiles libGL dies at 120-250 s, branching to the provider's
return address 0x13450100 (`code_page + 0x100`, depth 0). It shows up
on thread 24 (hb6/13/16/20) or thread 0 (hb21). hb10/17 instead had
thread 0's poll return to a heap address. `+libxcb,+libX11` (hb4) and
`+none.so` (hb3) reach the menu.
- Block 0x12c17e60 is UnityPlayer's `syscall@plt`. The diagnostic's
`.so +X` is relative to the text mapping, so UnityPlayer's base
there is 0x10752000. The caller is Unity's futex lock slow path at
+0xdf1ecd (FUTEX_WAIT_PRIVATE through `syscall()`).
- At the fault rax is still 0xca (futex, never answered), rcx is
after libc syscall()'s `syscall`, and rsp is syscall()'s entry rsp.
So the futex trapped out to wait, and the thread resumed at the
provider's ra instead of the saved pc (0x12ee34f9).
- Every provider guest call in the ring is 0x10d90 deeper than that
sp, every call-out is closed, and "provider guest call open" is
false.
- `provider_try` resets GUEST_CALLS on any exception, not only its
raise tag, and rethrows. So a wasm trap that unwound a provider's
frames would look exactly like this.
- hb21 (`+libGL.so.1` only; since 7e23c82a the rules also keep other
objects' callees out of a module) crashed the same way at 248 s. So
JIT'd libc is not needed for it.
- Diagnostics, f823644f: `[gex] a trap left through N provider guest
call(s)` (from provider_try, with the JS stack) and `... reached the
run loop with N open`. The wild-branch report now also lists the
Worker's last 16 set_resume calls (lib.rs line, pc).
- hb23 and hb24 (`+libGL.so.1,provcheck=1`) crashed the same way.
provcheck said every provider guest call ended at its slot + 8, and
the dump showed the "slot" at 0x57f4830 holding 0x12d7a280: no
`ret` read 0x13450100. hb22 (`+libc.so.6`) instead ran a libc
continuation (+0x8ea87, after `call *0x378(%r12)` into ld.so's
`_dl_catch_error`) with Unity's registers and called a heap address.
- **Cause: a stale resume (fixed in e2f06c87).** The dispatch miss
that recognises a call-out's return (`cgen::returned`) sets the
resume and traps when it cannot leave by returning, which is the
case with JIT bodies on the stack. `#callOut`'s catch returned on
`gex_callout_hit()` without reading the resume. The next time the
thread blocked with nothing else to run (`schedule()` returning 2;
here Unity's futex), the run loop took the stale resume. That was
the provider's ra 0x13450100 (hb21/23/24, the last resume in the
ring) or 0x12e63a87 (hb22). The catch now drops the resume when it
ends its own call-out.
- hb25 (`+libGL.so.1`, the fix): no crash; lit 59-60k from ~150 s;
the last readback (`shots/hb25/c013.png`) is the menu; 15.0
presents/s at 392 s (no unit). hb26 (the full function JIT) and hp8
(`--fjit` with the pinned unit, the target configuration) are
queued (`q16.sh`, `q17.sh`).
- **HK's start-up in the page (hp7, the unit):** first frame 55 s;
"Performing automatic level start" by 122 s; 18 presents/s by 152 s;
the menu is up at about 2.5 min (native gate: menu ready 406.5 s).
- **After the fix (2026-10-08 evening; `hk-bis.sh`, `hk-bisu.sh` for
the unit runs; lit = the compositor's lit pixels, ~59k the menu):**
| run | config | result |
|---|---|---|
| hb26 | full fjit, no unit | never lit, 59.5 presents/s; player log: "The selected window backend is (null)", "Video subsystem has not been initialized", "Desktop is 0 x 0" |
| hb27 | +UnityPlayer.so | menu (lit from ~270 s) |
| hb28 | +libc.so.6 | menu (lit from ~210 s) |
| hb29 | -UnityPlayer.so,-libc.so.6 | never lit; SDL's X11 video init fails as in hb26 |
| hp8 | full fjit + the pinned unit | died at 32 s: p_memcpy OOB from libxcb `_xcb_in_read_block` (+0xf16f), `queue_len` negative (as hp1) |
| hu1 | +libxcb.so.1 + the unit | menu at 393 s, 8.9 presents/s |
| hu2 | -libxcb.so.1 + the unit | **menu, 41.6 presents/s** (first frame 128 s, 100 presents 166 s, 41.1-41.7 from 302 s) |
- hu2 against hp7 (the unit, no fjit): 41.6 vs 26.3 presents/s at
the menu (58% more), and against native gexc's 30.59 (36% more);
but the first frame is later (128 vs 55 s), as the JIT compiles.
- libxcb crashes only with other objects JIT-compiled too, and alone
it is fine (hu1). `queue_len` can only go negative if two threads
run `read_packet` at once, so the suspect is the iolock: libxcb
drops it around `poll()` in `_xcb_conn_wait` and takes it again
after. A wrong resume after the blocking poll (the class of the
stale-resume bug) would skip the re-lock.
- The black screen is in SDL's X11 video init and needs neither
UnityPlayer nor libc JIT-compiled (hb29). 3-minute strict runs,
judged by the player log's "window backend is x11|(null)":
| run | fjitlibs | backend |
|---|---|---|
| hb30 | +libX11,+libxcb | x11 |
| hb31 | +the 12 X extension/aux libraries (Xext, Xrandr, Xi, Xinerama, Xcursor, Xss, Xxf86vm, Xfixes, Xrender, X11-xcb, Xau, Xdmcp) | x11 |
| hb32 | +ld.so, libdl, libpthread, libm, libstdc++, libgcc_s, librt | x11 |
| hb33 | everything but UnityPlayer, libc and all 14 X libraries | x11 |
| hb34 | everything but UnityPlayer, libc, ld.so, libdl, libpthread, libm, libgcc_s, librt | (null) |
So it takes the X libraries together with something outside them.
(A `+` in `?fjitlibs=` must be `%2B` inside a name: hb34 meant to
drop libstdc++ too and did not.) `q24.sh`: X plus libstdc++
(hb35), plus the GL libraries (hb36), plus audio/system (hb37).
- **xbig hung in the page (xb3 without fjit, xb4 with; fixed in
895a3b8c).** xb5 (per-step times): round 0 (300 KB property, 480 KB
image, 50 atoms) done at 1.7 s, then the second property read never
returned. The X server is the host-side wasm Xwayland, and its
`gex_poll` (web/xwayland/gexos.c) set POLLOUT on every descriptor
but returned only for POLLIN: a reply that did not fit the guest's
kernel socket (`SOCK_BUF`, 256 KiB) waited for input that never came,
since the client was waiting for the reply. POLLOUT now comes from
the socket's room (`k_sock_writable`, `hostx.js` `writable`) and
counts. Any X client reading more than 256 KiB at once was exposed
(Unity's XGetImage/property reads, Wine). Rebuilding Xwayland.wasm on
gooner needed xorgproto 2024.1, util-macros, libxshmfence's header,
wayland-protocols and W's host wayland-scanner staged by hand
(`scratch/unity/xwl/`, `PKG_CONFIG_PATH`/`ACLOCAL_PATH`).
- lockpoll (libxcb's unlock/poll/lock on 4 threads, 80,000 entries,
`scratch/unity/page/lockpoll`) passes in the page with and without
fjit (lp1/lp2) and natively, so the hp8 libxcb race is not that
pattern on its own.
- **Muck's grass is fixed by `?gllimits=41` with GL's rebased Mesa
(mods/gpu, mesa-wink gl/perf 60202246):** mpg3 (fjit + the unit):
the context is GL 4.2, no "Shader Unsupported" for the grass, and
`shots/mpg3/c013.png` shows the grass blades and the full 3D menu
scene; 36.6-38.1 presents/s. mpg1/mpg2 (mods/gpu at GL 4.0) were
dots. With the site Mesa (mpg4) the 3D scene is never drawn (lit
5179; "WebGPU validation: Bind group layout ..."), so it needs both.
mp26's black scene was at GL 4.6 (the compute limits too); 4.1's
texture limits alone do not cause it.
- **The black screen bisect, continued (3-minute runs, X = the 14 X
libraries, judged by the player log's window backend):**
| run | fjitlibs | backend |
|---|---|---|
| hb35 | X + libstdc++ | x11 |
| hb36 | X + the GL libraries | x11 |
| hb37 | X + audio/system | (null) |
| hb39 | X + libbsd, libz, liblzma and the other small ones | x11 |
| hb40 | X + audio, dbus, systemd, udev, steam | (null) |
| hb43 | X + dbus, systemd, udev | (null) |
| hb44 | X + libsteam_api | x11 |
| hb45 | X + libdbus-1 | **(null)** |
| hb46 | X + libsystemd | x11 |
| hb47 | X + libudev | x11 |
So the X libraries JIT-compiled with libdbus. `LD_DEBUG` (hb41 fails,
hb42 passes): after `SDL_X11_LoadSymbols` the failing run unloads
the X libraries at once (`X11_CreateDevice` failed) and has opened
one X connection, the passing run two. libdbus does not touch X: it
only changes which functions land in which batch, so this looks like
the same libxcb miscompile as hp8's crash. hs1/hs2 (`?strace=1`, X +
libdbus and X + steam) record the syscalls around the connect.
- **hp8's libxcb crash, bisected inside libxcb** (the unit + fjit;
`name@lo-hi` excludes an offset range of the object's text):
| run | fjitlibs | result |
|---|---|---|
| hu3 | +xcb,+X11 | pass |
| hu4 | +xcb,+libc | pass |
| hu11 | +xcb,+ld.so | pass |
| hu12 | +xcb,+libc,+ld.so | pass |
| hu6 | everything, xcb 0-9f00 excluded | pass |
| hu7 | everything, xcb 9f00-14000 excluded | crash |
| hu8 | everything, only xcb 3120-32a0 (`_xcb_in_read_block`) excluded | pass |
| hu9 | everything, xcb 0-3120 (the PLT, `xcb_connect_to_fd`'s callers) excluded | pass |
| hu13 | everything, `watch=` on `queue_len` (c+0x1074) | queue_len stays 0 to the crash |
It is deterministic (~30-34 s, libxcb +0x316f). At the crash the
registers hold `memmove(queue, queue+8, -8)` with done = 8 while
`queue_len` is 0: the `min(queue_len, len)` select came out wrong in
the page's code, not a race (the iolock theory above is wrong;
lockpoll agrees). The offline harness (`scratch/unity/fjitrepro`:
libxcb alone, and with libc and the glibc_mem providers, eager and
lazy binding) emits a correct select, so it depends on the page's
batch: PLT stubs inlined into it (`gex-fjit` inline.rs) or a callee
summary. `?fjitlibs=dumpmod=ADDR` (fjit.rs) dumps the module whose
batch root is ADDR as base64 `[fjitdump]` lines; hu14
(`dumpmod=1a79d120`) captures the crashing module.
- **Start-up in the page (unit + fjit, `-libxcb.so.1`):** ht1
(`?attachtrace=1`): thread Workers attach ~0.1 s after their post, and
posts are ~0.25 s apart, so spawning threads costs little. ht2 (a
Chrome profile of the main Worker, 70-120 s): 64% (33 of 52 s) in
`fjit::translate`: recovery 27.6 s (discover/recover_one 16.8, lift
10), analyze 6.4, jump tables 4.4, emit 5.5, build_runtime 5.7, and
7 s in `lib_wanted`/`object_file_off` (only with `?fjitlibs=` rules).
ht3 (`byobj=1`, counts by object up to the menu,
translations/functions/wasm): UnityPlayer.so 750 / 3,667 / 36 MB
although the unit serves it, then libasound 7.9 MB, libc 4.7 MB,
libcrypto 3.5 MB, libX11 2.1 MB; first frame 125 s. A miss in
UnityPlayer (a function the unit lacks) pulled up to 63 of the unit's
own functions into its batch. 1cf4e76f keeps callees the unit serves
out of the batch (`cgen::index_of`).
- **Root cause of both: the JIT kept a dlclose'd library's function
starts (fixed in 763a1538).** `?fjitlibs=dumpmod=` lines must be
under ~4,000 characters (the page console cuts them; ae44c162 uses
3,000; `dumpmod=OBJ@OFF` names a function by object and text-mapping
offset). hu16's module has `_xcb_in_read_block` ending after its `cmp`
at +0x2d with a tail call to +0x30, the `cmovle`, and no flags stored
before it: +0x30 was a strong start, so recovery ended the function
there as at a tail call, with the flags dead, and the `cmovle` read
stale flags: `min(queue_len, len)` gave `len` (hp8's memmove of -8
bytes). hs1/hs3 (X + libdbus) show the first X connection shut right
after the setup `writev`, with no read: `read_setup`'s
`_xcb_in_read_block` took the reply from an empty queue (the black
screen). hs4 (`dumpmod=libxcb.so.1@3150`) found no batch holding
+0xf150 as a function, and hs5 (`scanconst=0x1310d150`, which reports
every module mentioning an address) found it in libdbus's modules:
SDL dlopens libdbus, dlcloses it, and libxcb is mapped over the same
range, while the JIT's learned starts (and its set of translated
functions) are keyed by address and outlived the unmap. That is why
hb45 needed libdbus JIT-compiled. 763a1538 keys the learned state by
an epoch that `retire_unmapped_code` moves on (`fjit::on_retire`,
logged as "code unmapped ... learning afresh"; hb50 had 10 epochs by
151 s, six from one libasound plugin range). hb50 (full fjit, no
unit): "window backend is x11", where hb26/hb48/hb49 had "(null)".
- Also (a12da54a, 6bc61569): a miss strictly inside an `.eh_frame` FDE
(vfprintf's computed-goto labels, the code after `clone`'s syscall,
return addresses reached through dispatch) is left to the fallback,
since `recover_one` records every root as a start for good and would
split the enclosing function the same way; `.plt`'s single FDE is
exempt (its stubs are entries). Natively the same holes exist (the
learned state is per process, kept across dlclose; mid-function
roots become starts), masked by the images' seeded starts.
- 1cf4e76f's first form used `cgen::index_of`, which also answers for
the JIT's own entries, and `executable` is what instruction fetch
asks of every byte: hu15 decoded the `cmovle` at 0x1a79d150 (a JIT
entry by then) as illegal (SIGILL, status 132). 5ec43e82 counts unit
entries only (`cgen::unit_serves`).
- **Results after the fixes (2026-10-09, HK in the page on edger;
presents/s at the menu, from the last minutes of 7-minute runs):**
| run | config | shim | first frame | 100 presents | menu presents/s |
|---|---|---|---|---|---|
| hp7 | unit, no fjit | before | 55 s | - | 26.3 |
| hu2 | unit + fjit `-libxcb` | before | 128 s | 166 s | 41.6 |
| ht3 | unit + fjit `-libxcb` | before (byobj) | 125 s | - | - |
| ht5 | unit + fjit `-libxcb` | 5ec43e82 (unit callees out of batches) | 113 s | - | 40.2 |
| ht6 | unit + fjit `-libxcb` | a12da54a (+ mid-function misses to the fallback) | 78 s | 96 s | 39.6-39.7 |
| hp9/hp10 | full fjit + unit | a12da54a/6bc61569 | - | - | crash at 23.6 s (memmove after writev: the stale-start split) |
| hp11 | full fjit + unit | 763a1538 (epochs) | 131 s | 161 s | 40.7-42.0 |
| hp12 | full fjit + unit | 58e95157 (epochs only where translated) | 128 s | 158 s | 42.4-42.7 |
| **hp13** | **full fjit + unit** | **977a2236 (current JIT callees out of batches too)** | **87 s** | **119 s** | **39.6-39.9** |
| hb51 | full fjit, no unit | 58e95157 | 222 s | 270 s | 33.9 at 392 s, rising (x11 backend; was black) |
| hb52 | full fjit, no unit | merge/unity-rekey d146c248 | - | - | SIGILL at start-up (status 132) |
| hb53 | full fjit, no unit | merge/unity-rekey 64efd78e | 178 s | 225 s | 35.7 at 392 s (x11 backend) |
Native gexc's gate (unit, no fjit): menu at 406.5 s, 30.59
presents/s. hp13 against hp7: 52% more presents/s (39.9 vs 26.3),
first frame 32 s later (87 vs 55 s). hp13's menu rate is 6% below
hp12's (39.9 vs 42.4): callees served by older modules are called
through the table instead of directly, which may cost that much, but
the menu rate ranges 39.7-42.7 across these runs. By the menu hp13's
JIT has made libasound 20 MiB of wasm (947 functions), libc 4.3 MiB,
libudev 3.2 MiB, libcrypto 2.9 MiB; UnityPlayer is out of the top 12.
- **merge/unity-rekey (64efd78e, one commit on 4ed5cad3, for its own
round):** the native JIT's two holes fixed in gex-xlate
(`Jit::retire(process, ranges)` from both hosts' on_retire; a miss's
root stays a start only if it was a strong one already) and fjit.rs out
of GEX_CGEN_RECOVERY_INPUTS (key c6014ffc... with and without an
fjit.rs edit, 855acf6a... after a func.rs edit). hb52 (d146c248) died
with SIGILL: with misses no longer starts, recovery walked over a JIT
entry inside the function, and the page's "served" test was part of
`executable`, which fetch asks of every byte, so the bytes decoded as
illegal. 64efd78e passes it as `skip` (`Jit::translate_skipping`), the
batch bound only. hb53: menu, x11, first frame 178 s, 35.7 presents/s
(hb51 on the epoch shim: 222 s, 33.9). Native: tests/fjit-remap (the
Oct 8 gexc gets 99,800 of 200,000 wrong under --fjit), fjit-procs,
x86-programs (82 ok), embed PASS, gex-xlate tests. The page's unit cache
(scratch/unity/units/cgen-page) must be rebuilt after it lands.
- **Mesa pin (1d5b26d9): mesa-wink 60202246 and `gllimits=41` by
default.** Replays gpu vs this build: Noita 92f095c5, HK 9eece5c5, 0
rejects and 0 validation errors each; GL's 4-slot repro (tinyu) 2,777
frames with 0 validation errors; browser-gui --gl x86 and x64 12 ok
each; HK hp14 (merge/unity shim, this Mesa) menu at 41.8 presents/s.
Muck with the default and no `?gllimits` (mk2, the seed shim, no unit):
"OpenGL 4.2" context, no "Shader Unsupported", grass blades in
`shots/mk2/c013.png`, 36.9 presents/s at 393 s.
- **Muck dies with SIGILL at ~25 s on merge/unity's shim (977a2236,
4ed5cad3)**, with the unit (mpg5, mk1) and without (mk4). It does not
with 64efd78e (mk3, 4 minutes, no unit) or 1bf5ba09 (mk2). The page asked
"served by the unit or the JIT" of instruction fetch (part of
`executable`); with epochs a JIT entry the current epoch has no start
for is read as non-code inside the function being recovered and decoded
as illegal. 64efd78e passes it as `skip` (batch bound only).
- **Start-up (decision 3).** hp14's main-Worker profile, 10-90 s (unit +
fjit): 61% (49.8 of 81 s) in `fjit::translate`: recover_one 27.2 s
(jump tables 16.9, discover 9.2, lift 6.9), summaries 17.7, build_runtime
8.8, emit 8.4, prepare 5.5; the per-byte `executable` 5.9 (2.3 in
elfprov::provided's locks). Audio: the page has no audio device; FMOD's
output init fails ("Unknown PCM default", tried twice), after pulse
fails to connect. libasound's 20 MiB was mostly copies: the page's JIT
had no function starts but those calls name, so a tail jump absorbed the
function it reached, and libasound's exported wrappers each carried one.
Offline (fjitrepro, all 1,698 exports): 57.2 MiB / 16.4 s unseeded, 9.7
MiB / 7.1 s seeded. ALSA with no config (hp15, ALSA_CONFIG_PATH=/dev/null)
cut libasound to 11.2 MiB but not the first frame (112 s against hp14's
110 s, both profiled), so it is not the path to cut.
merge/unity-seed 1bf5ba09 (on 64efd78e) seeds every mapped object's
starts (`Jit::add_image`) before a translation and takes the fetch
checks once per translation: no unit, hb53 (64efd78e) 178 s first
frame, 225 s 100 presents, 35.7 presents/s; hb54 (seeding) 121 s, 171 s,
39.1; hb55 (both) 121 s, 166 s, 40.1; libasound 26.0 -> 3.7 MiB.
The HK unit re-keyed for 64efd78e (key 7bd87731, byte-identical c.wasm,
1,941 s on gooner) is on edger in `units/cgen-page-rk`; Muck's is
rebuilt too (4e58f35b, 884 s), also in `cgen-page-rk`.
With the unit (target configuration, seed shim 1bf5ba09):
- hp16 (HK): first frame 56 s (hp13 87 s), 100 presents 85 s (119 s),
41.5 presents/s at 392 s (39.9).
- hp17 (HK, profiled 10-90 s): `fjit::translate` 20.7 s of 81 s (25.5%;
hp14 49.8 s, 61.5%); the main Worker now waits on its threads 31.6 s.
Profiled runs' first frame (110 s both) is held by the profile.
- mk5 (Muck): first frame 82 s, 100 presents 95 s, 34.0 presents/s at
392 s, OpenGL 4.2, no "Shader Unsupported", grass blades
(`shots/mk5/c013.png`).
Branches: merge/unity-rekey 64efd78e (own round), merge/unity-seed
1bf5ba09 on it (worktree wt/unity-rk); merge/unity 1d5b26d9 (pin).
Natively the JIT seeds only the main image too (gexc's
FjitTranslator::image); a Translator hook per mapped object would do
the same there.
+44 -2
View File
@@ -806,6 +806,8 @@ export class Gex {
static wasmDlls = '';
/** `name,...` (the page's `?elfunits=`): shared objects that get function-tier units. */
static elfUnits = '';
/** `+name,-name` (the page's `?fjitlibs=`): objects the function JIT is limited to or kept from. */
static fjitLibs = '';
static #whySlot = 0;
#bytes() {
@@ -2574,6 +2576,13 @@ export class Gex {
* providers (cgen/libprov.rs). */
#libprovConfig() {
const e = this.#shim.exports;
if (Gex.fjitLibs && e.gex_fjit_libs) {
const b = new TextEncoder().encode(Gex.fjitLibs);
const p = e.gex_alloc(Math.max(8, b.length)) >>> 0;
this.#bytes().set(b, p);
e.gex_fjit_libs(p, b.length);
e.gex_free?.(p, Math.max(8, b.length));
}
if (Gex.elfUnits && e.gex_cgen_elf_units) {
const b = new TextEncoder().encode(Gex.elfUnits);
const p = e.gex_alloc(Math.max(8, b.length)) >>> 0;
@@ -2821,6 +2830,8 @@ export class Gex {
* exception, so code the provider publishes at run time (its JIT, which imports it)
* can catch it with `try_table`, and `provider_try` takes it when nothing did. */
#provTag = null;
/** Traps seen unwinding provider guest calls (reported up to 16). */
#provLeaks = 0;
#providerTag() {
this.#provTag ??= new WebAssembly.Tag({ parameters: ['i32'] });
return this.#provTag;
@@ -2841,8 +2852,16 @@ export class Gex {
ftab.get(f)(arg);
return 0;
} catch (err) {
const open = e.gex_libprov_guest_calls();
e.gex_libprov_guest_calls_set(calls);
if (err instanceof WebAssembly.Exception && err.is(tag)) return err.getArg(tag, 0);
// Anything else that unwinds a provider's guest calls abandons
// frames the guest will return into (their return addresses stay
// on its stack): say which trap, where it resumes, and from where.
if (open > calls && (this.#provLeaks = (this.#provLeaks ?? 0) + 1) <= 16) {
console.error(`[gex] a trap left through ${open - calls} provider guest call(s): ${err?.message ?? err}; ` +
`resume ${BigInt.asUintN(64, BigInt(e.gex_resume_pc())).toString(16)}\n${err?.stack ?? ''}`);
}
throw err;
}
},
@@ -3675,6 +3694,15 @@ export class Gex {
// alone it slid into the guest's stack and overwrote saved registers.
this.#shim.exports.__stack_pointer.value = addrVal(this.#workerStack());
const e = this.#shim.exports;
// A trap that reached the run loop with a provider's guest call
// still open abandoned the provider's frames (see provider_try).
if ((e.gex_libprov_guest_calls?.() ?? 0) > 0) {
if ((this.#provLeaks = (this.#provLeaks ?? 0) + 1) <= 16) {
console.error(`[gex] a trap reached the run loop with ${e.gex_libprov_guest_calls()} provider guest call(s) open: ${err?.message ?? err}; ` +
`resume ${BigInt.asUintN(64, BigInt(e.gex_resume_pc())).toString(16)}\n${err?.stack ?? ''}`);
}
e.gex_libprov_guest_calls_set(0);
}
// A slot the shim wants written before the guest goes back to it: its
// module is instantiated with passive segments, so the code is present
// and simply not in the table. Filling here costs one segment; at
@@ -4342,7 +4370,21 @@ export class Gex {
e.__stack_pointer.value = sp0;
const sig = this.#calloutSignal;
this.#calloutSignal = null;
if (sig?.kind === 'return') return sig.i === me ? 1 : 0;
// A call-out ended here was the trap's whole business: the resume
// the miss path set at its return (`cgen::returned`) is spent, and
// left in place it is where the run loop next goes -- the next
// time this thread blocks with nothing else to run, it "resumed"
// at the provider's return address (HK under ?fjit, docs §12 34).
// (Only this one's: an outer call-out's hit returns through the
// frames between, which leave as before.)
const ended = (v) => {
if (v === 1) {
e.gex_resume_pc();
e.gex_resume_ctx();
}
return v;
};
if (sig?.kind === 'return') return ended(sig.i === me ? 1 : 0);
if (sig?.kind === 'bypass') {
if (!sig.f) throw new Error('gex: a watched dispatch entry with nothing to run');
next = sig.f;
@@ -4359,7 +4401,7 @@ export class Gex {
throw err;
}
const hit = e.gex_callout_hit();
if (hit >= 0) return hit === me ? 1 : 0;
if (hit >= 0) return ended(hit === me ? 1 : 0);
// gex_nr::NATIVE_LEFT (gexrt saw the guest leave a native caller's
// frames): a native caller's call-out leaves, at the context's PC
// (the entry the guest makes again); anything else carries on.
+1
View File
@@ -416,6 +416,7 @@ async function handle(msg) {
if (msg.mem64) globalThis.GEX_MEM64 = 1;
if (msg.wasmDlls) Gex.wasmDlls = msg.wasmDlls;
if (msg.elfUnits) Gex.elfUnits = msg.elfUnits;
if (msg.fjitLibs) Gex.fjitLibs = msg.fjitLibs;
if (msg.calloutTrace) globalThis.GEX_CALLOUT_TRACE = 1;
if (msg.lazyThread) globalThis.GEX_LAZY_THREAD = 1;
// `kernelStack` is this Worker's slice of the kernel's stack array. Absent for
+40
View File
@@ -1065,6 +1065,20 @@ pub(crate) fn index_of(addr: u64) -> Option<u32> {
ftab(|m| m.get(addr).map(|f| f.idx))
}
/// Whether a unit built ahead of time (or a provider) serves the function at
/// `addr`: not the JIT's own functions, whose resume points are published
/// at addresses inside functions.
pub(crate) fn unit_serves(addr: u64) -> bool {
ftab(|m| m.get(addr).is_some_and(|f| m.install(f.install).tier == Tier::Unit))
}
/// Whether a current JIT function serves `addr` (its pages unchanged since).
/// After the JIT forgets (`fjit::on_retire`) this is what still knows
/// which functions it has made.
pub(crate) fn jit_serves(addr: u64) -> bool {
ftab(|m| m.get(addr).is_some_and(|f| m.install(f.install).tier == Tier::Jit && crate::fjit::current(f.install)))
}
/// How many units are published: a Worker installs up to this many.
#[no_mangle]
pub extern "C" fn gex_cgen_count() -> u32 {
@@ -1298,9 +1312,27 @@ pub extern "C" fn gex_ftab_fallback(ctx: i32, _depth: i32, _need: i32, ret: i64)
c.len() - 1
});
CALLOUT_N.fetch_add(1, Ordering::Relaxed);
// Opened (result -1): one that never closes was abandoned by a throw.
CALLOUT_RING.with(|x| {
let mut x = x.borrow_mut();
if x.len() >= 24 {
x.remove(0);
}
x.push((t, ret as u64, want, depth as u32, -1, sp));
});
// SAFETY: a host import taking scalars; nothing here holds a lock.
let r = unsafe { call_out(ctx, t as i64, ret, want as i64) };
CALLOUTS.with(|c| c.borrow_mut().truncate(depth));
{
let after = GuestMem::new(crate::guest_bytes()).read_u64(c + gex_abi::ctx::x(sp_index()) as u64).unwrap_or(0);
CALLOUT_RING.with(|x| {
let mut x = x.borrow_mut();
if x.len() >= 24 {
x.remove(0);
}
x.push((t, ret as u64, want, depth as u32, r, after));
});
}
if r == gex_abi::ftab::RET_LEFT {
CALLOUT_LEFT.fetch_add(1, Ordering::Relaxed);
if libprov::provider_return(ret as u64) {
@@ -1424,6 +1456,14 @@ thread_local! {
static LOOKUP_RING: std::cell::RefCell<Vec<(u64, u32)>> = const { std::cell::RefCell::new(Vec::new()) };
/// The last stateful-provider guest calls that reached the fallback: (target, return, sp).
static PROV_RING: std::cell::RefCell<Vec<(u64, u64, u64)>> = const { std::cell::RefCell::new(Vec::new()) };
/// This Worker's last finished call-outs: (target, return, sp wanted,
/// depth, result, sp after), for the wild-branch diagnostic.
static CALLOUT_RING: std::cell::RefCell<Vec<(u64, u64, u64, u32, i32, u64)>> = const { std::cell::RefCell::new(Vec::new()) };
}
/// [`CALLOUT_RING`], for a diagnostic.
pub(crate) fn callouts_recent() -> Vec<(u64, u64, u64, u32, i32, u64)> {
CALLOUT_RING.with(|r| r.borrow().clone())
}
/// Whether a stateful provider's guest call is open on this Worker: the guest
+8
View File
@@ -49,6 +49,14 @@ pub extern "C" fn gex_elfprov_off() {
OFF.store(true, Ordering::Relaxed);
}
/// Every address `provided` holds, at once (the function JIT asks of each
/// byte it fetches; three locks a byte cost HK's start-up 2.3 s, hp14).
pub(crate) fn provided_set() -> std::collections::HashSet<u64> {
let mut s: std::collections::HashSet<u64> = lock(&BOUND).iter().flat_map(|b| b.addrs.iter().copied()).collect();
s.extend(super::libprov::bound_addrs());
s
}
/// Whether an ELF provider holds `addr` (the function JIT leaves it alone).
pub(crate) fn provided(addr: u64) -> bool {
lock(&BOUND).iter().any(|b| b.addrs.contains(&addr)) || super::libprov::bound(addr)
+52
View File
@@ -76,6 +76,16 @@ pub(crate) fn replaced(addr: u64) -> bool {
lock(&REPLACED).iter().any(|&(lo, hi)| addr >= lo && addr < hi)
}
/// The addresses replaced libraries' providers are bound at.
pub(crate) fn bound_addrs() -> Vec<u64> {
lock(&LIBS).iter().flat_map(|l| l.pairs.iter().map(|p| p.0)).collect()
}
/// The ranges of replaced libraries' own text.
pub(crate) fn replaced_ranges() -> Vec<(u64, u64)> {
lock(&REPLACED).clone()
}
/// Whether `addr` is one a replaced library's provider is bound at (its
/// pairs are sorted by address).
pub(crate) fn bound(addr: u64) -> bool {
@@ -587,10 +597,51 @@ thread_local! {
static GUEST_CALLS: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
}
/// `?fjitlibs=provcheck=1`: [`gex_libprov_guest_call`] checks that each
/// provider guest call ends with the guest's stack just past the return
/// address the call pushed (its SP when the call opens), and says when not.
pub(crate) static PROV_CHECK: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
thread_local! {
/// The open guest calls' return-address slots, for [`PROV_CHECK`].
static PROV_SLOTS: std::cell::RefCell<Vec<u64>> = const { std::cell::RefCell::new(Vec::new()) };
}
/// `gex.provider_guest_call(delta)`.
#[no_mangle]
pub extern "C" fn gex_libprov_guest_call(delta: i32) {
GUEST_CALLS.with(|c| c.set(c.get() + delta));
if PROV_CHECK.load(Ordering::Relaxed) {
prov_check(delta);
}
}
fn prov_check(delta: i32) {
let ctx = crate::with_host(|h| h.threads[crate::cur()].kstate.ctx_addr);
let gm = crate::GuestMem::new(crate::guest_bytes());
let reg = |o: usize| gm.read_u64(ctx + o as u64).unwrap_or(0);
let sp = reg(gex_abi::ctx::x(4));
if delta > 0 {
PROV_SLOTS.with(|s| s.borrow_mut().push(sp));
return;
}
let Some(slot) = PROV_SLOTS.with(|s| s.borrow_mut().pop()) else { return };
if sp == slot + 8 {
return;
}
static SAID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
if SAID.fetch_add(1, Ordering::Relaxed) >= 32 {
return;
}
crate::warn_line(&format!(
"[gex] provcheck: a provider guest call ended with sp {sp:#x}, but its return address was at {slot:#x} ([slot] now {:#x}); \
pc {:#x}, last target {:#x}, {} more open; last call-outs {:x?}\n",
gm.read_u64(slot).unwrap_or(0),
reg(gex_abi::ctx::PC),
reg(gex_abi::ctx::LAST_TARGET),
PROV_SLOTS.with(|s| s.borrow().len()),
crate::cgen::callouts_recent().iter().rev().take(6).collect::<Vec<_>>(),
));
}
/// The count, which `provider_try` restores when a raise unwinds guest calls.
@@ -602,6 +653,7 @@ pub extern "C" fn gex_libprov_guest_calls() -> i32 {
#[no_mangle]
pub extern "C" fn gex_libprov_guest_calls_set(v: i32) {
GUEST_CALLS.with(|c| c.set(v));
PROV_SLOTS.with(|s| s.borrow_mut().truncate(v.max(0) as usize));
}
/// Whether a provider's guest call is open on this Worker.
+423 -5
View File
@@ -64,6 +64,111 @@ static CHECKS: Mutex<Option<HashMap<u32, Vec<(u64, u32)>>>> = Mutex::new(None);
static TIER1_ONLY: Mutex<Option<HashSet<u64>>> = Mutex::new(None);
static TRANSLATED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// Translations, functions and wasm bytes by object (`?fjitlibs=byobj=1`
/// prints them every 256 translations): what the JIT spends start-up on.
static BY_OBJECT: Mutex<Option<std::collections::BTreeMap<String, (u64, u64, u64)>>> = Mutex::new(None);
static BY_OBJECT_ON: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// `?fjitlibs=dumpmod=ADDR`: print (base64, in 3,000-character lines: the page console cuts
/// a line at about 4,000) every JIT module
/// whose functions include guest address ADDR, with its root, to rebuild the
/// page's translation offline (scratch/unity/fjitrepro).
static DUMP_MOD: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static DUMP_OBJ: Mutex<Option<(String, u64)>> = Mutex::new(None);
/// `?fjitlibs=scanconst=ADDR`: say which translations mention guest address
/// ADDR as an `i64.const` (a call or jump to it, or a pc it stores), and
/// every translation asked for at ADDR: who made a mid-function address a
/// function start.
static SCAN_CONST: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
fn scan_const(root: u64, funcs: &[u64], wasm: &[u8]) {
let want = SCAN_CONST.load(Ordering::Relaxed);
if want == 0 {
return;
}
// `i64.const` (0x42) and its signed LEB128.
let mut pat = vec![0x42u8];
let mut v = want as i64;
loop {
let b = (v & 0x7f) as u8;
v >>= 7;
let done = (v == 0 && b & 0x40 == 0) || (v == -1 && b & 0x40 != 0);
pat.push(if done { b } else { b | 0x80 });
if done {
break;
}
}
let hits = wasm.windows(pat.len()).filter(|w| *w == &pat[..]).count();
if hits > 0 {
let list: Vec<String> = funcs.iter().take(24).map(|a| format!("{a:x}")).collect();
crate::warn_line(&format!(
"[fjitscan] {want:#x} x{hits} in the module of root {root:#x} ({}), {} functions: {}\n",
crate::object_off_str(root),
funcs.len(),
list.join(",")
));
}
}
fn dump_module(root: u64, funcs: &[u64], wasm: &[u8]) {
let mut want = DUMP_MOD.load(Ordering::Relaxed);
if want == 0 {
let obj = lock(&DUMP_OBJ).clone();
if let Some((name, off)) = obj {
if let Some(&a) = funcs.iter().find(|&&a| crate::object_file_off(a) == (name.clone(), off)) {
DUMP_MOD.store(a, Ordering::Relaxed);
*lock(&DUMP_OBJ) = None;
crate::warn_line(&format!("[fjitdump] {name}@{off:x} is {a:#x}\n"));
want = a;
}
}
}
if want == 0 || !funcs.contains(&want) {
return;
}
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut b = String::with_capacity(wasm.len() * 4 / 3 + 4);
for c in wasm.chunks(3) {
let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32;
for k in 0..4 {
if k <= c.len() {
b.push(T[(n >> (18 - 6 * k)) as usize & 63] as char);
} else {
b.push('=');
}
}
}
const W: usize = 3000;
let lines = b.len().div_ceil(W);
crate::warn_line(&format!("[fjitdump] root {root:#x} funcs {} bytes {} lines {lines}\n", funcs.len(), wasm.len()));
for (i, fs) in funcs.chunks(32).enumerate() {
let list: Vec<String> = fs.iter().map(|a| format!("{a:x}")).collect();
crate::warn_line(&format!("[fjitdump] {root:x} funcs {i}: {}\n", list.join(",")));
}
for (i, ch) in b.as_bytes().chunks(W).enumerate() {
crate::warn_line(&format!("[fjitdump] {root:x} {i} {}\n", std::str::from_utf8(ch).unwrap_or("")));
}
}
fn note_object(addr: u64, funcs: usize, bytes: usize) {
if !BY_OBJECT_ON.load(Ordering::Relaxed) {
return;
}
let (name, _) = crate::object_file_off(addr);
let mut g = lock(&BY_OBJECT);
let m = g.get_or_insert_with(Default::default);
let e = m.entry(name).or_default();
e.0 += 1;
e.1 += funcs as u64;
e.2 += bytes as u64;
let total: u64 = m.values().map(|e| e.0).sum();
if total % 256 == 0 {
let mut v: Vec<_> = m.iter().collect();
v.sort_by(|a, b| b.1 .2.cmp(&a.1 .2));
let list: Vec<String> = v.iter().take(12).map(|(n, e)| format!("{n} {}/{}/{}K", e.0, e.1, e.2 / 1024)).collect();
crate::warn_line(&format!("[fjit] {total} translations by object (translations/functions/wasm): {}\n", list.join(", ")));
}
}
static DECLINED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
@@ -133,10 +238,167 @@ fn page_gen(page: u64, fresh: &mut Vec<(u64, u32)>) -> u32 {
})
}
/// Each object's `.eh_frame` function ranges (unbiased, sorted by start)
/// and `PT_LOAD` headers (file offset, vaddr, file size), by file path;
/// empty when it has none or cannot be read.
type ObjFdes = (Vec<(u64, u64)>, Vec<(u64, u64, u64)>);
static FDES: Mutex<Option<std::collections::HashMap<String, std::sync::Arc<ObjFdes>>>> = Mutex::new(None);
static MID_DECLINED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// An ELF's `PT_LOAD` headers: (file offset, vaddr, file size).
fn loads(elf: &[u8]) -> Vec<(u64, u64, u64)> {
let mut out = Vec::new();
let Some(&class) = elf.get(4) else { return out };
let wide = class == 2;
let rd = |o: usize, n: usize| -> Option<u64> {
let b = elf.get(o..o + n)?;
Some(b.iter().rev().fold(0u64, |a, &x| a << 8 | x as u64))
};
let hdr = if wide { (rd(0x20, 8), rd(0x36, 2), rd(0x38, 2)) } else { (rd(0x1c, 4), rd(0x2a, 2), rd(0x2c, 2)) };
let (Some(phoff), Some(phentsize), Some(phnum)) = hdr else { return out };
for i in 0..phnum {
let ph = (phoff + i * phentsize) as usize;
if rd(ph, 4) != Some(1) {
continue;
}
let f = if wide { (rd(ph + 8, 8), rd(ph + 16, 8), rd(ph + 32, 8)) } else { (rd(ph + 4, 4), rd(ph + 8, 4), rd(ph + 16, 4)) };
if let (Some(o), Some(v), Some(n)) = f {
out.push((o, v, n));
}
}
out
}
/// Whether `addr` lies strictly inside a function of its object, by the
/// object's `.eh_frame`: a miss there is a resume into the middle of a
/// function (a signal's return, a block boundary of the fallback's), not an
/// entry. Translated as a root, `recover_one` records it as a function start
/// for good, and every later translation of the function around it ends at
/// it as at a tail call, with the flags dead across it: libxcb's
/// `_xcb_in_read_block` lost its `cmp` before the `cmovle` at +0x30 and took
/// `min(queue_len, len)` from stale flags (hp8, hu16: memmove of -8 bytes),
/// and SDL's X11 connect failed the same way (hb26, hb45). The fallback runs
/// it instead; the function's real entry still gets translated.
fn mid_function(addr: u64) -> bool {
let Some((path, off, start)) = crate::with_host(|h| {
h.kernel.mem.vmas().iter().find(|v| addr >= v.start && addr < v.end).and_then(|v| v.code.clone().map(|(p, o)| (p, o, v.start)))
}) else {
return false;
};
let cached = lock(&FDES).get_or_insert_with(Default::default).get(&path).cloned();
let obj = match cached {
Some(o) => o,
None => {
let bytes = crate::with_host(|h| h.kernel.read_file(&path)).unwrap_or_default();
// Not the PLT's: one FDE covers all of `.plt`, whose stubs are
// each an entry (hp9 left them to the fallback, and died).
let plt: Vec<(u64, u64)> = gex_xlate::ehframe::sections(&bytes)
.into_iter()
.filter(|s| s.name.starts_with(".plt"))
.map(|s| (s.addr, s.addr + s.size))
.collect();
let mut f: Vec<(u64, u64)> = gex_xlate::ehframe::fdes(&bytes, 0)
.into_iter()
.filter(|f| !plt.iter().any(|&(lo, hi)| f.start >= lo && f.start < hi))
.map(|f| (f.start, f.end))
.collect();
f.sort_unstable();
let o = std::sync::Arc::new((f, loads(&bytes)));
lock(&FDES).get_or_insert_with(Default::default).insert(path.clone(), o.clone());
o
}
};
let (fdes, loads) = &*obj;
// The vma's file offset gives its vaddr in the file, hence the bias.
let Some(base) = loads.iter().find(|&&(o, _, n)| off >= o && off < o + n.max(1)).map(|&(o, v, _)| v + (off - o)) else {
return false;
};
let x = addr - start + base;
let i = fdes.partition_point(|&(s, _)| s <= x);
let mid = i > 0 && fdes[i - 1].0 < x && x < fdes[i - 1].1;
if mid {
let n = MID_DECLINED.fetch_add(1, Ordering::Relaxed);
if n < 32 || (n + 1).is_power_of_two() {
let name = path.rsplit('/').next().unwrap_or(&path);
crate::warn_line(&format!("[fjit] {addr:#x} ({name} +{x:#x}) is inside a function: left to the fallback ({} so far)\n", n + 1));
}
}
mid
}
/// Code in `ranges` was unmapped or reprotected (`retire_unmapped_code`):
/// what the JIT learned there goes (`Jit::retire`). HK's SDL loads libdbus,
/// unloads it, and maps libxcb over the same range; with libdbus's starts
/// kept, libxcb's `_xcb_in_read_block` ended at a libdbus function's address
/// (+0x30, the `cmovle` of `min(queue_len, len)`) as at a tail call, flags
/// dead, and read the setup reply out of an empty queue: X11 video init
/// failed, the black screen (hb26, hb45; `scanconst=` in hs5 showed the
/// start was libdbus's), and with the unit, a memmove of -8 bytes (hp8).
pub(crate) fn on_retire(ranges: &[(u64, u64)]) {
if let Some(jit) = JIT.get() {
jit.retire(0, ranges);
}
lock(&SEEDED).retain(|&(_, lo, hi)| !ranges.iter().any(|&(s, e)| s < hi && lo < e));
}
/// The objects whose function starts the JIT has (`seed_mapped`): path and
/// the address range of their `PT_LOAD`s, dropped when code there retires.
static SEEDED: Mutex<Vec<(String, u64, u64)>> = Mutex::new(Vec::new());
/// Give the JIT the function starts of every mapped object it lacks (its
/// symbols, `.eh_frame`, init arrays and relocations, `Jit::add_image`),
/// as natively ahead of time. Without them recovery knows only the starts
/// calls name, and follows a tail jump into the function it reaches: each
/// of libasound's exported wrappers carried a copy of the function it
/// jumps to. Offline, translating each of libasound's 1,698 exports in
/// turn made 57.2 MiB of wasm in 16.4 s without its starts and 9.7 MiB in
/// 7.1 s with them (one function per module: 49.7 MiB and 14.1 s against
/// 3.2 MiB and 1.7 s); HK's menu had 20 MiB of libasound wasm (hp13).
fn seed_mapped(jit: &gex_xlate::fjit::Jit) {
let maps: Vec<(u64, String, u64)> = crate::with_host(|h| {
use gex_kernel::mem::prot;
h.kernel.mem.vmas().iter().filter(|v| v.prot & prot::EXEC != 0).filter_map(|v| v.code.clone().map(|(p, o)| (v.start, p, o))).collect()
});
for (start, path, off) in maps {
if lock(&SEEDED).iter().any(|(p, lo, hi)| *p == path && start >= *lo && start < *hi) {
continue;
}
let bytes = crate::with_host(|h| h.kernel.read_file(&path)).unwrap_or_default();
let lds = loads(&bytes);
// The vma's file offset gives its vaddr in the file, hence the bias.
let Some(bias) = lds.iter().find(|&&(o, _, n)| off >= o && off < o + n.max(1)).map(|&(o, v, _)| start.wrapping_sub(v + (off - o))) else {
lock(&SEEDED).push((path, start, start + 1));
continue;
};
let lo = lds.iter().map(|l| l.1).min().unwrap_or(0).wrapping_add(bias) & !0xfff;
let hi = lds.iter().map(|l| l.1 + l.2).max().unwrap_or(0).wrapping_add(bias);
let n = match gex_ld::elf::load(&bytes, bias) {
Ok(mut image) => {
gex_ld::elf::apply_own_relative(&mut image);
jit.add_image(&image, Some(&bytes))
}
Err(_) => 0,
};
if BY_OBJECT_ON.load(Ordering::Relaxed) {
let name = path.rsplit('/').next().unwrap_or(&path);
crate::warn_line(&format!("[fjit] starts of {name} at {bias:#x}: {n}\n"));
}
lock(&SEEDED).push((path, lo, hi.max(lo + 1)));
}
}
/// Translate the function at `addr` and publish it (`gex.lookup`'s miss):
/// its index in the function table, or `None` (the fallback runs it).
pub(crate) fn translate(addr: u64) -> Option<u32> {
if !on() || is_tier1_only(addr) {
let r = translate_root(addr);
if addr != 0 && addr == SCAN_CONST.load(Ordering::Relaxed) {
crate::warn_line(&format!("[fjitscan] translation asked at {addr:#x}: {r:?}\n"));
}
r
}
fn translate_root(addr: u64) -> Option<u32> {
if !on() || is_tier1_only(addr) || !lib_wanted(addr) {
return None;
}
let only = ONLY.load(Ordering::Relaxed);
@@ -144,6 +406,7 @@ pub(crate) fn translate(addr: u64) -> Option<u32> {
return None;
}
let jit = JIT.get()?;
seed_mapped(jit);
let (exec, ro, wx): (Vec<(u64, u64)>, Vec<(u64, u64)>, Vec<(u64, u64)>) = crate::with_host(|h| {
use gex_kernel::mem::prot;
let v = h.kernel.mem.vmas();
@@ -156,8 +419,39 @@ pub(crate) fn translate(addr: u64) -> Option<u32> {
if !exec.iter().any(|&(s, e)| addr >= s && addr < e) {
return None;
}
if mid_function(addr) {
return None;
}
// An address an ELF provider holds is not code to translate (cgen::elfprov).
let executable = |a: u64| exec.iter().any(|&(s, e)| a >= s && a < e) && !crate::cgen::elfprov::provided(a) && !crate::cgen::libprov::replaced(a);
// With `?fjitlibs=` rules, callees outside them are not brought in either.
let rules = !lock(&LIBS).is_empty();
// A callee a unit or the JIT already serves is not brought into the
// batch: the call reaches it through the table. Without this, a miss in
// UnityPlayer (a function its unit lacks) re-translated up to 63 of the
// unit's own functions with it -- 3,667 functions and 36 MiB of wasm by
// HK's menu (ht3), JIT code shadowing clang's -- and libasound's served
// callees went into every batch (28 MiB of wasm by the menu, hp12).
// `skip` bounds the batch only. Asked of instruction fetch (as part of
// `executable`), an entry inside the function being recovered -- a
// resume point, or a root the JIT took inside a function and, since
// misses no longer make starts, does not stop recovery at -- read as
// non-code and decoded as illegal: `cmovle` in libxcb's
// `_xcb_in_read_block` (hu15), and SIGILL at HK's start-up without the
// unit (hb52).
let served_memo = std::cell::RefCell::new(std::collections::HashMap::<u64, bool>::new());
let served = |a: u64| *served_memo.borrow_mut().entry(a).or_insert_with(|| crate::cgen::unit_serves(a) || crate::cgen::jit_serves(a));
// Asked of every byte fetched: the providers' addresses and replaced
// ranges taken once, and the range checks kept by 4 KiB page (exec,
// replaced and object ranges are pages; `?fjitlibs=` ranges are not).
let provided = crate::cgen::elfprov::provided_set();
let replaced = crate::cgen::libprov::replaced_ranges();
let page_memo = std::cell::RefCell::new(std::collections::HashMap::<u64, bool>::new());
let ranged = rules && lock(&LIBS).iter().any(|r| r.range.is_some());
let executable = |a: u64| {
let page_ok = |a: u64| exec.iter().any(|&(s, e)| a >= s && a < e) && !replaced.iter().any(|&(lo, hi)| a >= lo && a < hi) && (!rules || lib_wanted(a));
let ok = if ranged { page_ok(a) } else { *page_memo.borrow_mut().entry(a >> 12).or_insert_with(|| page_ok(a)) };
ok && !provided.contains(&a)
};
let read_only = |a: u64, n: u64| ro.iter().any(|&(s, e)| a >= s && a.saturating_add(n) <= e);
// Writable-and-executable code (a JIT's) stays tier-1, as natively.
let volatile = |a: u64| wx.iter().any(|&(s, e)| a >= s && a < e);
@@ -173,7 +467,7 @@ pub(crate) fn translate(addr: u64) -> Option<u32> {
};
let class32 = gex_abi::X86 && gex_xlate::arch::compat();
trace(&format!("translate {addr:#x}"));
let Some(wasm) = jit.translate(crate::guest_bytes(), addr, &executable, &read_only, &volatile, &pg, class32) else {
let Some(wasm) = jit.translate_skipping(0, crate::guest_bytes(), addr, &executable, &served, &read_only, &volatile, &pg, class32) else {
DECLINED.fetch_add(1, Ordering::Relaxed);
trace(&format!("declined {addr:#x}"));
return None;
@@ -183,6 +477,9 @@ pub(crate) fn translate(addr: u64) -> Option<u32> {
DECLINED.fetch_add(1, Ordering::Relaxed);
return None;
}
note_object(addr, funcs.len(), wasm.len());
dump_module(addr, &funcs, &wasm);
scan_const(addr, &funcs, &wasm);
let wasm = named(wasm);
// Held until the module is in the list: a Worker replaying the
// function table's log (`gex_fjit_ptr`, `gex_fjit_regs`) waits for it
@@ -210,7 +507,10 @@ pub(crate) fn translate(addr: u64) -> Option<u32> {
drop(modules);
TRANSLATED.fetch_add(1, Ordering::Relaxed);
let idx = crate::cgen::index_of(addr);
trace(&format!("module {n}: {addr:#x} {} funcs, install {install}, index {idx:?}", funcs.len()));
if TRACE.load(Ordering::Relaxed) {
let names: Vec<String> = crate::with_host(|h| funcs.iter().map(|&a| format!("{a:#x} {}", crate::object_off_at(h, a))).collect());
trace(&format!("module {n}: {addr:#x} {} funcs, install {install}, index {idx:?}: {}", funcs.len(), names.join(", ")));
}
idx
}
@@ -233,7 +533,7 @@ fn named(mut wasm: Vec<u8>) -> Vec<u8> {
/// make one)? Then its bridge reaches it once this Worker has installed
/// what the table's log holds (the caller traps or installs in place).
pub(crate) fn serve(pc: u64) -> bool {
if !on() || is_tier1_only(pc) {
if !on() || is_tier1_only(pc) || !lib_wanted(pc) {
return false;
}
if crate::cgen::index_of(pc).is_some() {
@@ -242,6 +542,124 @@ pub(crate) fn serve(pc: u64) -> bool {
translate(pc).is_some()
}
/// `+name,-name` (`?fjitlibs=`): the objects the JIT is limited to (any `+`)
/// and kept from (`-`), by file name. `name@lo-hi` (hex) narrows a rule to
/// the functions whose entry lies at those offsets into their mapping (the
/// `name +0x..` a trap prints), for bisecting within an object. Checked at
/// every translation, the table lookups' (`gex.lookup`) as well as tier-1's
/// misses: JIT code calling through a pointer must not pull in objects the
/// rules keep out.
struct Rule {
include: bool,
name: String,
range: Option<(u64, u64)>,
}
static LIBS: Mutex<Vec<Rule>> = Mutex::new(Vec::new());
#[no_mangle]
pub extern "C" fn gex_fjit_libs(ptr: u32, len: u32) {
let spec = String::from_utf8_lossy(&crate::guest_bytes()[ptr as usize..ptr as usize + len as usize]).into_owned();
let mut v = lock(&LIBS);
v.clear();
for w in spec.split(',').map(str::trim).filter(|w| !w.is_empty()) {
// `watch=0xADDR` (a debugging aid riding on the same query): say when
// the u64 there changes, at syscalls and safepoints (`watch_check`).
if let Some(a) = w.strip_prefix("watch=") {
if let Ok(a) = u64::from_str_radix(a.trim_start_matches("0x"), 16) {
WATCH.store(a, Ordering::Relaxed);
}
continue;
}
// `callout=0|2`, `inplace=0`, `callouttrap=1`: the A/B switches of
// the call-out and the in-place redispatch (bisection aids);
// `provcheck=1`: check each provider guest call ends at its return;
// `byobj=1`: count translations by object (`note_object`).
if let Some(a) = w.strip_prefix("scanconst=") {
if let Ok(a) = u64::from_str_radix(a.trim_start_matches("0x"), 16) {
SCAN_CONST.store(a, Ordering::Relaxed);
}
continue;
}
if let Some(a) = w.strip_prefix("dumpmod=") {
// `dumpmod=OBJ@OFF`: by the object's name and the hex offset from
// its text mapping (as `object_off_at` prints), for a layout
// whose addresses are not known ahead.
if let Some((name, off)) = a.split_once('@') {
if let Ok(off) = u64::from_str_radix(off.trim_start_matches("0x"), 16) {
*lock(&DUMP_OBJ) = Some((name.to_string(), off));
}
} else if let Ok(a) = u64::from_str_radix(a.trim_start_matches("0x"), 16) {
DUMP_MOD.store(a, Ordering::Relaxed);
}
continue;
}
if let Some((k, v)) = w.split_once('=') {
let v: i32 = v.parse().unwrap_or(1);
match k {
"callout" => crate::cgen::gex_set_callout(v),
"inplace" => crate::gex_set_in_place(v),
"callouttrap" => crate::cgen::TRAP_CALLOUTS.store(v != 0, Ordering::Relaxed),
"provcheck" => crate::cgen::libprov::PROV_CHECK.store(v != 0, Ordering::Relaxed),
"byobj" => BY_OBJECT_ON.store(v != 0, Ordering::Relaxed),
_ => crate::warn_line(&format!("gex: ?fjitlibs: no switch {k}\n")),
}
continue;
}
let (include, w) = match w.strip_prefix('-') {
Some(n) => (false, n),
None => (true, w.trim_start_matches('+')),
};
let hex = |x: &str| u64::from_str_radix(x.trim_start_matches("0x"), 16).ok();
let (name, range) = match w.split_once('@') {
Some((n, r)) => (n, r.split_once('-').and_then(|(lo, hi)| Some((hex(lo)?, hex(hi)?)))),
None => (w, None),
};
v.push(Rule { include, name: name.to_string(), range });
}
}
static WATCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static WATCH_LAST: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(u64::MAX);
static WATCH_SAID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
/// `?fjitlibs=watch=0xADDR`: the u64 at ADDR changed since the last look
/// (any thread's): say where this thread is (`at`: syscall or safepoint).
pub(crate) fn watch_check(pc: u64, at: &str) {
let a = WATCH.load(Ordering::Relaxed);
if a == 0 {
return;
}
// Not before the memory reaches it (the slice spans the maximum).
if a + 8 > crate::warch::memory_size(0) as u64 * 65536 {
return;
}
let mem = crate::guest_bytes();
let Some(b) = mem.get(a as usize..a as usize + 8) else { return };
let v = u64::from_le_bytes(b.try_into().unwrap());
let last = WATCH_LAST.swap(v, Ordering::Relaxed);
if last != v && WATCH_SAID.fetch_add(1, Ordering::Relaxed) < 64 {
let (thread, whose, vwhose) = crate::with_host(|h| (crate::cur(), crate::object_off_at(h, pc), crate::object_off_at(h, v)));
crate::warn_line(&format!(
"[watch] {a:#x}: {last:#x} -> {v:#x} ({vwhose}), seen by thread {thread} at {at} pc {pc:#x} ({whose}), {} JIT modules\n",
lock(&MODULES).len()
));
}
}
fn lib_wanted(pc: u64) -> bool {
let rules = lock(&LIBS);
if rules.is_empty() {
return true;
}
let (name, off) = crate::object_file_off(pc);
let hit = |r: &Rule| r.name == name && r.range.is_none_or(|(lo, hi)| off >= lo && off < hi);
if rules.iter().any(|r| !r.include && hit(r)) {
return false;
}
!rules.iter().any(|r| r.include) || rules.iter().any(|r| r.include && hit(r))
}
/// JIT module `n`'s bytes.
#[no_mangle]
pub extern "C" fn gex_fjit_ptr(n: u32) -> u32 {
+55 -1
View File
@@ -5959,6 +5959,7 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
IN_PROVIDER.with(|c| c.set(provider));
((pc as u64) & !PROVIDER_SYSCALL) as i64
};
fjit::watch_check(pc as u64, "syscall");
// A guest thread in a syscall counts as stopped for a provider's
// collection (cgen/libprov.rs); it parks on its way back if one runs.
let stop_tracked = !IN_PROVIDER.with(|c| c.get()) && cgen::libprov::any_registered();
@@ -7279,9 +7280,23 @@ fn resume() -> Option<u64> {
RESUME.with(|c| c.get())
}
thread_local! {
/// This Worker's last resumes set (the shim's line that set it, pc), for
/// the wild-branch report: who sent the guest where it went.
static RESUME_RING: std::cell::RefCell<Vec<(u32, u64)>> = const { std::cell::RefCell::new(Vec::new()) };
}
#[track_caller]
fn set_resume(v: u64) {
RESUME.with(|c| c.set(Some(v)));
let line = std::panic::Location::caller().line();
RESUME_RING.with(|r| {
let mut r = r.borrow_mut();
if r.len() == 16 {
r.remove(0);
}
r.push((line, v));
});
// GEX_NT_RESUME_LOG: each (call site, pc) a resume was set from, once:
// where the guest leaves wasm and comes back (an AOT-only run needs a
// unit or region entry at every such pc).
@@ -10590,6 +10605,7 @@ pub extern "C" fn gex_cache_op(_ctx: i32, _pc: i64, op: i32, addr: i64) {
/// preemption is the engine's job now; see the end of this function.)
#[no_mangle]
pub extern "C" fn gex_safepoint(ctx: i32, pc: i64) {
fjit::watch_check(pc as u64, "safepoint");
// A Windows process's clocks in KUSER_SHARED_DATA, which a guest polls
// without a syscall: a loop that spins on them passes here.
nt::tick();
@@ -11086,6 +11102,16 @@ pub extern "C" fn gex_dispatch_miss(_ctx: i32, pc: i64) -> i32 {
let stack: Vec<String> = (0..8)
.map(|i| format!("{:#x}", gm.read_u64(sp + i * 8).unwrap_or(0)))
.collect();
// The stack around sp, named, and the registers (a bad return's frame).
let around: Vec<String> = (0..24u64)
.map(|i| {
let a = sp.wrapping_sub(0x60).wrapping_add(i * 8);
let v = gm.read_u64(a).unwrap_or(0);
format!("{a:#x}={v:#x}{}", Some(object_off_at(h, v)).filter(|n| !n.is_empty()).map(|n| format!(" ({n})")).unwrap_or_default())
})
.collect();
let regs: Vec<String> = (0..16u8).map(|r| format!("r{r}={:#x}", gm.read_u64(c + ctx::x(r) as u64).unwrap_or(0))).collect();
warn_line(&format!("[gex] wild branch: stack {}; regs {}\n", around.join(", "), regs.join(" ")));
warn_line(&format!(
"[gex] pid {} thread {} branched to {pc:#x}, which is not executable guest memory (SIGSEGV); last indirect target {last:#x} ({}) from block {src:#x} ({}), sp={sp:#x} stack=[{}], last syscall {}\n",
h.kernel.pid,
@@ -11105,6 +11131,15 @@ pub extern "C" fn gex_dispatch_miss(_ctx: i32, pc: i64) -> i32 {
cgen::provider_calls_recent()
));
warn_line(&format!("[gex] last lookups (address, index): {:x?}\n", cgen::lookups_recent()));
warn_line(&format!(
"[gex] last call-outs (target, return, sp wanted, depth, result, sp after): {:x?}; provider guest call open {}\n",
cgen::callouts_recent(),
cgen::libprov::guest_call_open()
));
warn_line(&format!(
"[gex] last resumes set (lib.rs line, pc): {:x?}\n",
RESUME_RING.with(|r| r.borrow().iter().map(|&(l, v)| (l, v)).collect::<Vec<_>>())
));
}
set_exit(h, 128 + 11);
});
@@ -11893,6 +11928,7 @@ fn retire_unmapped_code() {
let retired = std::mem::take(&mut h.kernel.retired_code);
if !retired.is_empty() {
crate::cgen::on_retire(&retired);
crate::fjit::on_retire(&retired);
retire_code(h, &retired);
}
});
@@ -12511,12 +12547,30 @@ fn note_blocks(
e.2 = e.2.max(a);
}
/// The object file at `a` (by file name) and `a`'s offset into its mapping.
pub(crate) fn object_file_off(a: u64) -> (String, u64) {
with_host(|h| {
h.kernel
.mem
.vmas()
.iter()
.find(|v| a >= v.start && a < v.end)
.map(|v| (v.code.as_ref().map(|(p, _)| p.rsplit('/').next().unwrap_or(p).to_string()).unwrap_or_default(), a - v.start))
.unwrap_or_default()
})
}
/// [`object_off_at`], taking the host itself.
pub(crate) fn object_off_str(a: u64) -> String {
with_host(|h| object_off_at(h, a))
}
/// The short name of whatever object holds this address, for a counter. Takes
/// the host it already has, since the caller is inside `with_host` and a second
/// `RefCell` borrow would panic.
/// Like `object_name_at` but with the `+offset` into the object, so a return
/// address on a crash stack can be run through objdump to name the function.
fn object_off_at(h: &Host, a: u64) -> String {
pub(crate) fn object_off_at(h: &Host, a: u64) -> String {
h.kernel
.mem
.vmas()
+39
View File
@@ -742,11 +742,47 @@ fn bisect_allows(addr: u64) -> bool {
only.as_ref().is_none_or(inr) && !skip.as_ref().is_some_and(inr)
}
/// `GEX_FJIT_LIBS=+name,-name` (file names, as the page's `?fjitlibs=`):
/// the objects the function JIT is limited to (any `+`) and kept from (`-`).
fn libs_allow(caller: &mut Caller<'_, HostState>, addr: u64) -> bool {
static RULES: std::sync::OnceLock<Vec<(bool, String)>> = std::sync::OnceLock::new();
let rules = RULES.get_or_init(|| {
std::env::var("GEX_FJIT_LIBS")
.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|w| !w.is_empty())
.map(|w| match w.strip_prefix('-') {
Some(n) => (false, n.to_string()),
None => (true, w.trim_start_matches('+').to_string()),
})
.collect()
});
if rules.is_empty() {
return true;
}
let name = caller.data_mut().with_kernel(|k| {
k.mem
.vmas()
.iter()
.find(|v| addr >= v.start && addr < v.end)
.and_then(|v| v.code.as_ref().map(|(p, _)| p.rsplit('/').next().unwrap_or(p).to_string()))
.unwrap_or_default()
});
if rules.iter().any(|(inc, n)| !inc && *n == name) {
return false;
}
!rules.iter().any(|(inc, _)| *inc) || rules.iter().any(|(inc, n)| *inc && *n == name)
}
fn job(caller: &mut Caller<'_, HostState>, sh: &Arc<Mutex<JitShared>>, memory: &gex_mem::GuestMemory, addr: u64) -> Option<Job> {
gex_kernel::ftab::translator()?;
if !bisect_allows(addr) {
return None;
}
if !libs_allow(caller, addr) {
return None;
}
let (exec, ro, wx): (Vec<(u64, u64)>, Vec<(u64, u64)>, Vec<(u64, u64)>) = caller.data_mut().with_kernel(|k| {
let v = k.mem.vmas();
(
@@ -968,6 +1004,9 @@ pub(crate) fn install_bridges(mut cx: impl AsContextMut<Data = HostState>, slots
/// sending callers there first.
pub(crate) fn on_retire(sh: &mut JitShared, memory: &mut [u8], ranges: &[(u64, u64)]) {
sh.ftab.epoch += 1;
if let Some(t) = gex_kernel::ftab::translator() {
t.retire(sh.ftab.process.0, ranges);
}
sh.ftab.queued.retain(|&a| !ranges.iter().any(|&(s, e)| a >= s && a < e));
for &(start, end) in ranges {
let hit: Vec<(u64, gex_kernel::ftab::Func)> = sh.ftab.map.range(start, end).collect();
+55
View File
@@ -0,0 +1,55 @@
// The function JIT must forget what it learned of code that is unmapped: a
// dlclose and a later dlopen put another library at the same addresses. Code
// A has a function at +0x10; code B, mapped over it, has a min() whose cmp is
// before +0x10 and whose cmovle is at it. Kept, A's start split B's function
// there as at a tail call, with the flags dead (HK's libdbus and libxcb in the
// page, docs/unity-dotnet.md). Output must match native.
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
typedef int (*fn)(int, int);
static void *at;
// The first call picks the address; the second maps over the same one.
static void *place(const unsigned char *code, size_t n) {
void *p = mmap(at, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | (at ? MAP_FIXED : 0), -1, 0);
if (p == MAP_FAILED || (at && p != at)) return 0;
at = p;
memset(p, 0xcc, 4096);
memcpy(p, code, n);
if (mprotect(p, 4096, PROT_READ | PROT_EXEC)) return 0;
return p;
}
int main(void) {
// A: +0x00 ret; +0x10 lea (%rdi,%rsi),%eax; ret
unsigned char a[0x14] = {0xc3};
memset(a + 1, 0x90, 0xf);
a[0x10] = 0x8d; a[0x11] = 0x04; a[0x12] = 0x37; a[0x13] = 0xc3;
char *p = place(a, sizeof a);
if (!p) { puts("map A failed"); return 1; }
volatile fn f = (fn)(p + 0x10);
long s = 0;
for (int i = 0; i < 200000; i++) s += f(i, 1);
printf("A %ld\n", s);
if (munmap(p, 4096)) { puts("munmap failed"); return 1; }
// B: mov %esi,%eax; cmp %esi,%edi; nop x12; +0x10 cmovle %edi,%eax; ret
unsigned char b[0x14] = {0x89, 0xf0, 0x39, 0xf7};
memset(b + 4, 0x90, 0xc);
b[0x10] = 0x0f; b[0x11] = 0x4e; b[0x12] = 0xc7; b[0x13] = 0xc3;
p = place(b, sizeof b);
if (!p) { puts("map B failed"); return 1; }
volatile fn g = (fn)p;
long bad = 0, t = 0;
for (int i = 0; i < 200000; i++) {
int x = (i * 7919) % 1000 - 500, y = (i * 104729) % 1000 - 500;
int m = g(x, y);
t += m;
if (m != (x < y ? x : y)) bad++;
}
printf("B %ld bad %ld\n", t, bad);
return 0;
}
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
# Code unmapped and replaced at the same address (remap.c): output under gexc
# --fjit must match native.
#
# tests/fjit-remap/run.sh [gexc] (needs zig for the musl target)
set -u
HERE=$(cd "$(dirname "$0")" && pwd)
GEXC=${1:-target/x86/release/gexc}
W=${WORK:-$(mktemp -d)}
mkdir -p "$W"
zig cc -target x86_64-linux-musl -O1 -static -o "$W/remap" "$HERE/remap.c" || exit 1
native=$("$W/remap")
fail=0
for flags in "" "--fjit"; do
gex=$("$GEXC" run $flags "$W/remap" 2>&1)
if [ "$native" = "$gex" ]; then
echo "ok remap ${flags:-t1}"
else
echo "FAIL remap ${flags:-t1}"; printf 'native:\n%s\ngex:\n%s\n' "$native" "$gex"; fail=1
fi
done
exit $fail
+2
View File
@@ -252,6 +252,8 @@ async function main() {
writeFileSync(join(shotDir, `c${String(shots).padStart(3, '0')}.png`), encodePng(r.w, r.h, Buffer.from(r.b64, 'base64')));
}
}
// The guest's output so far (a game's log with `-logFile -`), kept whole.
if (shotDir && s?.out) writeFileSync(join(shotDir, 'out.txt'), s.out);
const rate = window.length > 1 ? (n - window[0][1]) / ((Date.now() - window[0][0]) / 1000) : 0;
log(`${((Date.now() - t0) / 1000).toFixed(0)} s: presents ${n}, ${rate.toFixed(1)}/s over the last minute; ${(s?.out ?? '').slice(-160).replace(/\n/g, ' | ')}`);
// The compositor's own account: its windows, its notes (GL attachment
+5 -1
View File
@@ -235,6 +235,7 @@ export async function openPage(query, { width = 1280, height = 900, shotDir = nu
const { sessionId: page } = await browser.send('Target.attachToTarget', { targetId, flatten: true });
const consoleLines = [];
let printed = 0;
let keepRe = null;
// Every Worker's console too: each guest thread is one.
browser.on('Runtime.consoleAPICalled', (p, sid) => {
const text = p.args.map((a) => a.value ?? a.description ?? '').join(' ');
@@ -246,7 +247,10 @@ export async function openPage(query, { width = 1280, height = 900, shotDir = nu
const wanted = p.type === 'error'
? !/^\[mesa-gl\] gex-gl: gl[A-Z]/.test(text)
: p.type === 'warning' && /gex-nt|gex-wgpu|wine:|\[gex\]|panic|trap/i.test(text);
if ((wanted || process.env.GEX_NT_BROWSER_VERBOSE) && printed++ < 20000) {
// GEX_NT_BROWSER_KEEP=REGEX: also any line it matches (Mesa's compile
// and link notes, say), without the verbose flood.
keepRe ??= process.env.GEX_NT_BROWSER_KEEP ? new RegExp(process.env.GEX_NT_BROWSER_KEEP) : false;
if ((wanted || process.env.GEX_NT_BROWSER_VERBOSE || (keepRe && keepRe.test(text))) && printed++ < 20000) {
log(`console[${sid?.slice(0, 6)} @${((Date.now() - T0) / 1000).toFixed(1)}s] ${text.slice(0, 4000)}`);
}
});
+20 -1
View File
@@ -20,7 +20,26 @@ else
fi
OUT=${1:-$DEFAULT_OUT}
BUILD=$OUT/build
EMDAWN=${GEX_EMDAWN_DIR:-$($ROOT/web/fetch-emdawnwebgpu.sh)}
# emdawnwebgpu: a Mesa checkout that pins its own (the wink branch:
# wink/lib/emdawn.sh, a release matched to its Tint and patched by
# wink/tools/patch-emdawnwebgpu.py) supplies it; else gex's own fetch.
if [ -z "${GEX_EMDAWN_DIR:-}" ] && [ -f "$MESA/wink/lib/emdawn.sh" ]; then
WINK_CACHE=${GEX_MESA_WINK_CACHE:-${TMPDIR:-/tmp}/gex-mesa-wink-cache}
EMSDK_ROOT=$(CDPATH='' cd -- "$(dirname "$(command -v emcc)")/../.." && pwd)
emdawn_log=$(WINK_ALLOW_ANY_EMSDK=1 bash "$MESA/wink/build.sh" --variant=mt --until=emdawn \
--cache="$WINK_CACHE" --build-dir="$WINK_CACHE/build" --emsdk="$EMSDK_ROOT" 2>&1) || {
echo "$emdawn_log" | tail -20 >&2
exit 1
}
EMDAWN=$(echo "$emdawn_log" | sed -n 's/^\[emdawn\] \(ready\|cached\) at //p' | tail -1)
[ -f "$EMDAWN/webgpu/include/webgpu/webgpu.h" ] || {
echo "$emdawn_log" | tail -20 >&2
echo "no emdawnwebgpu from wink/build.sh" >&2
exit 1
}
else
EMDAWN=${GEX_EMDAWN_DIR:-$($ROOT/web/fetch-emdawnwebgpu.sh)}
fi
TINT_OUT=${GEX_MESA_TINT:-${TMPDIR:-/tmp}/gex-mesa-wasm/mesa-tint-pthread}
TINT_TMP=${GEX_MESA_TINT_TMP:-${TMPDIR:-/tmp}/gex-mesa-tint-build}
+1
View File
@@ -1440,6 +1440,7 @@ self.onmessage = ({ data: m }) => {
canvas = m.canvas;
presentOverride = m.present ?? null;
// The compositor draws with WebGPU on the device Mesa will use.
if (m.glLimits != null) globalThis.GEX_GL_LIMITS = m.glLimits;
requestGexDevice().then((gpu) => {
comp = new Compositor(m.canvas, m.channel, m.stats ? new Int32Array(m.stats) : null, gpu);
comp.setDirect(m.direct);
+6 -1
View File
@@ -137,8 +137,13 @@ export class Compositor {
// is the canvas, fitted to the viewport; compositing only when needed.
const direct = pq?.get('direct') === '1';
this.#pointerLock = pq?.get('pointerlock') === '1';
// `?gllimits=40|41|43`: the device asks for WebGPU's default limits,
// GL 4.1's texture limits (the default), or 4.3's compute ones too,
// which decide the GL version Mesa's context earns (gpu-device.js). At
// 4.6 Muck's 3D scene drew black, so 43 is only per run.
const glLimits = pq?.has('gllimits') ? Number(pq.get('gllimits')) || 0 : null;
worker.postMessage(
{ kind: 'init', canvas: offscreen, channel, stats: this.#stats, present, direct },
{ kind: 'init', canvas: offscreen, channel, stats: this.#stats, present, direct, glLimits },
[offscreen],
);
await ready;
+21 -4
View File
@@ -44,11 +44,28 @@ export function requestGexDevice() {
maxStorageBuffersInVertexStage: 16,
maxStorageBuffersInFragmentStage: 16,
maxColorAttachmentBytesPerSample: 64,
// Not yet: asking for GL 4.1's texture limits (16384 texels, 2048
// layers) and GL 4.3's compute ones makes the context GL 4.6, and then
// Unity's 4.x shader variants draw Muck's 3D scene black (U, mp26).
// Back once that is fixed; see docs/gl-path.md.
// GL 4.3's compute limits (with them the context is GL 4.6) drew
// Muck's 3D scene black (U, mp26), so they are only per run; GL 4.1's
// texture limits are the default, below.
};
// `?gllimits=40|41|43` (GEX_GL_LIMITS, set by the compositor worker;
// 41 when unset): GL 4.1 wants 16384 texels a side and 2048 array
// layers, GL 4.3's compute shaders 1024 invocations a workgroup and
// 32 KiB of shared memory; WebGPU's defaults (8192, 256; 256, 16 KiB)
// keep the context at GL 4.0. Unity drops shaders whose target the
// context is below: at 4.0 Muck's grass geometry shader ("Shader
// Unsupported ... All passes removed") left the grass as dots; at 4.2
// with mesa-wink 60202246 it draws blades (U, mpg3).
const glLimitsSet = globalThis.GEX_GL_LIMITS;
const glLimits = glLimitsSet === undefined ? 41 : Number(glLimitsSet) || 0;
if (glLimits >= 41) {
Object.assign(want, { maxTextureDimension1D: 16384, maxTextureDimension2D: 16384,
maxTextureDimension3D: 2048, maxTextureArrayLayers: 2048 });
}
if (glLimits >= 43) {
Object.assign(want, { maxComputeInvocationsPerWorkgroup: 1024, maxComputeWorkgroupSizeX: 1024,
maxComputeWorkgroupSizeY: 1024, maxComputeWorkgroupSizeZ: 64, maxComputeWorkgroupStorageSize: 32768 });
}
const requiredLimits = {};
for (const [limit, wanted] of Object.entries(want)) {
const available = adapter.limits[limit];
+10 -1
View File
@@ -419,6 +419,15 @@ export class HostX {
}
},
/** Room to write: a guest's socket in the kernel fills (256 KiB); the
* compositor's queues never do. */
writable(fd) {
const ep = self.#fds.get(fd);
if (!ep) return -1;
if (ep.kind === 'kernel') return self.#k.k_sock_writable(ep.id) ? 1 : 0;
return 1;
},
send(fd, buf, len, fds, nfds) {
const ep = self.#fds.get(fd);
if (!ep) return -EBADF;
@@ -515,7 +524,7 @@ export class HostX {
const wrapped = {};
let left = this.#trace;
for (const [name, fn] of Object.entries(raw)) {
if (name === 'tick' || name === 'wait' || name === 'readable') {
if (name === 'tick' || name === 'wait' || name === 'readable' || name === 'writable') {
wrapped[name] = fn;
continue;
}
+11
View File
@@ -119,6 +119,12 @@ async function main() {
if (q.get('wasmdlls')) globalThis.GEX_WASM_DLLS = q.get('wasmdlls');
// `?elfunits=name,...`: shared objects that get function-tier units (with `?cgen=1`).
if (q.get('elfunits')) globalThis.GEX_ELF_UNITS = q.get('elfunits');
// `?fjitlibs=+name,-name`: the function JIT only for (+) or not for (-) those objects (a bisection aid).
if (q.get('fjitlibs')) globalThis.GEX_FJIT_LIBS = q.get('fjitlibs');
// `?pins=path@base+span,...` (hex): place those shared objects at fixed bases, as a
// closure's pins do, so a unit built for one base (`?elfunits=`) is used whatever
// else (the function JIT's heap, say) moved the arena first.
if (q.get('pins')) globalThis.GEX_PINS = q.get('pins');
if (q.get('dense') === '0') globalThis.GEX_DENSE = 0;
if (q.get('dense') === '2') globalThis.GEX_DENSE = 2;
// cgen=1|2 (prefer|require units), in the unguarded flavor of i386 units
@@ -133,6 +139,11 @@ async function main() {
if (q.get('aot')) globalThis.GEX_AOT = 1;
if (q.get('densedebug')) globalThis.GEX_DENSE_DEBUG = 1;
if (q.get('tracemisses')) globalThis.GEX_TRACE_MISSES = 1;
// `?attachtrace=1`: time each thread Worker from the guest's clone to its
// start (host.mjs, gex.mjs `#catchUp`), as main.js does for the demos.
if (q.get('attachtrace')) globalThis.GEX_ATTACH_TRACE = 1;
// `?strace=1`: narrate every syscall (`gex_set_trace`), as main.js does.
if (q.get('strace')) globalThis.GEX_STRACE = 1;
const [w, h] = (q.get('size') || '1024x768').split('x').map(Number);
const canvas = document.getElementById('out');
canvas.width = w;
+15 -1
View File
@@ -691,6 +691,8 @@ export async function runProgram(
wasmDlls: globalThis.GEX_WASM_DLLS || undefined,
// `?elfunits=name,...`: shared objects that get function-tier units (cgen/libprov.rs).
elfUnits: globalThis.GEX_ELF_UNITS || undefined,
// `?fjitlibs=+name,-name` (fjit.rs `gex_fjit_libs`).
fjitLibs: globalThis.GEX_FJIT_LIBS || undefined,
traceMisses: !!globalThis.GEX_TRACE_MISSES,
// `?sharetrace=1`: say how compiled modules are shared between a process's Workers.
shareTrace: flag('sharetrace') ? 1 : undefined,
@@ -815,7 +817,7 @@ export async function runProgram(
// out an address inside a pinned object and the image lands somewhere
// its own translated code does not expect.
opts: {
cwd, tty, uid, recompileObjects, heartbeat, link, dirs, modes, hard, symlinks, env, pins: closure?.pins,
cwd, tty, uid, recompileObjects, heartbeat, link, dirs, modes, hard, symlinks, env, pins: pinsFrom(closure?.pins, globalThis.GEX_PINS),
// `?strace=1`, and every child inherits it: see `#opts` in gex.mjs.
strace: globalThis.GEX_STRACE ? 1 : undefined,
// `?stracepid=N`: narrate that one process, which is affordable.
@@ -857,3 +859,15 @@ export async function runProgram(
export { TerminalEnv };
export { loadKernel as globalKernel };
/** A closure's pins plus the page's `?pins=path@base+span,...` (hex; the query wins). */
function pinsFrom(closurePins, spec) {
if (!spec) return closurePins;
const out = { ...(closurePins ?? {}) };
for (const w of spec.split(',').map((x) => x.trim()).filter(Boolean)) {
const m = /^(.+)@(?:0x)?([0-9a-f]+)(?:\+(?:0x)?([0-9a-f]+))?$/i.exec(w);
if (!m) { console.error(`[gex] ?pins: cannot read ${w}`); continue; }
out[m[1]] = { base: Number.parseInt(m[2], 16), span: m[3] ? Number.parseInt(m[3], 16) : 0 };
}
return out;
}
+13 -6
View File
@@ -36,6 +36,7 @@ EM_JS(int, gexjs_connect, (int fd, const char *path, int len), { return Module.g
EM_JS(int, gexjs_accept, (int fd), { return Module.gexos.accept(fd); });
EM_JS(int, gexjs_close, (int fd), { return Module.gexos.close(fd); });
EM_JS(int, gexjs_readable, (int fd), { return Module.gexos.readable(fd); });
EM_JS(int, gexjs_writable, (int fd), { return Module.gexos.writable(fd); });
EM_JS(int, gexjs_send, (int fd, const char *buf, int len, const int *fds, int nfds),
{ return Module.gexos.send(fd, buf, len, fds, nfds); });
EM_JS(int, gexjs_recv, (int fd, char *buf, int len, int *fds, int maxfds, int *nfds),
@@ -510,7 +511,7 @@ gex_poll(struct pollfd *fds, nfds_t nfds, int timeout)
fds[i].revents = 0;
if (fds[i].fd < 0)
continue;
if (fds[i].events & POLLOUT)
if ((fds[i].events & POLLOUT) && gexjs_writable(fds[i].fd) > 0)
fds[i].revents |= POLLOUT;
if (fds[i].events & POLLIN) {
int r = gexjs_readable(fds[i].fd);
@@ -519,11 +520,14 @@ gex_poll(struct pollfd *fds, nfds_t nfds, int timeout)
else if (r < 0)
fds[i].revents |= POLLNVAL;
}
if (fds[i].revents & (POLLIN | POLLNVAL))
if (fds[i].revents)
ready++;
}
/* POLLOUT alone is not worth waking for: the server asks for it while
* it has a client's reply buffered, and a queue here is never full. */
/* POLLOUT counts. The server asks for it when a client's socket in
* the kernel is full (256 KiB) with a reply still buffered, and it
* writes the rest only when poll says so: a poll that kept waiting
* for POLLIN instead hung every reply bigger than the socket (the
* client waits for the reply and sends nothing). */
if (ready || timeout == 0)
return ready;
int slice = timeout < 0 ? 20 : (timeout - (int) deadline_slept > 20
@@ -539,10 +543,13 @@ gex_poll(struct pollfd *fds, nfds_t nfds, int timeout)
for (nfds_t i = 0; i < nfds; i++) {
fds[i].revents = 0;
if (fds[i].fd >= 0 && (fds[i].events & POLLIN) &&
gexjs_readable(fds[i].fd) > 0) {
gexjs_readable(fds[i].fd) > 0)
fds[i].revents |= POLLIN;
if (fds[i].fd >= 0 && (fds[i].events & POLLOUT) &&
gexjs_writable(fds[i].fd) > 0)
fds[i].revents |= POLLOUT;
if (fds[i].revents)
last++;
}
}
return last;
}