mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
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:
commit
414f43dc2e
28 files changed
+1078
-77
No files matched your search
@@ -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::*;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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))?;
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -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++) {
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user