Merge master (b7fead07) into merge/wine-fork

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01PeRutwFgZ66XpxjasKrPn1
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-05 15:31:53 -04:00
commit 414f43dc2e
28 files changed
+1078 -77

No files matched your search

+7
View File
@@ -39,6 +39,13 @@ pub fn load_u32(bytes: &mut [u8], off: u64) -> Option<u32> {
Some(word(bytes, off)?.load(Ordering::SeqCst))
}
/// An atomic store (release, and every earlier store of this thread's with
/// it: a lock word's release).
pub fn store_u32(bytes: &mut [u8], off: u64, v: u32) -> Option<()> {
word(bytes, off)?.store(v, Ordering::SeqCst);
Some(())
}
#[cfg(test)]
mod tests {
use super::*;
+114
View File
@@ -0,0 +1,114 @@
/* PROVIDES [nt-trampolines] i386
*
* The i386 unix-call dispatcher (gex_kernel::nt::boot::tramp::UNIX_CALL_32,
* what ntdll's __wine_unix_call_dispatcher points at), with the hottest
* OpenGL calls written straight into the GL command ring. Wine's PE
* opengl32 makes one unix call per GL function; Noita makes about 3,900 a
* frame, 81% of them the seven below, and each was a trip through the NT
* layer (the syscall, the unix-call dispatch, opengl32's Unix side) to
* append 12 to 16 bytes to the ring. Here they are those bytes: the same
* command the layer writes for them (gex_kernel::nt::opengl's `hot_one`).
*
* The ring is the layer's: it publishes the ring's address at GL_FAST_RING
* once the device is open (0 before, and with GEX_NT_GL_FAST=0), and the
* tail at GL_FAST_TAIL, whose top bit is the ring's lock (the layer holds it
* while it writes a command or submits). Anything else -- another library,
* another GL function, no ring yet, the lock held, a full ring -- is the
* trampoline's own work: the UNIX_CALL syscall, made as the trampoline
* makes it (`mov eax, UNIX_CALL; call 1f; ret 16; 1: int 0x80; ret`).
*
* stdcall (handle: u64, code, params), 16 bytes popped; returns NTSTATUS. */
/* gex_kernel::nt::TRAMPOLINES and the i386 unix-call trampoline in it. */
#define TR 0x3f0000u
#define TR_RET16 (TR + 0x6au) /* the `ret 16` the inner call returns to */
#define TR_INT80 (TR + 0x6du) /* `int 0x80` */
#define TR_RET (TR + 0x6fu) /* the `ret` after it */
#define UNIX_CALL_NR 0x7f000001u /* gex_kernel::nt::gex_nr::UNIX_CALL */
/* gex_kernel::nt::opengl: the fast path's words and the ring's shape. */
#define GL_FAST_RING 0x7ffe0fe0u
#define GL_FAST_TAIL 0x7ffe0fe4u
#define TAIL_BUSY 0x80000000u
#define RING_HEADER 32u
#define CMD_CAPACITY (8u << 20)
/* opengl32's unixlib handle ((3 << 16) | 1, gex_kernel::nt::unixcall). */
#define GL_HANDLE 0x30001u
/* opengl32's unix-call codes (`enum unix_funcs`, gex_kernel::nt::opengl::
* table::FUNCS) and gex-gl's opcodes (gex_gl::proto::op). Checked against
* both by gex-kernel's test `nt_unixcall_provider_constants`. */
#define FN_glBindTexture 14u
#define FN_glDisable 72u
#define FN_glEnable 81u
#define FN_glActiveTexture 350u
#define FN_glUniform1i 2524u
#define FN_glUniform2f 2542u
#define FN_glUseProgram 2645u
#define OP_ENABLE 19u
#define OP_DISABLE 20u
#define OP_USE_PROGRAM 55u
#define OP_UNIFORM_1I 64u
#define OP_UNIFORM_2F 66u
#define OP_BIND_TEXTURE 98u
#define OP_ACTIVE_TEXTURE 99u
/* Append one command of `n` argument words (1 to 3) taken from the params
* struct (after its TEB pointer). 1 when it is in the ring. */
GEX_INLINE int gl_put(uint32_t op, uint32_t params, uint32_t n) {
uint32_t ring = __atomic_load_n(GA(GL_FAST_RING), __ATOMIC_ACQUIRE);
if (!ring) {
return 0;
}
uint32_t len = 8u + 4u * n;
uint32_t t = __atomic_load_n(GA(GL_FAST_TAIL), __ATOMIC_ACQUIRE);
if ((t & TAIL_BUSY) || t + len > CMD_CAPACITY) {
return 0;
}
if (!__atomic_compare_exchange_n(GA(GL_FAST_TAIL), &t, t | TAIL_BUSY, 0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) {
return 0;
}
uint32_t at = ring + RING_HEADER + t;
GW(at) = op;
GW(at + 4u) = len;
for (uint32_t i = 0; i < n; ++i) {
GW(at + 8u + 4u * i) = GW(params + 4u + 4u * i);
}
__atomic_store_n(GA(GL_FAST_TAIL), t + len, __ATOMIC_RELEASE);
return 1;
}
static GEX_PROVIDER(p_unix_call) {
(void)need;
if (gp_arg(ctx, 0) == GL_HANDLE && gp_arg(ctx, 1) == 0) {
uint32_t code = gp_arg(ctx, 2), params = gp_arg(ctx, 3), op = 0, n = 0;
switch (code) {
case FN_glEnable: op = OP_ENABLE, n = 1; break;
case FN_glDisable: op = OP_DISABLE, n = 1; break;
case FN_glActiveTexture: op = OP_ACTIVE_TEXTURE, n = 1; break;
case FN_glUseProgram: op = OP_USE_PROGRAM, n = 1; break;
case FN_glBindTexture: op = OP_BIND_TEXTURE, n = 2; break;
case FN_glUniform1i: op = OP_UNIFORM_1I, n = 2; break;
case FN_glUniform2f: op = OP_UNIFORM_2F, n = 3; break;
default: break;
}
if (n && params >= 0x10000u && gl_put(op, params, n)) {
return gp_return_eax(ctx, ret, 16, 0);
}
}
/* The trampoline: its inner call's return address pushed, the syscall
* made at its `int 0x80` (a call that does not come straight back
* leaves from inside gex_syscall, the guest at TR_RET with this very
* stack), then its `ret` and `ret 16`. */
(void)depth;
uint32_t sp = gp_reg(ctx, R_ESP) - 4u;
GW(sp) = TR_RET16;
gp_set(ctx, R_ESP, sp);
gp_set(ctx, R_EAX, UNIX_CALL_NR);
GEX_C64(GEX_CTX_PC) = TR_RET;
gex_syscall(ctx, (int64_t)TR_INT80);
gp_set(ctx, R_ESP, gp_reg(ctx, R_ESP) + 4u);
return gp_return(ctx, ret, 16);
}
GEX_PROVIDE(p_unix_call, "unix_call_32")
+112 -19
View File
@@ -60,8 +60,17 @@ pub fn body_name(entry: u64) -> String {
/// What a call to `t` names: the unit-private copy when `t` is a small
/// leaf (`FnUnit::leaves`), else its body. On the unit's fields, so it is
/// callable while others are borrowed.
fn callee_name(leaves: &HashMap<u64, Function>, copying: bool, copies: &mut BTreeSet<u64>, stats: &mut FnStats, t: u64) -> String {
if !copying && leaves.contains_key(&t) {
fn callee_name(leaves: &HashMap<u64, Function>, copying: bool, in_loop: bool, copies: &mut BTreeSet<u64>, stats: &mut FnStats, t: u64) -> String {
// A small caller's copy only at a call in an outlined loop function
// (`in_loop`), where V8 runs out of inlining budget (0x724580's
// l_724580_0 calling 0x70b110); everywhere it grew noita.exe's unit by
// 42% (165 -> 234 MiB) for a gain only the loop showed
// (`GEX_CGEN_EXP=SMALLCALLSALL` restores that; both are opt-in, see
// `is_small_copyable`). Inside a copy, only a
// leaf's copy (no chains of calling copies): a small caller's copy keeps
// its own leaf callee inlinable, as the body it replaces had it
// (0x70b110's 0x4ac0d0).
if leaves.get(&t).is_some_and(|f| makes_no_calls(f) || (!copying && (in_loop || exp("SMALLCALLSALL")))) {
copies.insert(t);
stats.calls_leaf_copy += 1;
leaf_name(t)
@@ -83,12 +92,29 @@ pub fn leaf_name(entry: u64) -> String {
/// no measure: 0x72b390, 18 instructions with four compares, is 288. The
/// flags go when the copy is inlined, the caller's `need` folding the
/// return's flag store away.
pub fn is_small_leaf(f: &Function) -> bool {
/// A function none of whose blocks ends in a call.
fn makes_no_calls(f: &Function) -> bool {
!f.blocks.iter().any(|b| matches!(b.term, FTerm::Call { .. }))
}
pub fn is_small_copyable(f: &Function) -> bool {
const LEAF_INSTS: usize = 32;
// Also a small function making at most two calls that return to it
// (noita.exe 0x70b110, 0x70b050: a load and a virtual call): V8 never
// inlines those into 0x724580's outlined loop (726 wire bytes each, and
// the loop's 26 KB leaves it no inlining budget), where pewasm's
// TurboFan does. Opt-in (`GEX_CGEN_EXP=SMALLCALLS`: at calls in
// outlined loops; `SMALLCALLSALL`: everywhere; not in the unit key): the
// page ABBAs on edger (docs/noita-perf.md, i3b/i3c) found 2-3% less
// time in 0x724580's family and no world-fps change beyond noise, so
// the default copies leaves only.
let calls_ok = exp("SMALLCALLS") || exp("SMALLCALLSALL");
let mut n = 0;
let mut calls = 0;
for b in &f.blocks {
match b.term {
FTerm::Jump(_) | FTerm::Branch { .. } | FTerm::Return { .. } => {}
FTerm::Call { ret: Some(_), .. } if calls_ok && calls < 2 => calls += 1,
_ => return false,
}
n += b.insts.len();
@@ -162,6 +188,34 @@ pub fn ret_pops(functions: &[Function], class32: bool, read: &dyn Fn(u64) -> Opt
out
}
/// The calls in `functions` that read the ZF their caller set: MSVC's
/// stack checks, `cmp esi, esp` (3B F4 or 39 E6) right before `call
/// __chkesp` / `call [__imp___chkesp]` / `call _RTC_CheckEsp`, whose `jne`
/// reports a corrupt stack on a stale ZF (debug builds; stream W's games).
/// No ABI passes anything in the flags otherwise (`flags_abi`). By the
/// call's pc. Empty off x86.
pub fn zf_calls(functions: &[Function], read: &dyn Fn(u64) -> Option<u8>) -> HashSet<u64> {
let mut out = HashSet::new();
if !gex_abi::X86 {
return out;
}
for f in functions {
for b in &f.blocks {
if !matches!(b.term, FTerm::Call { .. }) || b.synthetic || b.insts.len() < 2 {
continue;
}
let prev = b.insts[b.insts.len() - 2];
if b.insts[b.insts.len() - 1] != b.term_pc || prev + 2 != b.term_pc {
continue;
}
if matches!((read(prev), read(prev + 1)), (Some(0x3b), Some(0xf4)) | (Some(0x39), Some(0xe6))) {
out.insert(b.term_pc);
}
}
}
out
}
/// The recursion-depth guard (`docs/function-tier.md` §7). `depth` is an
/// estimate, in bytes, of the engine stack the C frames below this one in
/// the nest use; a function whose own frame would take it past this budget
@@ -329,6 +383,9 @@ pub struct FnUnit {
leaf_copies: BTreeSet<u64>,
/// Emitting a copy: `static inline`, under `leaf_name`.
copying: bool,
/// Calls that read the ZF their caller set ([`zf_calls`]), by the
/// call's pc. Whoever adds a function fills in its entries.
pub zf_calls: HashSet<u64>,
}
impl Default for FnUnit {
@@ -412,28 +469,36 @@ impl FnUnit {
leaves: Default::default(),
leaf_copies: BTreeSet::new(),
copying: false,
zf_calls: HashSet::new(),
}
}
/// The copies of small leaves this unit's calls named ([`callee_name`]),
/// as `static inline` functions after its bodies.
pub fn add_leaf_copies(&mut self, known: &HashSet<u64>, sums: &HashMap<u64, Summary>, trapfree: &HashMap<u64, u32>) -> Result<()> {
let want = std::mem::take(&mut self.leaf_copies);
let leaves = std::sync::Arc::clone(&self.leaves);
let saved = std::mem::take(&mut self.stats);
self.copying = true;
let mut r = Ok(());
for a in &want {
if let Some(f) = leaves.get(a) {
if let Err(err) = self.add(f, known, sums, trapfree) {
r = Err(err);
break;
// A worklist: a calling copy may want a leaf's copy (`callee_name`).
let mut done = BTreeSet::new();
'all: loop {
let pending: Vec<u64> = self.leaf_copies.difference(&done).copied().collect();
if pending.is_empty() {
break;
}
for a in pending {
done.insert(a);
if let Some(f) = leaves.get(&a) {
if let Err(err) = self.add(f, known, sums, trapfree) {
r = Err(err);
break 'all;
}
}
}
}
self.copying = false;
self.stats = saved;
self.leaf_copies = want;
r
}
@@ -532,7 +597,7 @@ impl FnUnit {
e.resize(f.n_values as usize);
let mut regs = registers(f);
regs.clean_spills = clean_spills();
self.live = if live_writebacks() { live_in(f, sums) } else { vec![RegSet::ALL; f.blocks.len()] };
self.live = if live_writebacks() { live_in_with(f, sums, &self.zf_calls) } else { vec![RegSet::ALL; f.blocks.len()] };
self.carry = if regs.clean_spills { self.carried(f, known, sums, trapfree) } else { HashMap::new() };
let dirty = if regs.clean_spills { dirty_in(f, &self.carry) } else { Vec::new() };
let used: Vec<GuestReg> = regs.used.iter().copied().collect();
@@ -545,7 +610,11 @@ impl FnUnit {
let name = if self.copying { leaf_name(f.entry) } else { body_name(f.entry) };
// The body's head, kept aside: its loop functions go first.
let mut fhead = String::new();
let _ = writeln!(fhead, "{}int {name}(int ctx, int32_t depth, uint32_t need, uint64_t ret) {{", self.storage());
// A calling copy is too costly for clang's threshold at a call site
// of many (0x70b110 in 0x724580's loop): forced, as V8 would not
// inline it either.
let force = if self.copying && !makes_no_calls(f) { "__attribute__((always_inline)) " } else { "" };
let _ = writeln!(fhead, "{force}{}int {name}(int ctx, int32_t depth, uint32_t need, uint64_t ret) {{", self.storage());
for r in &used {
let _ = writeln!(fhead, " {} {};", emit::reg_cty(*r, self.class32), reg_name(*r));
}
@@ -673,6 +742,9 @@ impl FnUnit {
// dispatches on `ctx::PC` itself; nor has a whole-image unit, whose
// functions are reached through the function table.
if self.copying {
// A copy that calls (`is_small_copyable`) needs its callees
// declared like any body here.
self.protos.extend(calls);
return Ok(());
}
if self.ftab {
@@ -958,12 +1030,12 @@ impl FnUnit {
// itself.
(Callee::Indirect(_), Some(_)) if self.carry.contains_key(&i) => {
leave_wb = self.carry[&i];
let all = if flags_abi() { non_flags() } else { RegSet::ALL };
let all = if flags_abi() { call_reads(self.zf_calls.contains(&b.term_pc)) } else { RegSet::ALL };
all.minus(leave_wb)
}
// No callee reads the flags it is entered with
// (`flags_abi`).
_ if flags_abi() => non_flags(),
_ if flags_abi() => call_reads(self.zf_calls.contains(&b.term_pc)),
_ => RegSet::ALL,
};
// Dirty registers the callee never writes stay dirty across
@@ -1001,7 +1073,7 @@ impl FnUnit {
reload_all(e);
e.line(" } else");
}
let callee_c = callee_name(&self.leaves, self.copying, &mut self.leaf_copies, &mut self.stats, *t);
let callee_c = callee_name(&self.leaves, self.copying, scope.is_some(), &mut self.leaf_copies, &mut self.stats, *t);
if leave_wb.is_empty() {
e.line(format!(
" if (!{callee_c}(ctx, depth + {}, {need_back}, {ret_arg})) {fail}",
@@ -1041,7 +1113,7 @@ impl FnUnit {
} else {
String::new()
};
let callee_c = callee_name(&self.leaves, self.copying, &mut self.leaf_copies, &mut self.stats, k);
let callee_c = callee_name(&self.leaves, self.copying, scope.is_some(), &mut self.leaf_copies, &mut self.stats, k);
e.line(format!(" {{ const uint64_t ft = {t};"));
if carried.is_empty() {
e.line(format!(
@@ -1691,6 +1763,12 @@ pub fn live_writebacks() -> bool {
/// `longjmp`s to an earlier call's continuation in the same frame; C and
/// the ABIs leave them undefined there (docs/function-tier.md §5).
pub fn live_in(f: &Function, sums: &HashMap<u64, Summary>) -> Vec<RegSet> {
live_in_with(f, sums, &HashSet::new())
}
/// [`live_in`], with ZF read by the calls at `zf` (their `term_pc`s:
/// [`zf_calls`]).
pub fn live_in_with(f: &Function, sums: &HashMap<u64, Summary>, zf: &HashSet<u64>) -> Vec<RegSet> {
let n = f.blocks.len();
let mut live = vec![RegSet::EMPTY; n];
let order: Vec<usize> = f.rpo().iter().rev().map(|b| b.index()).collect();
@@ -1712,12 +1790,12 @@ pub fn live_in(f: &Function, sums: &HashMap<u64, Summary>) -> Vec<RegSet> {
..
} => match sums.get(t) {
Some(sm) => sm.inputs.union(live[r.to.index()]),
None if flags_abi() => non_flags().union(live[r.to.index()]),
None if flags_abi() => call_reads(zf.contains(&b.term_pc)).union(live[r.to.index()]),
None => RegSet::ALL,
},
// `flags_abi`: no callee reads the flags it is entered with.
FTerm::Call { ret, .. } if flags_abi() => {
non_flags().union(ret.as_ref().map_or(RegSet::EMPTY, |r| live[r.to.index()]))
call_reads(zf.contains(&b.term_pc)).union(ret.as_ref().map_or(RegSet::EMPTY, |r| live[r.to.index()]))
}
FTerm::TailCall { .. } if flags_abi() => non_flags(),
_ => RegSet::ALL,
@@ -1934,6 +2012,19 @@ pub fn devirt_carry() -> bool {
*ON.get_or_init(|| std::env::var("GEX_CGEN_WB_DEVIRT").as_deref() == Ok("1"))
}
/// What a call to a callee without a summary (indirect, outside the unit,
/// or unsummarised) reads of the caller's registers under `flags_abi`:
/// everything but the flags, and ZF at a stack check (`zf`, [`zf_calls`]).
/// `GEX_CGEN_EXP=ZFCALLS` gives every such call ZF (priced on edger at
/// -2.7%, gl2 in docs/noita-perf.md).
pub fn call_reads(zf: bool) -> RegSet {
if zf || exp("ZFCALLS") {
non_flags().union(RegSet::of(&[GuestReg::Z]))
} else {
non_flags()
}
}
pub fn non_flags() -> RegSet {
RegSet::ALL.minus(RegSet::of(FLAG_REGS))
}
@@ -2233,7 +2324,9 @@ b1 0x2005..0x2006 insts [2005]:
fn a_leaf_copy_names_its_outlined_loops_apart() {
let leaf = gex_ir::text::parse(LEAF).unwrap();
let caller = gex_ir::text::parse(CALLER).unwrap();
assert!(is_small_leaf(&leaf) && !is_small_leaf(&caller));
assert!(is_small_copyable(&leaf));
// One call that returns to it: copyable too with SMALLCALLS.
assert_eq!(is_small_copyable(&caller), exp("SMALLCALLS") || exp("SMALLCALLSALL"));
if !outline_loops() {
return;
}
+25 -1
View File
@@ -337,19 +337,39 @@ fn read_recovery(dir: &Path) -> Result<Recovery> {
pub fn function_c(recovery: &Path, entry: u64, read: &dyn Fn(u64) -> Option<u8>, icall_hints: &HashMap<u64, u64>) -> Result<String> {
let rec = read_recovery(recovery)?;
let known: HashSet<u64> = rec.entries.iter().copied().collect();
// The image's small leaves, as a whole-image unit has them, so the C
// names their copies (and holds them) as the unit's does.
let mut leaves = HashMap::new();
let mut leaf_pops = HashMap::new();
let mut leaf_zf = HashSet::new();
if func::leaf_copies() {
let mut gir = GirReader::open(recovery)?;
while let Some(f) = gir.next()? {
if func::is_small_copyable(&f) {
leaf_pops.extend(func::ret_pops(std::slice::from_ref(&f), rec.class32, read));
leaf_zf.extend(func::zf_calls(std::slice::from_ref(&f), read));
leaves.insert(f.entry, f);
}
}
}
let mut gir = GirReader::open(recovery)?;
while let Some(f) = gir.next()? {
if f.entry != entry {
continue;
}
let mut unit = FnUnit::new();
unit.leaves = std::sync::Arc::new(leaves);
unit.ret_pops = leaf_pops;
unit.zf_calls = leaf_zf;
unit.set_class32(rec.class32);
unit.ftab = true;
unit.touched = rec.touched.clone();
unit.guardfree = rec.guardfree.clone();
unit.icall_hints = std::sync::Arc::new(icall_hints.clone());
unit.ret_pops.extend(func::ret_pops(std::slice::from_ref(&f), rec.class32, read));
unit.zf_calls.extend(func::zf_calls(std::slice::from_ref(&f), read));
unit.add(&f, &known, &rec.sums, &rec.trapfree)?;
unit.add_leaf_copies(&known, &rec.sums, &rec.trapfree)?;
return Ok(unit.source());
}
bail!("no function at {entry:#x} in {}", recovery.display())
@@ -514,12 +534,14 @@ fn emit_and_link(
// their returns pop. A stub's body is not its function's: no copy.
let mut leaves = HashMap::new();
let mut leaf_pops = HashMap::new();
let mut leaf_zf = std::collections::HashSet::new();
if func::leaf_copies() {
for recovery in recoveries {
let mut gir = GirReader::open(recovery)?;
while let Some(f) = gir.next()? {
if !skip.contains(&f.entry) && func::is_small_leaf(&f) {
if !skip.contains(&f.entry) && func::is_small_copyable(&f) {
leaf_pops.extend(func::ret_pops(std::slice::from_ref(&f), rec.class32, read));
leaf_zf.extend(func::zf_calls(std::slice::from_ref(&f), read));
leaves.insert(f.entry, f);
}
}
@@ -535,6 +557,7 @@ fn emit_and_link(
unit.icall_hints = std::sync::Arc::clone(hints);
unit.leaves = std::sync::Arc::clone(&leaves);
unit.ret_pops = leaf_pops.clone();
unit.zf_calls = leaf_zf.clone();
unit
};
std::fs::create_dir_all(objects)?;
@@ -601,6 +624,7 @@ fn emit_and_link(
unit.add_leave_stub(f.entry);
} else {
unit.ret_pops.extend(func::ret_pops(std::slice::from_ref(&f), rec.class32, read));
unit.zf_calls.extend(func::zf_calls(std::slice::from_ref(&f), read));
unit.add(&f, &known, &rec.sums, &rec.trapfree)
.with_context(|| format!("emitting C for the function at {:#x}", f.entry))?;
}
+13
View File
@@ -40,6 +40,7 @@ const SOURCES: &[(&str, &str)] = &[
("debug_string", include_str!("../providers/debug_string.c")),
("crt_heap", include_str!("../providers/crt_heap.c")),
("nt_qpc", include_str!("../providers/nt_qpc.c")),
("nt_unixcall", include_str!("../providers/nt_unixcall.c")),
("msvcp120_thrd", include_str!("../providers/msvcp120_thrd.c")),
];
@@ -118,6 +119,18 @@ pub fn for_dll(dll: &str, class32: bool) -> Vec<Provider> {
.collect()
}
/// The library name the NT trampoline page has for providers
/// (`/* PROVIDES [nt-trampolines] ...`): the page is no PE image, so its
/// "exports" are the trampolines by name ([`trampoline_exports`]).
pub const TRAMPOLINES_DLL: &str = "[nt-trampolines]";
/// The NT trampoline page's entries providers may bind, by name, as offsets
/// from the page (gex_kernel::nt::boot::tramp): the i386 unix-call
/// dispatcher. `unix_call_32` is the page offset of `tramp::UNIX_CALL_32`.
pub fn trampoline_exports(unix_call_32: u32) -> HashMap<String, u32> {
HashMap::from([("unix_call_32".to_string(), unix_call_32)])
}
/// [`for_dll`] for an image whose file is `data`: without the `nowine`
/// providers when it is Wine's own build.
pub fn for_image(dll: &str, class32: bool, data: &[u8]) -> Vec<Provider> {
+4 -2
View File
@@ -568,7 +568,7 @@ pub fn build(dir: &Path, project: &Project, config: &Config) -> Result<PeReport>
// The web build's units, when the project has a browser runtime.
if dir.join(WEB_NT_DIR).join(WEB_NT_WASM[0]).exists() {
let stateful: Vec<(String, PathBuf)> = config.providers.iter().map(|p| (p.library.clone(), provider_wasm(dir, p))).collect();
build_web(dir, &ntss, project, &units, &web_inputs, &stateful, class32, jobs, &mut report)?;
build_web(dir, &ntss, project, config.args.as_deref(), &units, &web_inputs, &stateful, class32, jobs, &mut report)?;
std::env::set_var("GEX_CGEN_CACHE", &units);
}
let manifest = BuildManifest {
@@ -616,6 +616,8 @@ fn build_web(
dir: &Path,
ntss: &NtSession,
project: &Project,
// user/eject.json's "args", over the recorded ones (as `eject run`).
args: Option<&[String]>,
native: &Path,
inputs: &[(String, u64, Vec<u8>, Vec<u64>)],
stateful: &[(String, PathBuf)],
@@ -734,7 +736,7 @@ fn build_web(
("exe", s(&ntss.exe)),
("prefix", s(&ntss.prefix)),
("cwd", s(project.program.cwd.as_deref().unwrap_or("/"))),
("args", strs(&ntss.args)),
("args", strs(args.unwrap_or(&ntss.args))),
("env", strs(&ntss.env)),
("settings", strs(&settings)),
("gl", b(ntss.gl)),
+157 -45
View File
@@ -41,6 +41,51 @@ const GEX_GL_SUBMIT: u64 = 0xc008_6701;
/// waiting way.
const GEX_GL_SUBMIT_ASYNC: u64 = 0xc008_6704;
/// The ring's fast path (gex-cgen/providers/nt_unixcall.c): the unix-call
/// dispatcher's provider writes the hottest GL commands into the ring
/// itself, with no syscall. It needs the ring's address and its tail where
/// guest code reads them: two words at the end of KUSER_SHARED_DATA's page,
/// past the yield park's. [`GL_FAST_RING`] is 0 until the ring exists, and
/// stays 0 with `GEX_NT_GL_FAST=0` (every call then takes the syscall).
pub const GL_FAST_RING: u64 = super::USD + 0xfe0;
/// The ring's tail: the bytes of commands queued. [`TAIL_BUSY`] is its
/// lock: whoever sets it (the provider for one command, the layer for a
/// command or a submit) owns the ring until it stores the new tail.
pub const GL_FAST_TAIL: u64 = super::USD + 0xfe4;
pub const TAIL_BUSY: u32 = 1 << 31;
/// Whether the fast path is offered: the host sets it from `GEX_NT_GL_FAST`
/// (default on; `0` for A/B runs) before the program starts.
pub static FAST: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
fn fast_path() -> bool {
FAST.load(std::sync::atomic::Ordering::Relaxed)
}
/// Take the ring: its tail, with [`TAIL_BUSY`] set until [`ring_release`].
/// A provider holds it for the few stores of one command; another thread of
/// the layer for a command or a submit (a sync submit waits for the host).
fn ring_take(c: &mut Call<'_>) -> R<u32> {
let mut spins = 0u32;
loop {
let t = gex_atomic::load_u32(c.g.m.bytes, GL_FAST_TAIL).ok_or(Status(st::ACCESS_VIOLATION))?;
if t & TAIL_BUSY == 0 && gex_atomic::cas_u32(c.g.m.bytes, GL_FAST_TAIL, t, t | TAIL_BUSY) == Some(Ok(t)) {
return Ok(t);
}
spins += 1;
if spins < 64 {
std::hint::spin_loop();
} else {
std::thread::yield_now();
}
}
}
/// Give the ring back with `tail` bytes queued.
fn ring_release(c: &mut Call<'_>, tail: u32) {
let _ = gex_atomic::store_u32(c.g.m.bytes, GL_FAST_TAIL, tail);
}
/// A params struct: where each field of a unix call's arguments is, for the
/// process's ABI.
struct Params {
@@ -202,7 +247,6 @@ struct Ctx {
pub struct State {
ring: u64,
tail: u32,
dev: Option<Arc<dyn FileHandle>>,
host_made: bool,
contexts: HashMap<u64, Ctx>,
@@ -254,7 +298,6 @@ impl Default for State {
fn default() -> Self {
State {
ring: 0,
tail: 0,
dev: None,
host_made: false,
contexts: HashMap::new(),
@@ -306,6 +349,12 @@ fn open_device(c: &mut Call<'_>) -> R<()> {
for (i, w) in words.iter().enumerate() {
c.g.w32(ring + 4 * i as u64, *w)?;
}
// An empty ring, then (with the fast path) where it is: a provider
// that reads the address sees a tail that belongs to it.
c.g.w32(GL_FAST_TAIL, 0)?;
if fast_path() {
c.g.w32(GL_FAST_RING, ring as u32)?;
}
s = c.p.gl.lock().unwrap();
s.ring = ring;
s.dev = Some(dev);
@@ -366,28 +415,37 @@ fn cmd_with(c: &mut Call<'_>, opcode: u32, words: &[u32], tail: Tail<'_>) -> R<(
if len as u32 > CMD_CAPACITY {
return Err(c.unimplemented(&format!("a GL command of {len} bytes")));
}
// The room, taken under one lock (a flush first when the ring is full).
let reserve = |c: &mut Call<'_>| {
let mut s = c.p.gl.lock().unwrap();
if s.tail as usize + len > CMD_CAPACITY as usize {
return None;
// The room: the ring taken (`ring_take`) and held while the command is
// written, a flush first when it is full.
let reserve = |c: &mut Call<'_>| -> R<Option<(u64, u32)>> {
let at = ring_take(c)?;
if at as usize + len > CMD_CAPACITY as usize {
ring_release(c, at);
return Ok(None);
}
let at = s.tail;
s.tail += len as u32;
let mut s = c.p.gl.lock().unwrap();
if STATS.load(std::sync::atomic::Ordering::Relaxed) {
let who = s.serving;
s.stats.entry(who).or_insert((0, 0, 0, 0)).3 += len as u64;
}
Some((s.ring, at))
Ok(Some((s.ring, at)))
};
let (ring, at) = match reserve(c) {
let (ring, at) = match reserve(c)? {
Some(r) => r,
None => {
flush(c)?;
reserve(c).ok_or_else(|| c.unimplemented(&format!("a GL command of {len} bytes after a flush")))?
reserve(c)?.ok_or_else(|| c.unimplemented(&format!("a GL command of {len} bytes after a flush")))?
}
};
let base = ring + HEADER + at as u64;
let r = write_cmd(c, ring + HEADER + at as u64, opcode, words, tail, len);
ring_release(c, at + if r.is_ok() { len as u32 } else { 0 });
r
}
/// [`cmd_with`]'s writing, at `base` in the ring (taken).
fn write_cmd(c: &mut Call<'_>, base: u64, opcode: u32, words: &[u32], tail: Tail<'_>, len: usize) -> R<()> {
let tail_len = tail.len();
let padded = tail_len.div_ceil(4) * 4;
let head_len = 8 + 4 * words.len();
// On the stack for all but the widest commands.
let mut small = [0u8; 128];
@@ -427,25 +485,36 @@ fn cmd_with(c: &mut Call<'_>, opcode: u32, words: &[u32], tail: Tail<'_>) -> R<(
Ok(())
}
/// Submit what is queued; the reply, if any, follows.
fn flush(c: &mut Call<'_>) -> R<Vec<u8>> {
let (ring, tail, dev) = {
let s = c.p.gl.lock().unwrap();
(s.ring, s.tail, s.dev.clone())
};
let Some(dev) = dev else { return Ok(Vec::new()) };
if tail == 0 {
return Ok(Vec::new());
}
/// The device's submit `req` of the ring (taken, `tail` bytes queued): the
/// ioctl's result and its time.
fn submit_taken(c: &mut Call<'_>, dev: &Arc<dyn FileHandle>, ring: u64, tail: u32, req: u64) -> R<(Option<crate::errno::SysResult>, u64)> {
c.g.w32(ring + 8, tail)?;
c.g.w32(ring + 16, 0)?;
c.g.w32(ring + 20, 0)?;
let t0 = (c.p.clocks.mono_ns)();
let r = dev.ioctl(&mut c.g.m, GEX_GL_SUBMIT, ring);
let dt = (c.p.clocks.mono_ns)().saturating_sub(t0);
let r = dev.ioctl(&mut c.g.m, req, ring);
Ok((r, (c.p.clocks.mono_ns)().saturating_sub(t0)))
}
/// Submit what is queued; the reply, if any, follows.
fn flush(c: &mut Call<'_>) -> R<Vec<u8>> {
let (ring, dev) = {
let s = c.p.gl.lock().unwrap();
(s.ring, s.dev.clone())
};
let Some(dev) = dev else { return Ok(Vec::new()) };
// The ring is held through the submit: nothing is queued behind it
// while the host reads it.
let tail = ring_take(c)?;
if tail == 0 {
ring_release(c, 0);
return Ok(Vec::new());
}
let r = submit_taken(c, &dev, ring, tail, GEX_GL_SUBMIT);
ring_release(c, 0);
let (r, dt) = r?;
{
let mut s = c.p.gl.lock().unwrap();
s.tail = 0;
s.submits.0 += 1;
s.submits.1 += dt;
s.submits.2 += tail as u64;
@@ -466,27 +535,27 @@ fn flush(c: &mut Call<'_>) -> R<Vec<u8>> {
/// Submit what is queued without waiting for it to run ([`GEX_GL_SUBMIT_ASYNC`]).
/// A failure surfaces at the next submit, as that batch's.
fn flush_async(c: &mut Call<'_>) -> R<()> {
let (ring, tail, dev) = {
let (ring, dev) = {
let s = c.p.gl.lock().unwrap();
(s.ring, s.tail, s.dev.clone())
(s.ring, s.dev.clone())
};
let Some(dev) = dev else { return Ok(()) };
let tail = ring_take(c)?;
if tail == 0 {
ring_release(c, 0);
return Ok(());
}
c.g.w32(ring + 8, tail)?;
c.g.w32(ring + 16, 0)?;
c.g.w32(ring + 20, 0)?;
let t0 = (c.p.clocks.mono_ns)();
let r = dev.ioctl(&mut c.g.m, GEX_GL_SUBMIT_ASYNC, ring);
if r.is_none() {
let r = submit_taken(c, &dev, ring, tail, GEX_GL_SUBMIT_ASYNC);
if matches!(r, Ok((None, _))) {
// A device without it: the batch is still queued.
ring_release(c, tail);
flush(c)?;
return Ok(());
}
let dt = (c.p.clocks.mono_ns)().saturating_sub(t0);
ring_release(c, 0);
let (r, dt) = r?;
{
let mut s = c.p.gl.lock().unwrap();
s.tail = 0;
s.submits.0 += 1;
s.submits.1 += dt;
s.submits.2 += tail as u64;
@@ -544,23 +613,21 @@ pub fn take_stats(p: &super::Process, n: usize) -> String {
/// For a reply the size of a frame, which a host copy would allocate twice
/// a frame.
fn flush_in_place(c: &mut Call<'_>) -> R<(u64, usize)> {
let (ring, tail, dev) = {
let (ring, dev) = {
let s = c.p.gl.lock().unwrap();
(s.ring, s.tail, s.dev.clone())
(s.ring, s.dev.clone())
};
let Some(dev) = dev else { return Ok((0, 0)) };
let tail = ring_take(c)?;
if tail == 0 {
ring_release(c, 0);
return Ok((0, 0));
}
c.g.w32(ring + 8, tail)?;
c.g.w32(ring + 16, 0)?;
c.g.w32(ring + 20, 0)?;
let t0 = (c.p.clocks.mono_ns)();
let r = dev.ioctl(&mut c.g.m, GEX_GL_SUBMIT, ring);
let dt = (c.p.clocks.mono_ns)().saturating_sub(t0);
let r = submit_taken(c, &dev, ring, tail, GEX_GL_SUBMIT);
ring_release(c, 0);
let (r, dt) = r?;
{
let mut s = c.p.gl.lock().unwrap();
s.tail = 0;
s.submits.0 += 1;
s.submits.1 += dt;
s.submits.2 += tail as u64;
@@ -2358,4 +2425,49 @@ mod tests {
let p = Params { at: 0, offs: layout(sig, Abi::P64), n: sig.len(), sig };
assert_eq!(&p.offs[..5], &[0, 8, 12, 16, 24]);
}
/// gex-cgen/providers/nt_unixcall.c writes the hottest GL commands into
/// the ring itself: its constants must be this layer's.
#[test]
fn nt_unixcall_provider_constants() {
let src = include_str!("../../../../gex-cgen/providers/nt_unixcall.c");
let def = |name: &str| -> u64 {
let l = src.lines().find(|l| l.starts_with(&format!("#define {name} "))).unwrap_or_else(|| panic!("no #define {name}"));
let v = l[format!("#define {name} ").len()..].split_whitespace().next().unwrap().trim_end_matches('u');
if let Some(h) = v.strip_prefix("0x") { u64::from_str_radix(h, 16).unwrap() } else { v.parse().unwrap() }
};
assert_eq!(def("TR"), super::super::TRAMPOLINES);
assert_eq!(def("UNIX_CALL_NR"), super::super::gex_nr::UNIX_CALL as u64);
assert_eq!(def("GL_FAST_RING"), GL_FAST_RING);
assert_eq!(def("GL_FAST_TAIL"), GL_FAST_TAIL);
assert_eq!(def("TAIL_BUSY"), TAIL_BUSY as u64);
assert_eq!(def("RING_HEADER"), HEADER);
assert!(src.contains("#define CMD_CAPACITY (8u << 20)") && CMD_CAPACITY == 8 << 20);
assert_eq!(super::super::boot::tramp::UNIX_CALL_32, super::super::TRAMPOLINES + 0x60);
// The trampoline's bytes the provider's slow path follows.
let page = super::super::boot::trampoline_page();
assert_eq!(&page[0x6a..0x70], &[0xc2, 0x10, 0x00, 0xcd, 0x80, 0xc3]);
for (f, o, name, op) in [
("FN_glEnable", "OP_ENABLE", "glEnable", op::ENABLE),
("FN_glDisable", "OP_DISABLE", "glDisable", op::DISABLE),
("FN_glActiveTexture", "OP_ACTIVE_TEXTURE", "glActiveTexture", op::ACTIVE_TEXTURE),
("FN_glUseProgram", "OP_USE_PROGRAM", "glUseProgram", op::USE_PROGRAM),
("FN_glBindTexture", "OP_BIND_TEXTURE", "glBindTexture", op::BIND_TEXTURE),
("FN_glUniform1i", "OP_UNIFORM_1I", "glUniform1i", op::UNIFORM_1I),
("FN_glUniform2f", "OP_UNIFORM_2F", "glUniform2f", op::UNIFORM_2F),
] {
let code = def(f) as usize;
assert_eq!(table::FUNCS[code].0, name, "{f}");
assert_eq!(def(o), op as u64, "{o}");
// The command it writes is the one `hot_one` writes.
assert!(hot(code as u32).is_some(), "{name} is not a hot call here");
}
// i386 params: the TEB pointer, then 4-byte fields.
for sig in ["pi", "pii", "piff"] {
let offs = layout(sig, Abi::P32);
for (i, o) in offs[..sig.len()].iter().enumerate() {
assert_eq!(*o, 4 * i as u64, "{sig}");
}
}
}
}
+66
View File
@@ -831,6 +831,72 @@ and the compositor. Paths: project root `~/gex-sandbox`, scratch
multi-threaded loading. Then the compositor items only if a profile
puts the worker back on the critical path.
- **Post-merge check (site19, master 5ab54ab2 merged, keys unchanged: 38
units present, 0 built; p-noita17's user/eject.json now has `"args": []`,
which the web build honours since this commit's sibling):** ABBA on edger
ejected 57.2, 58.6 vs pewasm 46.0, 45.0 = **1.27x**, world-up 49 s after
the process start. Multi-threaded loading is the project's setting
(user/eject.json `args`), not an eject-site default: eject-site is
generic, and `--args` stays the override.
- Start-up profiles from the process start for both runtimes:
`$S/pw-prof-start.sh TAG AFTER SECS` (pewasm, F's browser-prof.mjs) and
`prof-edger.sh SITE TAG 1 60` (page-run now profiles Workers that start
during the window too).
- **Queued (user decision, after the start-up work): no WebGL in the
browser.** Port the Wayland compositor (web/src/compositor-worker.js, and
dom-compositor.js if still live) from WebGL2 to WebGPU on the GPUDevice
Mesa uses: GL windows become textures the compositor samples directly
(no cross-API copy, no second context, no webgl2 start-up retry, no
overlay canvas), wl_shm rows uploaded with queue.writeTexture from guest
memory; keep the frame-skip semantics (no present when nothing changed).
web/src/gl.js and the demos' WebGL path are being removed on master by
the coordinator.
- **Start-up profile from the process start (site19, prof-gst):** every
guest thread's Worker spent 2.5-3.7 s in `loadFile` (synchronous
requests through the service worker) plus 0.5-1.5 s compiling modules:
`loadUnit` fetched each unit's c.wasm (131 MiB for noita.exe) only to
read its entries, though the Module came from a sibling. Fix 065139bb:
the bytes are fetched lazily, only by a Worker that compiles. A/B
site19/site20 queued (`$S/edger-wab3.txt`).
- **Start-up: threads recompiled every unit (fixed, 35578368).** A
per-second timeline of the start-up profile (`$S/profline.mjs`,
`$S/profimg.mjs`) showed Noita's loader threads starting one after the
other, each busy ~3 s while the rest (main included) waited in the ConcRT
condvar provider: each new thread's Worker fetched and compiled all 39
units (352 `[unit]` lines in one run). host.mjs's thread start path
never sent the process's compiled modules (the `start` closure that did
is dead code). With them: 44 unit compiles per run. **World-up after
navigation (edger, A/B x3): site19 50.6-51.6 s -> site21 36.5-40.6 s**;
pewasm 30.2 s. ABBA (site21): ejected 56.8, 56.8 vs pewasm 48.3, 48.0 =
1.18x; world-up 35-36 s after the process start. The lazy c.wasm fetch
(065139bb) alone measured nothing; it matters now that threads have the
Modules (no bytes fetched at all).
- pewasm's own start-up timeline (`$S/wup-pewasm2.sh`, timestamped
console): process ~4.3 s, first surface 4.7 s, its input frames 60/120/180
at 10.6/11.0/11.3 s, world 29.2 s: about 6 s of init and 18 s of world
generation. Ours before the fix: init ~20 s (the serial thread starts),
world generation ~16 s.
- **Unit preload (a2… see log, eject page `?preload=`):** the page
compiles every unit with `compileStreaming` while the bundle downloads
and hands the Modules to every Worker (`preModules`); no Worker fetches
or compiles a unit (0 `[unit]` lines). World-up A/B x3: site21 37.5-38.5
s -> site22 35.6-38.8 s (mean 38.2 -> 36.7). pewasm 30.2 s.
- **Where the remaining ~6.5 s is (site21, per-second timeline, process
time):** boot 0-5 s on the main thread (now mostly gone with the
preload: 40% loadFile, 10% compile; 17% gex_key sha256 remains);
shader translation in the compositor worker (Tint SPIR-V->WGSL) at
9-12 s and 40-43 s, during which Noita's main thread waits on GL
(~3 s each), so the menus run at ~40 fps where pewasm's run at ~170
(its clicks at frames 60/120/180 land within 0.7 s, ours within ~3 s);
world generation 19-40 s on one thread, guest code plus spin-yields
(~19k NtYieldExecution per 10 s; 0x1d80 guest 7.6-9.9 s of 10), against
~18 s in pewasm. Levers: a persistent SPIR-V->WGSL cache (IndexedDB,
helps every load after the first), the shader translation off the
critical path, and world generation (F, codegen).
- Harness: `$S/play.mjs` = page-run.mjs + `--script FILE` (timed clicks,
keys, holds, screenshots, evals, `persist` snapshots and reloads through
CDP input, as a person would).
+255
View File
@@ -1385,3 +1385,258 @@ V8 profiles (`cmp.py`, ms of work per world frame, gb2 vs pb2):
- Total: 61 against 26 ms. Two cores are saturated, so preemption inflates
the main thread's per-function times (0x70b200 is 0.83 of pewasm's time on
the workers but 1.6 on the main thread).
### Stream F before the move (2026-10-04, night)
merge/fjit = master 7c84c09d (trunk; merge/main is gone) + b2bb3c95 (provider key fix, not in master yet)
+ 26e9f7bc, 857546ce, 0c8f9783, 916ed2c8.
**Item 2, the notify skip (26e9f7bc): correct, no gain.** The yield park
publishes its parked count at `YIELD_PARKED` (USD+0xff4); the ConcRT
provider's kick notifies only when it is nonzero. tests/providers (Noita's
CRT), browser NT sync/mtx 4/4 and mtx with Noita's CRT natively and in the
browser runtime pass. Page ABBA (`v8/abba-i2.out`): baseline 51.5/40.9,
change 50.7/50.6, pewasm 53.5/60.6/58.5. cond_notify and cs_unlock_ cost the
same before and after: real wakes of waiting threads, which pewasm pays too
(its ~159-byte notify function, 0.74 ms/frame).
**Item 3, inlining 0x70b110/0x70b050 into 0x724580 (0c8f9783).** Their calls
were direct already (devirtualised), but they are in the outlined loop
l_724580_0 (26 KB of wasm), and V8's `--trace-wasm-inlining` (harness:
`GEX_NT_CHROME_STDOUT=FILE GEX_NT_CHROME_FLAGS=--js-flags=--trace-wasm-inlining`)
says "not enough inlining budget" for both (726 wire bytes each). pewasm
gets them inlined by TurboFan, not in every run. Small functions with at
most two returning calls now get unit-private copies like small leaves,
`always_inline`, with leaf copies allowed inside them (0x4ac0d0). Without
always_inline (the first attempt, `v8/abba-i3.out`) clang refused them and
nothing changed: A 50.6/49.5, B 52.2/47.1. With it, noita.exe's unit has no
fl_70b110/fl_70b050 left (inlined). Native --cgen NT gate 60/60.
**Item 4 (direct import binding): dropped** (coordinator). About 45k
cross-image calls per world frame, all threads: an estimated 0.15-0.3 ms.
**Resume (stopped for the move):** the i3b Wine prefix units were not
finished (the cgen-prefix build was stopped); rerun it first:
`GEX_CGEN_CACHE=wt/fjit/target/bm/cgen GEX_CGEN_GUARD=1 scratchpad/fjit/gexc-i3b
cgen-prefix wt/fjit/target/bm/prefix --append --jobs 4` (from Noita's dir).
1. PENDING, never run: the i3b ABBA (part 2), once the GPU is free:
`scratchpad/fjit/v8/abba.sh i3b wt/fjit-a wt/fjit wt/fjit/target/bm wt/fjit/target/bm`
(A: wt/fjit-a = c4efc34a with main's shim plus the key fix; B: wt/fjit
with `scratchpad/fjit/shim-i3b.wasm` in web/public/gex). bm holds both
arms' units (keys differ). Check every leg's AOT line.
2. If it gains, the per-function table (`cmp.py`) for 0x724580's family;
if not, the V8 trace again on the new units.
### Stream F on gooner (2026-10-04/05; resume here)
Paths: project ~/gex-sandbox, scratch ~/gex-sandbox/scratch/fjit (old
`scratchpad/fjit`). No timing on gooner: every page number comes from edger
through scratch/bench/bench.sh, paired with pewasm on the same box.
- merge/fjit = master 4aed5829 (E's eject9 with b2bb3c95, msvcp120_thrd,
direct GL present, the compositor frame skip 40013fbe) + the GL fast
path (f7704244) + park sampling (760796a1) + ZF at stack checks
(8590e5a8). Assignment (coordinator,
2026-10-05): the main thread -- GL without the NT syscall, guest_yield,
NT-call overheads -- and pricing ZF at calls (W's _chkesp).
- gexc `wt/fjit/target/x86/release/gexc`; shim, kernel, seed in
wt/fjit/web/public/gex (`scratch/fjit/build-shims.sh`); Mesa replay copied
from worldmachine's wt/fjit (the one bm used); web/node_modules by `npm ci`.
- Caches (nice 19, up to 12 jobs, cg14's icall hints; `GEX_CGEN_EXP` is not
in the unit key, so arms are caches with the same keys):
`scratch/fjit/build-bm.sh wt/fjit/target/bm-g` (merge/fjit 6f5a538f =
master 4aed5829 + the GL fast path + the stack-check ZF; on gooner,
then synced to edger). A pricing arm:
`GEX_CGEN_EXP=... GEX_CGEN_MUTATE=... scratch/fjit/variant.sh wt/fjit/target/bm-d wt/fjit/target/NEW <Noita's images>`
(units hard-linked, the named images rebuilt, the prefix and Wine DLL
units shared; then link bm-d's prefix unit dirs into NEW, as done for i3c).
- edger: `scratch/fjit/e/sync.sh bm-c bm-a2 bm-b2` (worktree without
target/.git, caches without objects/recovery); runs:
`scratch/bench/bench.sh -- scratch/fjit/e/seq.sh NAME ~/gex-sandbox/wt/fjit 240 a=CACHE[@ENV] c=CACHE ...`
(ENV: that arm's extra page env, e.g. `@GEX_NT_GL_FAST=0`)
(pewasm, arms, pewasm, arms reversed, pewasm; 4C/8T; logs in
scratch/fjit/e/ and scratch/bench/fjit-* on edger; gooner keeps the
bench.sh output in scratch/fjit/e/*.out).
- pewasm profile on edger: `scratch/fjit/e/pw-prof.sh TAG` (under bench.sh);
per-function table: `scratch/fjit/v8/cmp.py --pw FRAMES pw*.cpuprofile --gex FRAMES gex*.cpuprofile`
(FRAMES = world frames in each 30 s profile window; profiles copied to
scratch/fjit/prof/).
- b2bb3c95 (provider key fix): its parent is in master; branch
`fjit/keyfix` points at it for the gate.
**i3b page ABBA on edger (2026-10-05, 4C/8T, seed pinned, merge/fjit
2dea8e82, one shim; A = `NOSMALLCALLS` in Noita's 8 images, B = 0c8f9783;
Wine DLL units shared):**
| leg | world fps |
|---|---|
| pewasm p1 | 45.7 |
| A a1 | 32.1 (AOT 99.1%) |
| B b1 | 34.4 (AOT 99.2%) |
| pewasm p2 | 46.7 |
| B b2 | 32.4 |
| A a2 | 32.5 |
| pewasm p3 | 47.6 |
A 32.3, B 33.4 (+3%, inside the ±3% leg noise); gex is 0.69-0.72 of
pewasm on edger (0.83 on worldmachine's 2 threads before the move). Worker
self time of the 0x724580 family over the 30 s profile (f_724580 +
l_724580_0 + 0x70b110 + 0x70b050 / fl_4ac0d0): A 22.4 and 20.1 s, B 21.1
and 19.6 s, so 3-6% less in the loop the change was for. Not slower, as
pewasm's non-leaf inlining was (1.4-1.7×), but noita.exe's unit grew from
165 to 234 MiB of wasm (107k calls to copies against 24k). Hence i3c: small
callers are copied only at calls inside an outlined loop function (the
budget-starved case); `GEX_CGEN_EXP=SMALLCALLSALL` keeps i3b's everywhere.
Also seen in the a1 profile: the compositor worker spends 23% of its time
in `texImage2D` and 51% in "(program)" on edger's P620; the main thread's
`guest_yield` is 20%, `#a` (gl-bridge) 11%. NtDuplicateObject/NtClose run at
~2.6k/s each (msvcp120 `_Thrd_current`; E's msvcp120_thrd provider covers
it in eject).
**i3c page sequence on edger (2026-10-05, merge/fjit c696cd07 + i3c, one
shim; A = no small-caller copies, C = copies at calls in outlined loops,
B = everywhere; Noita's 8 images per arm, Wine DLL units from C):**
| leg | world fps | 0x724580 family, worker self ms / 30 s |
|---|---|---|
| pewasm p1 | 46.7 | |
| A a1 | 35.0 | 20,981 |
| C c1 | 34.6 | 20,325 |
| B b1 | 34.0 | 20,441 |
| pewasm p2 | 47.7 | |
| B b2 | 34.7 | 20,370 |
| C c2 | 34.7 | 20,795 |
| A a2 | 34.1 | 21,424 |
| pewasm p3 | 48.6 | |
A 34.55, C 34.65, B 34.35 against pewasm 47.7 (gex 0.72×). The copies take
2-3% off their target loop and nothing measurable off the frame, so they
are **off by default** (the rule for this change: win in the page or be
reverted): `GEX_CGEN_EXP=SMALLCALLS` (loops only, noita.exe +0.6% wasm) or
`SMALLCALLSALL` (everywhere, +42%) for pricing. Every gex arm here is ~5%
above i3b's (32-33), where both arms ran i3b's everywhere-copied Wine DLL
units (prefix 356 MiB of wasm against 257 loop-only), with pewasm ~2% up
too: suggestive, not shown.
**Page profile on edger, gex i3c-a1 against pewasm pwprof1 (cmp.py, ms of
work per world frame over 30 s, all threads; `scratch/fjit/prof/cmp-edger-a1.txt`):**
guest code is level: gex 34.7 against pewasm 32.0 (0x724580 8.42 against
8.32, 1.01×; 0x70b110 0.87×, 0x70b200 1.58×, 0x70bdc0 1.42×). The gap is
outside guest code: total work 72.8 against 35.5.
- The compositor worker: 16.1 ms per frame, 7.9 of it `texImage2D` in
`#uploadGlCanvas` (web/src/compositor-worker.js:827), the WebGPU canvas
copied into a WebGL texture every composited frame (a readback on edger's
P620), plus createView/beginRenderPass/submit. The main thread waits for
it ("GL previous frame" 1.3-1.4 s per 10 s, about 4.5 ms per frame).
- The main game thread: 33.0 ms per frame against pewasm's busiest 13.8:
`guest_yield` 5.8, the gl-bridge's `#a` 5.2, mono_ns 1.7, NT syscalls.
Both are E's/R's areas (E1, E2), not codegen.
**Main thread, edger profile i3c-a1 (before E's direct present):** 31 s
busy of 31: `guest_yield` 5.4 s and gl-bridge `#a` 4.9 s are waits (a
parked yield; the previous frame's GL submit), not work. `gex_syscall` is
17.1 s inclusive: unix-call dispatch 7.4 (of it the swap's wait 5.1), the
yield 5.4, the syscall shell ~3.3 (imp::syscall 0.9, tick 0.8 of which
mono_ns JS calls, syscall_on 0.5, with_host 0.4, wasm-to-js 0.4, cgen
drain 0.3). NtDuplicateObject + NtClose cost 0.08 s in the layer. GL
census (GEX_NT_GL_STATS): 3,880 calls a frame, glDisable 620,
glActiveTexture 583, glUseProgram 557, glBindTexture 415, glEnable 411,
glUniform1i 291, glUniform2f 284, glEnable/DisableClientState 169 each.
**GL fast path (f7704244):** the provider `nt_unixcall` at the i386
unix-call trampoline writes those seven (81% of calls) into the ring;
the ring's tail is a guest word with a lock bit (USD+0xfe4), the address
at USD+0xfe0; `GEX_NT_GL_FAST=0` turns it off at run time (A/B on one
cache). tests/nt glfast; NT gate 75/75 with and without --cgen.
Client-state calls stay in the layer (their array flags are layer state).
**gl1 on edger (merge/fjit dd7df4db + master 5ab54ab2, bm-f, one cache,
arms by page env; before E's 40013fbe):** pewasm 45.9 / 46.5 / 45.4;
fast path off 39.0, 37.0; on 41.9, 37.9 (38.0 against 39.9, +5%, legs
drift ~5%). Main thread per 30 s profile: the unix-call path outside the
swap went from 6.0 s (both off legs) to 1.9 s (both on legs), about
3.6 ms a world frame; the time moved into the yield park (6.7/6.1 s ->
10.6/10.1 s): the main thread now waits on its workers instead. With the
fast path the main thread is (ms per world frame) park 8.9, guest 12.9,
shim work 2.5, JS 3.0 (gl-bridge collect 1.3, mono_ns 0.9 -- of it 0.45
the park's own timing, now sampled 1 in 16), providers 0.9. Workers 3/4
are busy 43% (0x724580 family), idle in cond_wait the rest.
**gl2 on edger (with E's compositor frame skip 40013fbe; arms off = fast
path off, on, zf = on with `GEX_CGEN_EXP=ZFCALLS` in Noita's images):**
| leg | world fps |
|---|---|
| pewasm p1 | 45.7 |
| off1 | 42.9 |
| on1 | 44.9 |
| zf1 | 45.2 |
| pewasm p2 | 44.9 |
| zf2 | 43.3 |
| on2 | 46.1 |
| off2 | 44.0 |
| pewasm p3 | 45.5 |
pewasm 45.4; off 43.45 (0.96×), **on 45.5 (1.00×: the normal gexc page
level with pewasm on edger)**, zf 44.25. The fast path is +4.7%. ZF at
every unsummarised call costs 2.7% (and 2.5% of noita.exe's wasm), so the
default writes ZF back only at MSVC's stack checks (`cmp esi, esp` right
before the call, 8590e5a8; tests/nt zfcall); ZFCALLS stays for pricing.
**g1 on edger (final tree 050023da, bm-g, master 4aed5829 inside):**
pewasm 48.2 / 45.8 / 47.4 (47.1); fast path off 44.2, 38.7 (41.5); on
43.3, 44.9 (44.1, 0.94× pewasm, +6% on off). Three sequences agree on the
fast path (+5%, +4.7%, +6%). NT gate 81/81 with and without --cgen.
Main thread with the fast path (ms per world frame, on1/on2): guest code
15.2/14.6, the yield park (waiting on the workers) 9.8/9.5, shim work
2.8, JS 1.4 (mono_ns 0.58: the QPC provider's clock and the syscall
tick), providers 1.0; the GL collect wait is gone (E's frame skip).
Next candidates, each under a millisecond a frame: gex_dispatch_miss 0.4
(tier-1 LuaJIT code), mono_ns 0.58, the client-state GL calls still in
the layer (338 a frame), dlmalloc 0.3.
### Stream F at the daemon restart (2026-10-05, afternoon; resume here)
merge/fjit = 8dbc35bf (queued for the gate) + 129c5720 (`gexc cgen-c`
emits the image's leaf copies, as the unit has them) + dbe79f05 (E's
35578368 cherry-picked: thread start carries compiled modules). 129c5720
touches gex-cgen, so units built before it have other keys.
**Worker threads are the critical path now** (the main thread parks ~9.6
ms/frame on them). Workers only, g1-on1 against pwprof1 (cmp.py, ms per
world frame): gex 13.2/13.1 work per worker against pewasm 10.8/10.8.
0x724580 1.03×, 0x70b110 0.95×, but 0x70b200 2.14×, 0x70bdc0 1.92×,
0x70c6e0 1.56×, 0x863fd0 1.49×, plus provider memset 0.58 and gex-only
0x864060/0x89c2d0/0x9a4210 (pewasm's unmapped pw#7960 etc. may be them).
Compiled C (`scratch/fjit/c/`): the guarded flavor is 25-27% more wasm
than the unguarded one for 0x70b200/0x70bdc0/0x70c6e0 (bounds guards:
select + lt_u per access). 0x70b200 calls seven trivial virtual leaves
(devirtualised, leaf copies): each call still stores six GPRs, reloads
seven and v0 (a v128, F5), and an atomic GEX_PATCHED load.
**N1 (unguarded units in the normal page) in progress.** The unguarded
noita.exe built fine this time (no unit-925 failure). Caches on gooner:
- `target/bm-hu` (GUARD=0): all 8 Noita images done; prefix stopped at
20/90 for the restart. Resume: `cd ~/gex-sandbox/scratch/fjit &&
GUARD=0 JOBS=12 ./build-bm.sh /home/claude/gex-sandbox/wt/fjit/target/bm-hu`
(cached images relink in seconds; the prefix continues from its objects).
- `target/bm-hud`: bm-hu with `GEX_CGEN_WB_DEVIRT=1` in noita.exe (done;
link bm-hu's prefix unit dirs into it once bm-hu finishes).
- `target/bm-h` (guarded, same tree): not started; objects and recovery
are there: `JOBS=12 ./build-bm.sh /home/claude/gex-sandbox/wt/fjit/target/bm-h`.
- The shims in wt/fjit/web/public/gex are from 129c5720 (they match these
keys); rebuild them (`scratch/fjit/build-shims.sh`) after any merge.
**Next:**
1. Finish bm-hu and bm-h, link prefix dirs into bm-hud, sync
(`scratch/fjit/e/sync.sh bm-h bm-hu bm-hud`), then on edger:
`scratch/bench/bench.sh -- scratch/fjit/e/seq.sh n1 ~/gex-sandbox/wt/fjit 240 g=$T/bm-h u=$T/bm-hu%&cgen=2&unguarded=1 ud=$T/bm-hud%&cgen=2&unguarded=1`
(seq.sh arms: `TAG=CACHE[@ENV][%FLAGS]`). If unguarded wins, make
`unguarded=1` the page default (web/src/nt-page.js) and the browser
build scripts GUARD=0.
2. Workers again (cmp.py on the n1 profiles): 0x70b200's call traffic
(trivial devirtualised leaves), then 0x70bdc0, 0x70c6e0.
3. Smaller main-thread items (each under 1 ms/frame): client-state GL in
the fast path (array flags into guest memory), mono_ns clock reads
(QPC provider, syscall tick), tier-1 dispatch misses.
Nothing running on gooner or edger for stream F.
+1 -1
View File
@@ -2427,7 +2427,7 @@ export class Gex {
if (mod) return mod;
const tl = performance.now();
got ??= this.#env.loadUnit?.(key) ?? null;
if (!got) throw new Error(`gex-cgen: unit ${key} was published but cannot be loaded here`);
if (!got?.wasm) throw new Error(`gex-cgen: unit ${key} was published but cannot be loaded here`);
const t0 = performance.now();
mod = got.wasm instanceof WebAssembly.Module ? got.wasm : new WebAssembly.Module(got.wasm);
this.#compileMs += performance.now() - t0;
+10
View File
@@ -392,6 +392,15 @@ export class ProcessHost {
shim: shimMod,
guest: this.#guest,
kernel: this.#kernel ?? undefined,
// A thread joins its process: every module the process's Workers
// have compiled so far, so it does not fetch and compile them all
// again (each Noita thread spent ~3 s doing that, one after the
// other, at start-up). The `start` closure above meant to send
// these, but this is the path that runs. A fork has its own memory
// and registry, so only threads (pid `proc:tid`).
...(msg.kind === 'thread'
? { sharedModules: [...(this.#moduleCache.get(String(pid).split(':')[0])?.entries() ?? [])] }
: {}),
}, forkTransfer(msg.child));
// Timed separately: the fork-to-adopt gap is 549ms and unaccounted --
// this `postMessage` (the 15.4 MiB image copy) or the closure below.
@@ -436,6 +445,7 @@ export class ProcessHost {
if (!cache) this.#moduleCache.set(proc, (cache = new Map()));
if (typeof m.key === 'string' && !cache.has(m.key)) {
cache.set(m.key, m.mod);
if (this.#extra?.shareTrace) console.warn(`[share] host cached ${m.key.slice(0, 30)} from ${pid} (${cache.size} for process ${proc})`);
for (const [key, other] of this.#workers) {
if (other !== w && String(key).split(':')[0] === proc) {
other.postMessage({ kind: 'sharedModule', key: m.key, mod: m.mod });
+17 -3
View File
@@ -348,9 +348,21 @@ async function handle(msg) {
// with a synchronous request -- allowed in a Worker, and what lets the
// nested callback loop install one too. Null when there is none.
loadUnit(key) {
const wasm = this.loadFile(`${key}/c.wasm`);
const entries = wasm && this.loadFile(`${key}/c.entries`);
return wasm && entries ? { wasm, entries } : null;
// The entries now; the module's bytes only if this Worker compiles it
// (most get the Module from a sibling: see `Gex#cgenModule`). Fetching
// every unit's bytes in every thread's Worker, only to use a shared
// Module, cost each new guest thread seconds at start-up.
const entries = this.loadFile(`${key}/c.entries`);
if (!entries) return null;
const env = this;
let wasm;
return {
entries,
get wasm() {
wasm ??= env.loadFile(`${key}/c.wasm`);
return wasm;
},
};
}
// Any file of the unit cache by its relative path (an export
// provider's module: `providers/<name>-<fingerprint>.wasm`), or null.
@@ -414,6 +426,8 @@ async function handle(msg) {
if (msg.cgen?.mode) gex.useCgen(msg.cgen.mode);
// Any sibling modules the host already had when this Worker started.
for (const [key, mod] of msg.sharedModules ?? []) gex.addSharedModule(key, mod);
for (const [key, mod] of msg.preModules ?? []) gex.addSharedModule(key, mod);
if (globalThis.GEX_SHARE_TRACE || msg.shareTrace) console.warn(`[share] ${msg.kind} start with ${(msg.sharedModules ?? []).length} shared modules (${(msg.sharedModules ?? []).filter(([k]) => String(k).startsWith('cgen:')).length} units)`);
// The GL device (gles-gears): the page's GlDevice, reached over a ring in a
// SharedArrayBuffer. `useGl` sets the flag `gex_set_gl` reads at load, so it
+22
View File
@@ -280,6 +280,11 @@ fn place(image: &gex_ld::Image, path: &str, base: u64, optional: bool, pe: Optio
}
}
let (key, mut providers) = unit_key(image, pe, base);
// The trampoline page's providers (gex-cgen/providers/nt_unixcall.c),
// bound at its entries after its unit, as an image's at its exports.
if pe.is_none() && base == gex_kernel::nt::TRAMPOLINES && image.class32 {
providers.extend(trampoline_providers());
}
if let Some(pe) = pe {
let dll = pe.exports.dll_name.to_ascii_lowercase();
for (_, file) in lock(&STATEFUL).iter().filter(|(d, _)| *d == dll) {
@@ -373,6 +378,23 @@ fn unit_key(image: &gex_ld::Image, pe: Option<&gex_ld::pe::PeImage>, base: u64)
(key, provided)
}
/// The NT trampoline page's providers, as [`unit_key`] gives an image's.
fn trampoline_providers() -> Vec<Provided> {
use gex_kernel::nt::{boot::tramp, TRAMPOLINES};
let exports = gex_cgen::provider::trampoline_exports((tramp::UNIX_CALL_32 - TRAMPOLINES) as u32);
gex_cgen::provider::for_dll(gex_cgen::provider::TRAMPOLINES_DLL, true)
.iter()
.map(|p| Provided {
file: format!("providers/{}-{}.wasm", p.name, gex_cgen::provider::fingerprint(std::slice::from_ref(p))),
binds: p.binds.iter().filter_map(|(f, e)| exports.get(e).map(|&rva| (f.clone(), TRAMPOLINES + rva as u64))).collect(),
stateful: None,
data: p.stateful,
skeleton: None,
})
.filter(|p| !p.binds.is_empty())
.collect()
}
/// Where unit `u`'s indirect-call profile is in the unit cache
/// (`icall/<identity>.txt`), for JS to read.
#[no_mangle]
+20 -4
View File
@@ -317,6 +317,7 @@ mod imp {
GL_READBACK.store(flag("GEX_NT_GL_READBACK"), Ordering::Relaxed);
GL_STATS.store(flag("GEX_NT_GL_STATS"), Ordering::Relaxed);
nt::opengl::STATS.store(flag("GEX_NT_GL_STATS"), Ordering::Relaxed);
nt::opengl::FAST.store(var("GEX_NT_GL_FAST").as_deref() != Some("0"), Ordering::Relaxed);
TRAP_PRINT.store(flag("GEX_NT_TRAP_PRINT"), Ordering::Relaxed);
if let Some(n) = var("GEX_NT_BIG_ALLOC").and_then(|v| v.parse::<u64>().ok()) {
crate::BIG_ALLOC.store(n, Ordering::Relaxed);
@@ -679,6 +680,10 @@ mod imp {
pub(crate) static YIELD_PARKS: [std::sync::atomic::AtomicU64; 3] = [const { std::sync::atomic::AtomicU64::new(0) }; 3];
pub(crate) static YIELD_KICKS: [std::sync::atomic::AtomicU64; 2] = [const { std::sync::atomic::AtomicU64::new(0) }; 2];
/// And the time spent in them, by result.
thread_local! {
/// This thread's parks, for sampling their time.
static PARK_N: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
pub(crate) static YIELD_PARK_NS: [std::sync::atomic::AtomicU64; 3] = [const { std::sync::atomic::AtomicU64::new(0) }; 3];
/// Exceptions dispatched nested and escaped back (the stats line).
pub(crate) static EXC_NESTED: AtomicU64 = AtomicU64::new(0);
@@ -829,11 +834,22 @@ mod imp {
fn park(&self, addr: u64, expected: u32, timeout: std::time::Duration) -> bool {
let p = std::ptr::with_exposed_provenance_mut::<i32>(addr as usize);
// SAFETY: an aligned guest word of the shared memory.
let t0 = mono_raw();
// Timed one park in SAMPLE, counted SAMPLE times: two clock
// reads (JS calls) per park were half a millisecond of every
// world frame on Noita's main thread (edger).
let n = PARK_N.with(|c| {
let n = c.get().wrapping_add(1);
c.set(n);
n
});
let timed = n % SAMPLE == 0;
let t0 = if timed { mono_raw() } else { 0 };
let r = unsafe { crate::warch::memory_atomic_wait32(p, expected as i32, timeout.as_nanos() as i64) };
if let Some(n) = YIELD_PARKS.get(r as usize) {
n.fetch_add(1, Ordering::Relaxed);
YIELD_PARK_NS[r as usize].fetch_add(mono_raw().saturating_sub(t0), Ordering::Relaxed);
if let Some(k) = YIELD_PARKS.get(r as usize) {
k.fetch_add(1, Ordering::Relaxed);
if timed {
YIELD_PARK_NS[r as usize].fetch_add(mono_raw().saturating_sub(t0) * SAMPLE, Ordering::Relaxed);
}
}
true
}
+1
View File
@@ -273,6 +273,7 @@ fn start_ticker(memory: gex_mem::GuestMemory, p: std::sync::Weak<nt::Process>, s
// GEX_NT_GL_STATS=1: the program's GL by function, with the 60 s report.
let gl_stats = std::env::var("GEX_NT_GL_STATS").as_deref() == Ok("1");
nt::opengl::STATS.store(gl_stats, std::sync::atomic::Ordering::Relaxed);
nt::opengl::FAST.store(std::env::var("GEX_NT_GL_FAST").map_or(true, |v| v != "0"), std::sync::atomic::Ordering::Relaxed);
let peek = std::env::var("GEX_NT_PEEK").ok().and_then(|v| {
let (addr, deref) = match v.strip_suffix('*') {
Some(a) => (a, true),
+17
View File
@@ -1034,6 +1034,23 @@ fn install_trampolines(store: &mut Store<HostState>, class32: bool) -> Result<()
};
let module = compile(&engine, cache.as_ref(), &path, &cwasm)?;
crate::ftab::publish(&shared, &memory, module, &entries, gex_kernel::ftab::Tier::Unit, Vec::new())?;
// Its providers (gex-cgen/providers/nt_unixcall.c), after the unit, at
// the trampolines they replace.
if class32 {
use gex_kernel::nt::{boot::tramp, TRAMPOLINES};
let exports = gex_cgen::provider::trampoline_exports((tramp::UNIX_CALL_32 - TRAMPOLINES) as u32);
for prov in gex_cgen::provider::for_dll(gex_cgen::provider::TRAMPOLINES_DLL, true) {
let (path, slots) = gex_cgen::provider::build(&prov, &cache_root()?)?;
let pairs = gex_cgen::provider::bindings(&prov, &slots, &exports, TRAMPOLINES, trace());
if pairs.is_empty() {
continue;
}
let module = Module::from_file(&engine, &path).with_context(|| format!("compiling provider {}", prov.name))?;
crate::aot_share::note(&module, crate::aot_share::Kind::Provider);
crate::ftab::publish_pairs(&shared, &memory, module, slots.len() as u32, &pairs, gex_kernel::ftab::Tier::Unit)?;
eprintln!("gex-nt: provider {} for the NT trampolines: {} entries", prov.name, pairs.len());
}
}
crate::ftab::catch_up(&mut *store)?;
catch_up_table(store)?;
if trace() {
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+4 -1
View File
@@ -217,7 +217,10 @@ export async function openPage(query, { width = 1280, height = 900, shotDir = nu
...(process.env.GEX_NT_CHROME_FLAGS ? process.env.GEX_NT_CHROME_FLAGS.split(' ') : []),
'--enable-logging=stderr', '--disable-hang-monitor', '--no-first-run', '--no-default-browser-check', '--remote-allow-origins=*',
`--remote-debugging-port=${CHROME_PORT}`, `--user-data-dir=${join(STATE, 'profile')}`, 'about:blank',
], { detached: true, stdio: ['ignore', 'ignore', openSync(join(STATE, 'chrome.log'), 'w')] });
], { detached: true, stdio: ['ignore',
// V8's traces (GEX_NT_CHROME_FLAGS=--js-flags=--trace-wasm-inlining) print to stdout.
process.env.GEX_NT_CHROME_STDOUT ? openSync(process.env.GEX_NT_CHROME_STDOUT, 'w') : 'ignore',
openSync(join(STATE, 'chrome.log'), 'w')] });
children.push(chrome);
let ws = null;
for (let i = 0; i < 80 && !ws; i++) {
+5
View File
@@ -131,6 +131,11 @@ for arch in x64 x86; do
check $arch/fixedbase-taken 0 "$allok" "$b/fixedbase.exe" taken
# opengl32 through the generated transport (tools/gen_gl_compat.py).
check_gl $arch/glcompat 0 "$allok" "$b/glcompat.exe"
# The GL calls the unix-call dispatcher's provider writes into the ring
# itself (i386; x64 makes them all through the layer).
check_gl $arch/glfast 0 "$allok" "$b/glfast.exe"
# A callee reading the ZF its caller set (MSVC's debug stack check).
check $arch/zfcall 0 "$allok" "$b/zfcall.exe"
# A window that paints and runs a message loop (tests/nt/browser-gui.mjs shows it).
check $arch/window 0 "shown${nl}bye" "$b/window.exe" 0
# Code written, rewritten and reallocated at run time (a JIT).
+129
View File
@@ -0,0 +1,129 @@
/* The GL calls the unix-call dispatcher's provider writes into the command
* ring itself (gex-cgen/providers/nt_unixcall.c): glEnable, glDisable,
* glActiveTexture, glUseProgram, glBindTexture, glUniform1i, glUniform2f.
* Each is checked by a query (which the NT layer answers after a submit),
* interleaved with calls the layer encodes, and many enough of them to fill
* the ring several times over. Needs `gexc run --gl`; meaningful with
* --cgen (the provider binds with the trampolines' unit).
* gex-nt-cc: clang --target=ARCH-w64-mingw32 -fuse-ld=lld -lgdi32 -lopengl32 */
#include <windows.h>
#include <GL/gl.h>
#include <stdio.h>
#define GL_TEXTURE0 0x84C0
#define GL_ACTIVE_TEXTURE 0x84E0
#define GL_CURRENT_PROGRAM 0x8B8D
static int failures;
#define CHECK(name, cond) do { int ok_ = (cond); printf("%s %s\n", ok_ ? "ok" : "FAIL", name); if (!ok_) failures++; } while (0)
#define PROC(ret, name, args) typedef ret (WINAPI *name##_t) args; static name##_t p##name;
PROC(void, glActiveTexture, (GLenum))
PROC(GLuint, glCreateShader, (GLenum))
PROC(void, glShaderSource, (GLuint, GLsizei, const char *const *, const GLint *))
PROC(void, glCompileShader, (GLuint))
PROC(GLuint, glCreateProgram, (void))
PROC(void, glAttachShader, (GLuint, GLuint))
PROC(void, glLinkProgram, (GLuint))
PROC(void, glUseProgram, (GLuint))
PROC(GLint, glGetUniformLocation, (GLuint, const char *))
PROC(void, glUniform1i, (GLint, GLint))
PROC(void, glUniform2f, (GLint, float, float))
PROC(void, glGetUniformfv, (GLuint, GLint, float *))
PROC(void, glGetUniformiv, (GLuint, GLint, GLint *))
#define LOAD(name) p##name = (name##_t)wglGetProcAddress(#name)
static LRESULT CALLBACK proc(HWND w, UINT m, WPARAM wp, LPARAM lp) { return DefWindowProcW(w, m, wp, lp); }
int main(void) {
WNDCLASSW wc = {0};
wc.style = CS_OWNDC;
wc.lpfnWndProc = proc;
wc.hInstance = GetModuleHandleW(0);
wc.lpszClassName = L"gex_glfast";
RegisterClassW(&wc);
HWND w = CreateWindowExW(0, L"gex_glfast", L"glfast", WS_POPUP, 0, 0, 64, 64, 0, 0, wc.hInstance, 0);
HDC dc = GetDC(w);
PIXELFORMATDESCRIPTOR pfd = {sizeof pfd, 1, PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, PFD_TYPE_RGBA, 32};
SetPixelFormat(dc, ChoosePixelFormat(dc, &pfd), &pfd);
HGLRC gl = wglCreateContext(dc);
CHECK("a context", gl && wglMakeCurrent(dc, gl));
glEnable(GL_BLEND);
CHECK("glEnable", glIsEnabled(GL_BLEND));
glDisable(GL_BLEND);
CHECK("glDisable", !glIsEnabled(GL_BLEND));
GLuint tex[2];
glGenTextures(2, tex);
glBindTexture(GL_TEXTURE_2D, tex[1]);
GLint b = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &b);
CHECK("glBindTexture", b == (GLint)tex[1]);
LOAD(glActiveTexture); LOAD(glCreateShader); LOAD(glShaderSource); LOAD(glCompileShader);
LOAD(glCreateProgram); LOAD(glAttachShader); LOAD(glLinkProgram); LOAD(glUseProgram);
LOAD(glGetUniformLocation); LOAD(glUniform1i); LOAD(glUniform2f); LOAD(glGetUniformfv); LOAD(glGetUniformiv);
CHECK("GL 2.0 entry points", pglActiveTexture && pglUseProgram && pglUniform2f);
if (!pglActiveTexture || !pglUseProgram || !pglUniform2f) return 1;
pglActiveTexture(GL_TEXTURE0 + 3);
GLint at = 0;
glGetIntegerv(GL_ACTIVE_TEXTURE, &at);
CHECK("glActiveTexture", at == GL_TEXTURE0 + 3);
pglActiveTexture(GL_TEXTURE0);
const char *vs = "#version 110\nvoid main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }";
const char *fs = "#version 110\nuniform vec2 off; uniform int k; void main() { gl_FragColor = vec4(off, float(k), 1.0); }";
GLuint v = pglCreateShader(0x8B31), fr = pglCreateShader(0x8B30), prog = pglCreateProgram();
pglShaderSource(v, 1, &vs, 0);
pglShaderSource(fr, 1, &fs, 0);
pglCompileShader(v);
pglCompileShader(fr);
pglAttachShader(prog, v);
pglAttachShader(prog, fr);
pglLinkProgram(prog);
pglUseProgram(prog);
/* (GL_CURRENT_PROGRAM answers the host's name for it, which the layer
* does not map back: the uniforms below show the program is in use.) */
GLint cur = 0;
glGetIntegerv(GL_CURRENT_PROGRAM, &cur);
CHECK("glUseProgram", cur != 0 && glGetError() == GL_NO_ERROR);
GLint off = pglGetUniformLocation(prog, "off"), s = pglGetUniformLocation(prog, "k");
pglUniform2f(off, 0.25f, -1.5f);
pglUniform1i(s, 5);
float fv[2] = {0, 0};
GLint iv = 0;
pglGetUniformfv(prog, off, fv);
pglGetUniformiv(prog, s, &iv);
CHECK("glUniform2f", fv[0] == 0.25f && fv[1] == -1.5f);
CHECK("glUniform1i", iv == 5);
/* Two million fast calls (some 26 MB of commands: the ring fills and is
* submitted three times), with layer-encoded calls among them, ending in
* a known state. */
for (int i = 0; i < 1000000; i++) {
glEnable(GL_BLEND);
if ((i & 4095) == 0) glClearColor(0, 0, 0, 1);
glDisable(GL_BLEND);
}
for (int i = 0; i < 1000; i++) {
glBindTexture(GL_TEXTURE_2D, tex[i & 1]);
pglUniform2f(off, (float)i, 2.0f);
pglUniform1i(s, i);
}
glEnable(GL_DEPTH_TEST);
glGetIntegerv(GL_TEXTURE_BINDING_2D, &b);
pglGetUniformfv(prog, off, fv);
pglGetUniformiv(prog, s, &iv);
CHECK("after the ring filled: the last of each", !glIsEnabled(GL_BLEND) && glIsEnabled(GL_DEPTH_TEST) &&
b == (GLint)tex[1] && fv[0] == 999.0f && fv[1] == 2.0f && iv == 999);
GLenum err = glGetError();
CHECK("no GL error", err == GL_NO_ERROR);
if (err) printf(" error %#x; blend %d depth %d texture %d off %g,%g s %d\n", err, glIsEnabled(GL_BLEND), glIsEnabled(GL_DEPTH_TEST), b, fv[0], fv[1], iv);
wglMakeCurrent(0, 0);
wglDeleteContext(gl);
printf("%d failures\n", failures);
return failures ? 1 : 0;
}
+48
View File
@@ -0,0 +1,48 @@
/* A callee that reads the ZF its caller set, as MSVC's debug-CRT stack
* check does (`cmp esi, esp; call [__imp___chkesp]`, whose `jne` reports a
* bad stack): the function tier must write ZF back at an indirect call. */
#include <stdio.h>
static int failures;
#define CHECK(name, cond) do { int ok_ = (cond); printf("%s %s\n", ok_ ? "ok" : "FAIL", name); if (!ok_) failures++; } while (0)
__attribute__((noinline)) static int readzf(void) {
int r;
__asm__ volatile("setz %%al\n\tmovzbl %%al, %0" : "=r"(r) : : "eax", "cc");
return r;
}
static int (*volatile fp)(void) = readzf;
/* As MSVC emits it around a call through a pointer in a debug build: esi
* holds esp from before the call, and `cmp esi, esp; call __chkesp`
* follows; here esi is off by `skew` bytes when it should not match. */
__attribute__((noinline)) static int equal(long skew) {
int (*f)(void) = fp;
int r;
#ifdef __i386__
__asm__ volatile("push %%esi\n\t"
"mov %%esp, %%esi\n\t"
"add %2, %%esi\n\t"
"cmp %%esp, %%esi\n\t"
"call *%1\n\t"
"pop %%esi"
: "=a"(r)
: "r"(f), "r"(skew)
: "ecx", "edx", "cc", "memory");
#else
r = skew == 0;
(void)f;
#endif
return r;
}
int main(int argc, char **argv) {
(void)argv;
int n = 0;
for (int i = 0; i < 100000; i++) {
n += equal(0) + 2 * equal(argc * 4);
}
CHECK("the callee sees the caller's ZF", n == 100000);
printf("%d failures\n", failures);
return failures != 0;
}
+10
View File
@@ -83,6 +83,7 @@ walk(join(project, 'runtime/web/page'), (p, rel) => place(p, rel));
const cgen = nt.cgen;
const units = join(project, cgen);
let unitBytes = 0;
const preload = [];
// Only this build's units: a unit cache keeps every key it ever built
// (a re-keyed rebuild leaves the old ones). build/build.json names the
// images' units; the NT trampolines' unit is not an image, and is kept when
@@ -108,8 +109,17 @@ walk(units, (p, rel) => {
if (!want) return;
unitBytes += statSync(p).size;
place(p, join('project', cgen, rel));
// What the page compiles while the bundle downloads (nt.html ?preload=):
// module names as gex.mjs keys them.
const url = `/project/${cgen}/${rel}`;
if (name === 'c.wasm') preload.push([`cgen:${rel.slice(0, -'/c.wasm'.length)}`, url]);
else if (top === 'providers') preload.push([`cgenfile:${rel}`, url]);
});
// Largest first: noita.exe's unit is most of the compile.
preload.sort((a, b) => (files[b[1]]?.size ?? 0) - (files[a[1]]?.size ?? 0));
writeFileSync(join(out, 'project', cgen, 'preload.json'), JSON.stringify(preload));
// The bundle, without what --drop names, with --overlay DIR's files (guest
// paths under DIR, as user/files) laid over it.
const drops = opts('--drop');
+3 -1
View File
@@ -77,6 +77,8 @@
cgenurl: `/project/${m.cgen}`,
size: q.get('size') ?? m.size ?? fit,
aot: '1',
// Compile the units while the bundle unpacks (eject-site's list).
preload: `/project/${m.cgen}/preload.json`,
});
if (m.cwd && m.cwd !== '/') p.set('cwd', m.cwd);
if (m.unguarded) p.set('unguarded', '1');
@@ -87,7 +89,7 @@
if (m.args?.length) p.set('argv', JSON.stringify(m.args));
if (m.gl) { p.set('gl', '1'); p.set('glbackend', 'wgpu'); }
// Diagnostics for the page itself, passed through.
for (const k of ['glpresent', 'mesaenv', 'trapstack']) if (q.has(k)) p.set(k, q.get(k));
for (const k of ['glpresent', 'mesaenv', 'trapstack', 'sharetrace']) if (q.has(k)) p.set(k, q.get(k));
location.replace(`nt.html?${p}`);
} catch (e) {
fail(String(e?.message ?? e));
+31
View File
@@ -72,6 +72,31 @@ async function fetchShared(url) {
return out;
}
/** Compile the units a preload list names, a few at a time, streaming from
* the network (or the service worker's cache); null if the list is gone. */
async function preloadUnits(url) {
let list;
try {
list = await (await fetch(url)).json();
} catch {
return null;
}
const out = [];
let i = 0;
const one = async () => {
while (i < list.length) {
const [name, href] = list[i++];
try {
out.push([name, await WebAssembly.compileStreaming(fetch(href))]);
} catch (e) {
console.warn(`[nt-page] preload ${href}: ${e?.message ?? e}`);
}
}
};
await Promise.all([one(), one(), one(), one()]);
return out;
}
async function main() {
// `?trapstack=1`: a trap's wasm frames, which name the shim function it
// was in (runtime.js passes the switch to every Worker).
@@ -96,6 +121,9 @@ async function main() {
canvas.style.height = `${h}px`;
state.phase = 'fetching';
mark('fetching');
// `preload=URL`: the units to compile while the bundle downloads (a JSON
// list of [module name, url]); every Worker then starts with them.
const preloading = q.get('preload') ? preloadUnits(q.get('preload')) : Promise.resolve(null);
// `tar=a.tar,b.tar`: bundles laid over one another, later ones winning --
// a small test program over a large cached bundle of its libraries.
const trees = [];
@@ -142,6 +170,8 @@ async function main() {
if (!env.some((e) => e.startsWith('GEX_WINE_PREFIX='))) env.push('GEX_WINE_PREFIX=/wine');
if (!env.some((e) => e.startsWith('GEX_NT_STRICT='))) env.push('GEX_NT_STRICT=1');
const gl = q.get('gl') === '1';
const preModules = await preloading;
if (preModules) mark(`compiled ${preModules.length} unit modules ahead`);
state.phase = 'running';
mark('running');
if (persistDir) {
@@ -167,6 +197,7 @@ async function main() {
wl: canvas,
wlInput: canvas,
hostX: q.get('hostx') === '1',
preModules,
});
state.status = status;
state.phase = 'done';
+7
View File
@@ -440,6 +440,10 @@ export async function runProgram(
// process of the same program starts at its own entry with every
// relocation already applied.
link = Number(globalThis.GEX_LINK ?? 0),
// Function-tier modules compiled ahead of the run (an ejected project's
// page compiles its units while the bundle downloads), as [name, Module]
// pairs every Worker takes instead of fetching and compiling its own.
preModules = null,
},
) {
const { shimBytes, seedBytes } = await loadRuntime();
@@ -659,6 +663,9 @@ export async function runProgram(
aot: globalThis.GEX_AOT ? true : undefined,
denseDebug: !!globalThis.GEX_DENSE_DEBUG,
traceMisses: !!globalThis.GEX_TRACE_MISSES,
// `?sharetrace=1`: say how compiled modules are shared between a process's Workers.
shareTrace: flag('sharetrace') ? 1 : undefined,
preModules: preModules ?? undefined,
// `?forkskip=N`, in `extra` because the process that acts on it is a
// forked child, not the page's own.
forkSkip: typeof globalThis.GEX_FORK_SKIP_MIB === 'number'