Merge branch 'master' of /home/claude/gex-sandbox/gex into merge/unity

This commit is contained in:
velzie committed 2026-10-08 08:55:25 -04:00
commit 83548eac18
26 files changed
+1187 -47

No files matched your search

+194 -1
View File
@@ -36,6 +36,15 @@
* it reads back before a draw. */
#define GL_FAST_ARRAYS 0x7ffe0fe8u
#define ARRAYS_TEX_SHIFT 16
/* The array state the layer shares with the provider (gex_kernel::nt::
* opengl::GL_FAST_STATE and the FS_ words): GL_ARRAY_BUFFER's binding,
* whether a generic attribute reads client memory, then per fixed array
* size, type, stride, pointer, buffer. 0 without one (x64, or no fast path). */
#define GL_FAST_STATE 0x7ffe0fecu
#define FS_ARRAY_BUFFER 0u
#define FS_ATTRIB_CLIENT 4u
#define FS_ARRAYS 16u
#define FS_ARRAY_WORDS 5u
#define A_VERTEX 0
#define A_COLOR 1
#define A_TEXCOORD 2
@@ -58,6 +67,15 @@
#define FN_glUniform1i 2524u
#define FN_glUniform2f 2542u
#define FN_glUseProgram 2645u
#define FN_glVertexPointer 343u
#define FN_glColorPointer 60u
#define FN_glTexCoordPointer 298u
#define FN_glDrawArrays 74u
#define FN_glBlendFunc 16u
#define FN_glBlendFuncSeparate 463u
#define FN_glViewport 344u
#define FN_glTexParameteri 313u
#define FN_glUniform1f 2520u
#define FN_glEnableClientState 82u
#define FN_glDisableClientState 73u
#define OP_ENABLE 19u
@@ -65,12 +83,24 @@
#define OP_USE_PROGRAM 55u
#define OP_UNIFORM_1I 64u
#define OP_UNIFORM_2F 66u
#define OP_BIND_BUFFER 34u
#define OP_DRAW_ARRAYS 112u
#define OP_VERTEX_POINTER 187u
#define OP_COLOR_POINTER 188u
#define OP_TEX_COORD_POINTER 189u
#define OP_CLIENT_ACTIVE_TEXTURE 190u
#define OP_CLIENT_ARRAY 200u
#define OP_VIEWPORT 18u
#define OP_BLEND_FUNC 21u
#define OP_BLEND_FUNC_SEPARATE 37u
#define OP_UNIFORM_1F 65u
#define OP_TEX_PARAMETER_I 100u
#define OP_BIND_TEXTURE 98u
#define OP_ACTIVE_TEXTURE 99u
#define OP_ENABLE_CLIENT_STATE 185u
#define OP_DISABLE_CLIENT_STATE 186u
/* Append one command of `n` argument words (1 to 3) taken from the params
/* Append one command of `n` argument words (1 to 4) 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);
@@ -122,6 +152,149 @@ GEX_INLINE int client_state(uint32_t params, int on) {
return 1;
}
/* glVertexPointer, glColorPointer, glTexCoordPointer into client memory
* (no GL_ARRAY_BUFFER bound): pure state, which the layer would only have
* recorded. Written into the shared state, where the layer reads it back
* before it uses the arrays and where the fast draw below reads it. With a
* buffer bound the layer sends the pointer command, so that is its job. */
GEX_INLINE int set_pointer(uint32_t code, uint32_t params) {
uint32_t st = __atomic_load_n(GA(GL_FAST_STATE), __ATOMIC_ACQUIRE);
if (!st || GW(st + FS_ARRAY_BUFFER) != 0) {
return 0;
}
uint32_t i;
if (code == FN_glVertexPointer) {
i = A_VERTEX;
} else if (code == FN_glColorPointer) {
i = A_COLOR;
} else {
uint32_t unit = (GW(GL_FAST_ARRAYS) >> ARRAYS_TEX_SHIFT) & 15u;
if (unit >= TEX_UNITS) {
return 0;
}
i = A_TEXCOORD + unit;
}
uint32_t at = st + FS_ARRAYS + 4u * FS_ARRAY_WORDS * i;
GW(at) = GW(params + 4u); /* size */
GW(at + 4u) = GW(params + 8u); /* type */
GW(at + 8u) = GW(params + 12u); /* stride */
GW(at + 12u) = GW(params + 16u); /* pointer */
GW(at + 16u) = 0u; /* buffer */
return 1;
}
/* gex_kernel::nt::opengl's element_bytes. */
GEX_INLINE uint32_t element_bytes(int32_t size, uint32_t ty) {
uint32_t comp;
switch (ty) {
case 0x1400u: case 0x1401u: comp = 1; break;
case 0x1402u: case 0x1403u: case 0x140bu: comp = 2; break;
case 0x1404u: case 0x1405u: case 0x1406u: case 0x140cu: comp = 4; break;
case 0x140au: comp = 8; break;
default: return 0;
}
uint32_t n = size == 0x80e1 ? 4u : size < 0 ? 0u : size > 4 ? 4u : (uint32_t)size;
return comp * n;
}
/* glDrawArrays, as the layer does it: each enabled client-memory array's
* bytes as an op::CLIENT_ARRAY (texture coordinates with their unit made
* the client active texture first), GL_ARRAY_BUFFER and the client active
* texture put back, then the draw. Only the vertex, colour and texture
* coordinate arrays; anything else (a normal, secondary colour or fog array
* in client memory, a generic attribute in client memory, a ring without
* room) is the layer's. */
#define MAX_CLIENT_BYTES (1u << 20)
GEX_INLINE int draw_arrays(uint32_t params) {
uint32_t ring = __atomic_load_n(GA(GL_FAST_RING), __ATOMIC_ACQUIRE);
uint32_t st = __atomic_load_n(GA(GL_FAST_STATE), __ATOMIC_ACQUIRE);
if (!ring || !st || GW(st + FS_ATTRIB_CLIENT) != 0) {
return 0;
}
uint32_t mode = GW(params + 4u);
int32_t first = (int32_t)GW(params + 8u), count = (int32_t)GW(params + 12u);
uint32_t w = GW(GL_FAST_ARRAYS);
uint32_t unit = (w >> ARRAYS_TEX_SHIFT) & 15u;
if (unit >= TEX_UNITS) {
return 0;
}
uint32_t vertices = first >= 0 && count > 0 ? (uint32_t)first + (uint32_t)count : 0u;
/* What it will write, and that it may. */
uint32_t len = 20u, any = 0;
for (uint32_t i = 0; i <= A_FOG; ++i) {
if (!(w >> i & 1u)) {
continue;
}
uint32_t at = st + FS_ARRAYS + 4u * FS_ARRAY_WORDS * i;
if (GW(at + 16u) != 0 || GW(at + 12u) == 0) {
continue; /* in a buffer, or no pointer: nothing to upload */
}
if (i >= A_NORMAL || !vertices) {
return 0;
}
uint32_t elem = element_bytes((int32_t)GW(at), GW(at + 4u));
int32_t stride = (int32_t)GW(at + 8u);
if (!elem || stride < 0) {
return 0;
}
uint64_t n = (uint64_t)(vertices - 1u) * (stride > 0 ? (uint32_t)stride : elem) + elem;
if (n > MAX_CLIENT_BYTES) {
return 0;
}
len += (i >= A_TEXCOORD ? 12u : 0u) + 8u + 4u * 7u + (((uint32_t)n + 3u) & ~3u);
any |= 1u << i;
}
if (any) {
len += 16u + 12u;
}
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;
for (uint32_t i = 0; i <= A_TEXCOORD + TEX_UNITS - 1u; ++i) {
if (!(any >> i & 1u)) {
continue;
}
uint32_t s = st + FS_ARRAYS + 4u * FS_ARRAY_WORDS * i;
uint32_t op = i == A_VERTEX ? OP_VERTEX_POINTER : i == A_COLOR ? OP_COLOR_POINTER : OP_TEX_COORD_POINTER;
if (i >= A_TEXCOORD) {
GW(at) = OP_CLIENT_ACTIVE_TEXTURE, GW(at + 4u) = 12u, GW(at + 8u) = 0x84c0u + (i - A_TEXCOORD);
at += 12u;
}
/* Checked above: an element size, a stride >= 0, and at most
* MAX_CLIENT_BYTES. (Recomputed: a provider has no stack.) */
uint32_t elem = element_bytes((int32_t)GW(s), GW(s + 4u)), stride = GW(s + 8u);
uint32_t n = (vertices - 1u) * (stride ? stride : elem) + elem, padded = (n + 3u) & ~3u;
GW(at) = OP_CLIENT_ARRAY;
GW(at + 4u) = 8u + 4u * 7u + padded;
GW(at + 8u) = i; /* slot */
GW(at + 12u) = op; /* the pointer command */
GW(at + 16u) = 3u; /* its words before the pointer */
GW(at + 20u) = GW(s); /* size, type, stride */
GW(at + 24u) = GW(s + 4u);
GW(at + 28u) = GW(s + 8u);
GW(at + 32u) = n;
__builtin_memmove(GEX_P(at + 36u), GEX_P(GW(s + 12u)), n);
for (uint32_t k = n; k < padded; ++k) {
*(uint8_t *)GEX_P(at + 36u + k) = 0;
}
at += 8u + 4u * 7u + padded;
}
if (any) {
GW(at) = OP_BIND_BUFFER, GW(at + 4u) = 16u, GW(at + 8u) = 0x8892u, GW(at + 12u) = GW(st + FS_ARRAY_BUFFER);
GW(at + 16u) = OP_CLIENT_ACTIVE_TEXTURE, GW(at + 20u) = 12u, GW(at + 24u) = 0x84c0u + unit;
at += 28u;
}
GW(at) = OP_DRAW_ARRAYS, GW(at + 4u) = 20u;
GW(at + 8u) = mode, GW(at + 12u) = (uint32_t)first, GW(at + 16u) = (uint32_t)count;
__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) {
@@ -134,6 +307,26 @@ static GEX_PROVIDER(p_unix_call) {
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;
/* The next most frequent in Noita's frame after the seven above
* (GEX_NT_GL_STATS: 39, 8, 9, 12 and 14 a frame), all commands
* with no layer state, as the layer's `hot_one` writes them. */
case FN_glBlendFunc: op = OP_BLEND_FUNC, n = 2; break;
case FN_glBlendFuncSeparate: op = OP_BLEND_FUNC_SEPARATE, n = 4; break;
case FN_glViewport: op = OP_VIEWPORT, n = 4; break;
case FN_glTexParameteri: op = OP_TEX_PARAMETER_I, n = 3; break;
case FN_glUniform1f: op = OP_UNIFORM_1F, n = 2; break;
case FN_glVertexPointer:
case FN_glColorPointer:
case FN_glTexCoordPointer:
if (params >= 0x10000u && set_pointer(code, params)) {
return gp_return_eax(ctx, ret, 16, 0);
}
break;
case FN_glDrawArrays:
if (params >= 0x10000u && draw_arrays(params)) {
return gp_return_eax(ctx, ret, 16, 0);
}
break;
case FN_glEnableClientState:
case FN_glDisableClientState:
if (params >= 0x10000u && client_state(params, code == FN_glEnableClientState)) {
+79
View File
@@ -0,0 +1,79 @@
//! Replay a recorded guest GL stream (`crates/gex-gl/src/trace.rs`) on the
//! native host (gex-gl's `Gl`, the device natively run games use), timing it:
//!
//! cargo run --release -p gex-gl --example replay -- TRACE [FRAMES]
//!
//! Prints, over the last FRAMES frames (a batch with PRESENT ends a frame):
//! wall ms per frame inside `Gl::run`, batches per frame, and the commands
//! by host time.
use gex_gl::host::Gl;
use gex_gl::proto::{op, Commands};
use gex_gl::trace;
use std::time::Instant;
fn main() {
let args: Vec<String> = std::env::args().collect();
let path = &args[1];
let frames: usize = args.get(2).and_then(|v| v.parse().ok()).unwrap_or(300);
let bytes = std::fs::read(path).expect("trace");
let t = trace::parse(&bytes).expect("a trace");
let mut gl = Gl::new(t.info.width.max(1) as i32, t.info.height.max(1) as i32).expect("host GL");
let presents: Vec<bool> = t
.batches
.iter()
.map(|b| {
let mut c = Commands::new(&b.bytes);
let mut present = false;
while let Some(Ok((o, _))) = c.next() {
present |= o == op::PRESENT;
}
present
})
.collect();
let total = presents.iter().filter(|p| **p).count();
let start_frame = total.saturating_sub(frames);
let (mut frame, mut ms, mut nb, mut measuring) = (0usize, 0f64, 0usize, false);
let mut per = Vec::new();
let mut fms = 0f64;
let t_all = Instant::now();
for (i, b) in t.batches.iter().enumerate() {
if !measuring && frame >= start_frame {
measuring = true;
gl.time_ops(true);
}
let t0 = Instant::now();
let (_, status, _) = gl.run(&b.bytes);
let e = t0.elapsed().as_secs_f64() * 1e3;
if status != 0 {
eprintln!("batch {i}: status {status}");
}
if measuring {
ms += e;
fms += e;
nb += 1;
}
if presents[i] {
if measuring {
per.push(fms);
}
fms = 0.0;
frame += 1;
}
}
let n = per.len().max(1);
per.sort_by(|a, b| a.partial_cmp(b).unwrap());
println!(
"{total} frames ({:.1} s to replay all); last {n}: {:.3} ms a frame in Gl::run (median {:.3}, p90 {:.3}), {:.2} batches a frame",
t_all.elapsed().as_secs_f64(),
ms / n as f64,
per[n / 2],
per[n * 9 / 10],
nb as f64 / n as f64
);
let ops = gl.take_op_times();
let sum: u64 = ops.iter().map(|o| o.2).sum();
println!("commands: {:.3} ms a frame in all", sum as f64 / 1e6 / n as f64);
for (o, c, ns) in ops.iter().take(30) {
println!(" op {o:5} {:8.1}/frame {:8.3} ms/frame", *c as f64 / n as f64, *ns as f64 / 1e6 / n as f64);
}
}
+102 -1
View File
@@ -462,6 +462,12 @@ pub struct Gl {
frame: Option<Frame>,
/// PRESENTs so far.
pub presents: u64,
/// `op::CLIENT_ARRAY`'s bytes by slot: GL keeps the pointer, so they
/// stay until the slot's next one.
client_arrays: std::collections::HashMap<u32, Vec<u8>>,
/// Per opcode: commands run and nanoseconds in them, when asked for
/// ([`Gl::time_ops`]; the replay tool's profile).
op_times: Option<std::collections::HashMap<u32, (u64, u64)>>,
}
/// `GEX_GL_READBACK_EVERY=N`: read back (and hand on) only every Nth
@@ -661,6 +667,8 @@ impl Gl {
keep_frames: false,
frame: None,
presents: 0,
client_arrays: std::collections::HashMap::new(),
op_times: None,
})
}
}
@@ -815,6 +823,19 @@ impl Gl {
Ok(())
}
/// Time every command by opcode from now on ([`Gl::take_op_times`]).
pub fn time_ops(&mut self, on: bool) {
self.op_times = on.then(std::collections::HashMap::new);
}
/// Commands and nanoseconds by opcode since the last call.
pub fn take_op_times(&mut self) -> Vec<(u32, u64, u64)> {
let Some(m) = self.op_times.as_mut() else { return Vec::new() };
let mut v: Vec<_> = m.drain().map(|(k, (c, n))| (k, c, n)).collect();
v.sort_by(|a, b| b.2.cmp(&a.2));
v
}
/// Whether the context is a desktop one (fixed function available).
pub fn is_desktop(&self) -> bool {
self.compat.is_some()
@@ -889,7 +910,14 @@ impl Gl {
None => return (reply, error::NONE, index),
Some(Err(())) => return (reply, error::TRUNCATED, index),
Some(Ok((opcode, body))) => {
if !self.one(opcode, body, &mut reply) {
let t = self.op_times.is_some().then(std::time::Instant::now);
let ok = self.one(opcode, body, &mut reply);
if let (Some(t), Some(m)) = (t, self.op_times.as_mut()) {
let e = m.entry(opcode).or_insert((0, 0));
e.0 += 1;
e.1 += t.elapsed().as_nanos() as u64;
}
if !ok {
return (reply, error::BAD_OPCODE, index);
}
gl_error_check(opcode);
@@ -1096,6 +1124,32 @@ impl Gl {
let names: Vec<GLuint> = (0..n).filter_map(|_| a.u32()).collect();
glDeleteTextures(names.len() as GLsizei, names.as_ptr());
}
op::CLIENT_ARRAY => {
// { slot, op, n, n words, bytes }: run the pointer
// command `op` with the bytes' address as its pointer and
// no GL_ARRAY_BUFFER bound (proto.rs).
let (Some(slot), Some(inner), Some(n)) = (a.u32(), a.u32(), a.u32()) else {
return true;
};
if n > 8 || inner == op::CLIENT_ARRAY {
return false;
}
let mut body = Vec::with_capacity(n as usize * 4 + 8);
for _ in 0..n {
let Some(w) = a.u32() else { return true };
body.extend_from_slice(&w.to_le_bytes());
}
let Some(bytes) = a.blob() else { return true };
let mem = self.client_arrays.entry(slot).or_default();
mem.clear();
mem.extend_from_slice(bytes);
if mem.is_empty() {
mem.push(0);
}
body.extend_from_slice(&(mem.as_ptr() as usize as u64).to_le_bytes());
glBindBuffer(0x8892, 0); // GL_ARRAY_BUFFER
return self.one(inner, &body, reply);
}
op::BIND_BUFFER => {
if let (Some(t), Some(n)) = (a.u32(), a.u32()) {
glBindBuffer(t, n);
@@ -2332,6 +2386,53 @@ mod tests {
assert_eq!(pixel16(&px, 3, 8), (255, 0, 0, 255), "the client-array quad, scaled");
assert_eq!(pixel16(&px, 9, 8), (0, 0, 0, 255), "nothing between");
assert_eq!(pixel16(&px, 14, 8), (0, 255, 0, 255), "the immediate quad, moved by the loaded matrix");
// CLIENT_ARRAY: client-memory arrays carried with their pointer
// commands, drawn while the guest's own buffer stays bound. (In this
// test rather than one of its own: the host tests share the native
// decoder's name tables, so a further concurrent GL test races them.)
b = Batch::new();
b.cmd(op::LOAD_IDENTITY, &[]);
b.cmd(op::MATRIX_MODE, &[GL_PROJECTION]);
b.cmd(op::LOAD_IDENTITY, &[]);
b.cmd(op::MATRIX_MODE, &[GL_MODELVIEW]);
b.cmd(op::DISABLE, &[GL_ALPHA_TEST]);
// The left half in clip space, red; then the right half, blue, from
// the same slots: each CLIENT_ARRAY replaces its slot's bytes.
let quad = |x0: f32, x1: f32| {
let mut v = Vec::new();
for (x, y) in [(x0, -1.0f32), (x1, -1.0), (x1, 1.0), (x0, 1.0)] {
v.extend_from_slice(&x.to_le_bytes());
v.extend_from_slice(&y.to_le_bytes());
}
v
};
let colour = |c: [u8; 4]| c.repeat(4);
b.cmd(op::CLEAR_COLOR, &[0, 0, 0, f(1.0)]);
b.cmd(op::CLEAR, &[GL_COLOR_BUFFER_BIT]);
b.cmd(op::GEN_BUFFERS, &[1, 9]);
b.cmd(op::BIND_BUFFER, &[GL_ARRAY_BUFFER, 9]);
b.cmd(op::ENABLE_CLIENT_STATE, &[GL_VERTEX_ARRAY]);
b.cmd(op::ENABLE_CLIENT_STATE, &[GL_COLOR_ARRAY]);
for (x0, x1, c) in [(-1.0, 0.0, [255, 0, 0, 255]), (0.0, 1.0, [0, 0, 255, 255])] {
let v = quad(x0, x1);
b.raw(op::CLIENT_ARRAY, &[0, op::VERTEX_POINTER, 3, 2, GL_FLOAT, 8, v.len() as u32], &v);
let c = colour(c);
b.raw(op::CLIENT_ARRAY, &[1, op::COLOR_POINTER, 3, 4, GL_UNSIGNED_BYTE, 4, c.len() as u32], &c);
b.cmd(op::BIND_BUFFER, &[GL_ARRAY_BUFFER, 9]);
b.cmd(op::DRAW_ARRAYS, &[GL_QUADS, 0, 4]);
}
read_back(&mut b);
b.cmd(op::GET_ERROR, &[]);
let (reply, status, _) = gl.run(b.bytes());
assert_eq!(status, error::NONE, "every command was understood");
let r = reply.bytes();
let mut at = 0;
let _offset = read_u32(r, &mut at);
let px = read_blob(r, &mut at);
assert_eq!(read_u32(r, &mut at), 0, "GL reported no error");
assert_eq!(pixel16(&px, 3, 8), (255, 0, 0, 255), "the first quad, from its own bytes");
assert_eq!(pixel16(&px, 12, 8), (0, 0, 255, 255), "the second, from the slots' new bytes");
}
/// Protocol 12's attribute stack, and the host's uniform vectors and
+9
View File
@@ -422,6 +422,15 @@ pub mod op {
pub const PUSH_ATTRIB: u32 = 198;
/// `{}` -- glPopAttrib.
pub const POP_ATTRIB: u32 = 199;
/// A client-memory vertex array: `{ slot, op, n, n words, bytes }`.
/// The host keeps the bytes (one copy per `slot`, until that slot's next
/// CLIENT_ARRAY) and runs pointer command `op` with the `n` argument
/// words and the bytes' address as its pointer, with no
/// GL_ARRAY_BUFFER bound. How the NT layer passes glVertexPointer-style
/// client arrays: GL then uploads them the way it uploads any user
/// array, instead of the guest rewriting a buffer of its own before each
/// draw (which a batched backend has to stage and copy).
pub const CLIENT_ARRAY: u32 = 200;
}
/// Writes commands into a batch. For the host's tests: the guest half is C,
+201 -28
View File
@@ -61,6 +61,20 @@ pub const TAIL_BUSY: u32 = 1 << 31;
pub const GL_FAST_ARRAYS: u64 = super::USD + 0xfe8;
const ARRAYS_TEX_SHIFT: u32 = 16;
/// Where the fast path's array state is: the address of a block of words
/// the layer allocates with the ring ([`FastState`]), 0 when there is none.
/// The provider sets client-memory array pointers there and draws from it;
/// the layer keeps it current and reads the pointers back before it uses
/// them ([`sync_arrays`]).
pub const GL_FAST_STATE: u64 = super::USD + 0xfec;
/// The block's words: GL_ARRAY_BUFFER's binding, then whether any enabled
/// generic attribute reads client memory (no fast draw then), then five per
/// fixed array (`VERTEX` .. `FOG`): size, type, stride, pointer, buffer.
pub const FS_ARRAY_BUFFER: u64 = 0;
pub const FS_ATTRIB_CLIENT: u64 = 4;
pub const FS_ARRAYS: u64 = 16;
pub const FS_ARRAY_WORDS: u64 = 5;
/// 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);
@@ -261,6 +275,8 @@ struct Ctx {
pub struct State {
ring: u64,
/// The fast path's array state ([`GL_FAST_STATE`]), 0 without one.
fast_state: u64,
dev: Option<Arc<dyn FileHandle>>,
host_made: bool,
contexts: HashMap<u64, Ctx>,
@@ -312,6 +328,7 @@ impl Default for State {
fn default() -> Self {
State {
ring: 0,
fast_state: 0,
dev: None,
host_made: false,
contexts: HashMap::new(),
@@ -367,12 +384,26 @@ fn open_device(c: &mut Call<'_>) -> R<()> {
// that reads the address sees a tail that belongs to it.
c.g.w32(GL_FAST_TAIL, 0)?;
c.g.w32(GL_FAST_ARRAYS, arrays_off())?;
if fast_path() {
c.g.w32(GL_FAST_RING, ring as u32)?;
}
// The provider is i386's, and the block holds 32-bit pointers.
let fast_state = if fast_path() && c.p.abi == Abi::P32 {
super::sysinfo::alloc_private(c, 4096, "[gex-gl fast state]")?
} else {
0
};
s = c.p.gl.lock().unwrap();
s.ring = ring;
s.dev = Some(dev);
s.fast_state = fast_state;
drop(s);
if fast_state != 0 {
publish_words(c)?;
for i in 0..FIXED_ARRAYS {
publish_array(c, i)?;
}
c.g.w32(GL_FAST_STATE, fast_state as u32)?;
c.g.w32(GL_FAST_RING, ring as u32)?;
}
s = c.p.gl.lock().unwrap();
Ok(())
}
@@ -1039,35 +1070,65 @@ fn pointer_op(name: &str) -> u32 {
generated::POINTER_PLANS.binary_search_by(|e| e.0.cmp(name)).map(|i| generated::POINTER_PLANS[i].1).unwrap_or(0)
}
/// Send fixed array `i`'s pointer, as `offset` into the bound array buffer.
fn send_array_pointer(c: &mut Call<'_>, i: usize, a: &Array, offset: u64) -> R<()> {
let [lo, hi] = proto::Batch::words64(offset);
/// Fixed array `i`'s pointer command and its words before the pointer. A
/// texture coordinate array's also needs its unit made the client active
/// texture first ([`send_array_pointer`] does).
fn array_pointer(i: usize, a: &Array) -> (u32, Vec<u32>) {
let (size, ty, stride) = (a.size as u32, a.ty, a.stride as u32);
match i {
VERTEX => cmd(c, op::VERTEX_POINTER, &[size, ty, stride, lo, hi], &[]),
COLOR => cmd(c, op::COLOR_POINTER, &[size, ty, stride, lo, hi], &[]),
NORMAL => cmd(c, pointer_op("glNormalPointer"), &[ty, stride, lo, hi], &[]),
SECONDARY => cmd(c, pointer_op("glSecondaryColorPointer"), &[size, ty, stride, lo, hi], &[]),
FOG => cmd(c, pointer_op("glFogCoordPointer"), &[ty, stride, lo, hi], &[]),
_ => {
cmd(c, op::CLIENT_ACTIVE_TEXTURE, &[0x84c0 + (i - TEXCOORD) as u32], &[])?;
cmd(c, op::TEX_COORD_POINTER, &[size, ty, stride, lo, hi], &[])
}
VERTEX => (op::VERTEX_POINTER, vec![size, ty, stride]),
COLOR => (op::COLOR_POINTER, vec![size, ty, stride]),
NORMAL => (pointer_op("glNormalPointer"), vec![ty, stride]),
SECONDARY => (pointer_op("glSecondaryColorPointer"), vec![size, ty, stride]),
FOG => (pointer_op("glFogCoordPointer"), vec![ty, stride]),
_ => (op::TEX_COORD_POINTER, vec![size, ty, stride]),
}
}
/// Generic attribute `i`'s pointer command and its words before the pointer.
fn attrib_pointer(i: usize, a: &Attrib) -> (u32, Vec<u32>) {
let (idx, size, ty, stride) = (i as u32, a.size as u32, a.ty, a.stride as u32);
match a.kind {
0 => (pointer_op("glVertexAttribPointer"), vec![idx, size, ty, a.normalized as u32, stride]),
1 => (pointer_op("glVertexAttribIPointer"), vec![idx, size, ty, stride]),
_ => (pointer_op("glVertexAttribLPointer"), vec![idx, size, ty, stride]),
}
}
/// Send fixed array `i`'s pointer, as `offset` into the bound array buffer.
fn send_array_pointer(c: &mut Call<'_>, i: usize, a: &Array, offset: u64) -> R<()> {
let (opcode, mut words) = array_pointer(i, a);
if (TEXCOORD..ARRAYS).contains(&i) {
cmd(c, op::CLIENT_ACTIVE_TEXTURE, &[0x84c0 + (i - TEXCOORD) as u32], &[])?;
}
words.extend(proto::Batch::words64(offset));
cmd(c, opcode, &words, &[])
}
/// Send generic attribute `i`'s pointer, as `offset` into the bound array
/// buffer.
fn send_attrib_pointer(c: &mut Call<'_>, i: usize, a: &Attrib, offset: u64) -> R<()> {
let [lo, hi] = proto::Batch::words64(offset);
let (idx, size, ty, stride) = (i as u32, a.size as u32, a.ty, a.stride as u32);
match a.kind {
0 => cmd(c, pointer_op("glVertexAttribPointer"), &[idx, size, ty, a.normalized as u32, stride, lo, hi], &[]),
1 => cmd(c, pointer_op("glVertexAttribIPointer"), &[idx, size, ty, stride, lo, hi], &[]),
_ => cmd(c, pointer_op("glVertexAttribLPointer"), &[idx, size, ty, stride, lo, hi], &[]),
}
let (opcode, mut words) = attrib_pointer(i, a);
words.extend(proto::Batch::words64(offset));
cmd(c, opcode, &words, &[])
}
/// A client-memory array's bytes and its pointer command, as one
/// `op::CLIENT_ARRAY`: the host keeps the bytes in slot `slot` and points GL
/// at them. Replaces a scratch buffer of ours rewritten with glBufferData
/// before every draw (`upload_scratch`), which a batched GL backend can only
/// do as a staged copy that ends the render pass.
fn send_client_array(c: &mut Call<'_>, slot: u32, opcode: u32, words: &[u32], data: &[u8]) -> R<()> {
let mut head = Vec::with_capacity(3 + words.len());
head.extend_from_slice(&[slot, opcode, words.len() as u32]);
head.extend_from_slice(words);
cmd_blob(c, op::CLIENT_ARRAY, &head, data)
}
/// Whether client arrays go as `op::CLIENT_ARRAY` (default) or through
/// scratch buffers (`GEX_NT_GL_CLIENT_ARRAYS=buffer`, for A/B runs).
pub static CLIENT_ARRAY_OP: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
/// Copy `bytes` of client memory at `pointer` into scratch buffer `*scratch`
/// (made on first use), left bound to GL_ARRAY_BUFFER.
fn upload_scratch(c: &mut Call<'_>, scratch: u32, target: u32, data: &[u8]) -> R<u32> {
@@ -1093,6 +1154,7 @@ fn upload_arrays_instanced(c: &mut Call<'_>, vertices: usize, instances: usize)
(s.arrays, s.attribs, s.array_buffer)
};
const GL_ARRAY_BUFFER: u32 = 0x8892;
let by_op = CLIENT_ARRAY_OP.load(std::sync::atomic::Ordering::Relaxed);
let mut rebound = false;
for (i, a) in arrays.iter().enumerate() {
if !a.enabled || a.buffer != 0 || a.pointer == 0 || vertices == 0 {
@@ -1104,9 +1166,17 @@ fn upload_arrays_instanced(c: &mut Call<'_>, vertices: usize, instances: usize)
}
let stride = if a.stride > 0 { a.stride as usize } else { elem };
let data = c.g.bytes(a.pointer, (vertices - 1) * stride + elem)?;
rebound = true;
if by_op {
if (TEXCOORD..ARRAYS).contains(&i) {
cmd(c, op::CLIENT_ACTIVE_TEXTURE, &[0x84c0 + (i - TEXCOORD) as u32], &[])?;
}
let (opcode, words) = array_pointer(i, a);
send_client_array(c, i as u32, opcode, &words, &data)?;
continue;
}
let scratch = upload_scratch(c, a.scratch, GL_ARRAY_BUFFER, &data)?;
c.p.gl.lock().unwrap().arrays[i].scratch = scratch;
rebound = true;
send_array_pointer(c, i, a, 0)?;
}
for (i, a) in attribs.iter().enumerate() {
@@ -1120,9 +1190,14 @@ fn upload_arrays_instanced(c: &mut Call<'_>, vertices: usize, instances: usize)
let stride = if a.stride > 0 { a.stride as usize } else { elem };
let count = if a.divisor == 0 { vertices } else { instances.div_ceil(a.divisor as usize).max(1) };
let data = c.g.bytes(a.pointer, (count - 1) * stride + elem)?;
rebound = true;
if by_op {
let (opcode, words) = attrib_pointer(i, a);
send_client_array(c, 64 + i as u32, opcode, &words, &data)?;
continue;
}
let scratch = upload_scratch(c, a.scratch, GL_ARRAY_BUFFER, &data)?;
c.p.gl.lock().unwrap().attribs[i].scratch = scratch;
rebound = true;
send_attrib_pointer(c, i, a, 0)?;
}
if rebound {
@@ -1141,6 +1216,50 @@ fn sync_arrays(c: &Call<'_>) {
for (i, a) in s.arrays.iter_mut().enumerate().take(FOG + 1) {
a.enabled = w >> i & 1 != 0;
}
// And the pointers the provider set (glVertexPointer and the others
// into client memory).
let st = s.fast_state;
if st != 0 {
for (i, a) in s.arrays.iter_mut().enumerate().take(FOG + 1) {
let at = st + FS_ARRAYS + 4 * FS_ARRAY_WORDS * i as u64;
let word = |k: u64| c.g.u32(at + 4 * k).unwrap_or(0);
a.size = word(0) as i32;
a.ty = word(1);
a.stride = word(2) as i32;
a.pointer = word(3) as u64;
a.buffer = word(4);
}
}
}
/// Fixed array `i` into the fast state, after the layer changed it.
fn publish_array(c: &mut Call<'_>, i: usize) -> R<()> {
let (st, a) = {
let s = c.p.gl.lock().unwrap();
(s.fast_state, s.arrays[i])
};
if st == 0 || i > FOG {
return Ok(());
}
let at = st + FS_ARRAYS + 4 * FS_ARRAY_WORDS * i as u64;
for (k, v) in [a.size as u32, a.ty, a.stride as u32, a.pointer as u32, a.buffer].into_iter().enumerate() {
c.g.w32(at + 4 * k as u64, v)?;
}
Ok(())
}
/// GL_ARRAY_BUFFER's binding and whether a generic attribute reads client
/// memory, into the fast state.
fn publish_words(c: &mut Call<'_>) -> R<()> {
let (st, ab, attrib_client) = {
let s = c.p.gl.lock().unwrap();
(s.fast_state, s.array_buffer, s.attribs.iter().any(|a| a.enabled && a.buffer == 0 && a.pointer != 0))
};
if st == 0 {
return Ok(());
}
c.g.w32(st + FS_ARRAY_BUFFER, ab)?;
c.g.w32(st + FS_ATTRIB_CLIENT, attrib_client as u32)
}
/// Write the layer's array flags and client texture to [`GL_FAST_ARRAYS`].
@@ -1333,6 +1452,7 @@ const GL44_GET_TEX_IMAGE: u32 = 1247;
/// glNormalPointer, glFogCoordPointer, glSecondaryColorPointer: fixed array
/// `i` with `size` components.
fn set_typed_pointer(c: &mut Call<'_>, i: usize, size: i32, ty: u32, stride: i32, pointer: u64) -> R<()> {
sync_arrays(c);
let a = {
let mut s = c.p.gl.lock().unwrap();
let buffer = s.array_buffer;
@@ -1344,6 +1464,7 @@ fn set_typed_pointer(c: &mut Call<'_>, i: usize, size: i32, ty: u32, stride: i32
a.buffer = buffer;
*a
};
publish_array(c, i)?;
if a.buffer != 0 {
send_array_pointer(c, i, &a, pointer)?;
}
@@ -1363,6 +1484,7 @@ fn array_index(c: &Call<'_>, cap: u32) -> Option<usize> {
}
fn set_pointer(c: &mut Call<'_>, i: usize, opcode: u32, size: i32, ty: u32, stride: i32, pointer: u64) -> R<()> {
sync_arrays(c);
let buffer = c.p.gl.lock().unwrap().array_buffer;
{
let mut s = c.p.gl.lock().unwrap();
@@ -1373,6 +1495,7 @@ fn set_pointer(c: &mut Call<'_>, i: usize, opcode: u32, size: i32, ty: u32, stri
a.pointer = pointer;
a.buffer = buffer;
}
publish_array(c, i)?;
if buffer != 0 {
let [lo, hi] = proto::Batch::words64(pointer);
cmd(c, opcode, &[size as u32, ty, stride as u32, lo, hi], &[])?;
@@ -1426,6 +1549,11 @@ enum Hot {
BindTexture,
Uniform1i,
Uniform2f,
BlendFunc,
BlendFuncSeparate,
Viewport,
TexParameteri,
Uniform1f,
}
fn hot(code: u32) -> Option<Hot> {
@@ -1441,6 +1569,11 @@ fn hot(code: u32) -> Option<Hot> {
"glBindTexture" => Some(Hot::BindTexture),
"glUniform1i" => Some(Hot::Uniform1i),
"glUniform2f" => Some(Hot::Uniform2f),
"glBlendFunc" => Some(Hot::BlendFunc),
"glBlendFuncSeparate" => Some(Hot::BlendFuncSeparate),
"glViewport" => Some(Hot::Viewport),
"glTexParameteri" => Some(Hot::TexParameteri),
"glUniform1f" => Some(Hot::Uniform1f),
_ => None,
})
.collect()
@@ -1457,6 +1590,13 @@ fn hot_one(c: &mut Call<'_>, h: Hot, p: &Params) -> R<()> {
Hot::BindTexture => cmd(c, op::BIND_TEXTURE, &[p.u32(c, 1)?, p.u32(c, 2)?], &[]),
Hot::Uniform1i => cmd(c, op::UNIFORM_1I, &[p.u32(c, 1)?, p.u32(c, 2)?], &[]),
Hot::Uniform2f => cmd(c, op::UNIFORM_2F, &[p.u32(c, 1)?, p.u32(c, 2)?, p.u32(c, 3)?], &[]),
Hot::BlendFunc => cmd(c, op::BLEND_FUNC, &[p.u32(c, 1)?, p.u32(c, 2)?], &[]),
Hot::BlendFuncSeparate => {
cmd(c, op::BLEND_FUNC_SEPARATE, &[p.u32(c, 1)?, p.u32(c, 2)?, p.u32(c, 3)?, p.u32(c, 4)?], &[])
}
Hot::Viewport => cmd(c, op::VIEWPORT, &[p.u32(c, 1)?, p.u32(c, 2)?, p.u32(c, 3)?, p.u32(c, 4)?], &[]),
Hot::TexParameteri => cmd(c, op::TEX_PARAMETER_I, &[p.u32(c, 1)?, p.u32(c, 2)?, p.u32(c, 3)?], &[]),
Hot::Uniform1f => cmd(c, op::UNIFORM_1F, &[p.u32(c, 1)?, p.u32(c, 2)?], &[]),
}
}
@@ -2017,6 +2157,7 @@ fn one(c: &mut Call<'_>, name: &str, p: &Params) -> R<()> {
a.kind = kind;
*a
};
publish_words(c)?;
if a.buffer != 0 {
send_attrib_pointer(c, index, &a, pointer)?;
}
@@ -2027,6 +2168,7 @@ fn one(c: &mut Call<'_>, name: &str, p: &Params) -> R<()> {
if let Some(a) = c.p.gl.lock().unwrap().attribs.get_mut(index as usize) {
a.enabled = on;
}
publish_words(c)?;
cmd(c, if on { op::ENABLE_VERTEX_ATTRIB_ARRAY } else { op::DISABLE_VERTEX_ATTRIB_ARRAY }, &[index], &[])?;
}
"glVertexAttribDivisor" => {
@@ -2125,15 +2267,23 @@ fn one(c: &mut Call<'_>, name: &str, p: &Params) -> R<()> {
// --- mapping buffers: a copy in guest memory, written back
"glMapBuffer" => {
let (t, access) = (p.u32(c, 1)?, p.u32(c, 2)?);
// GL_READ_ONLY, GL_WRITE_ONLY, GL_READ_WRITE
let addr = map_buffer(c, t, 0, None, access != 0x88b9, access != 0x88b8, false)?;
// GL_READ_ONLY, GL_WRITE_ONLY, GL_READ_WRITE. Read back even
// for GL_WRITE_ONLY: unmap uploads the whole buffer, so the copy
// must hold what the application does not overwrite.
let addr = map_buffer(c, t, 0, None, true, access != 0x88b8, false)?;
p.set_ptr(c, 3, addr)?;
}
"glMapBufferRange" => {
let (t, off, len, access) = (p.u32(c, 1)?, p.isize(c, 2)?, p.isize(c, 3)?, p.u32(c, 4)?);
// GL_MAP_READ_BIT, _WRITE_BIT, _INVALIDATE_RANGE_BIT,
// _INVALIDATE_BUFFER_BIT, _FLUSH_EXPLICIT_BIT
let read = access & 1 != 0 && access & 0xc == 0;
// The copy starts from the buffer's bytes wherever the
// application may read them (GL_MAP_READ_BIT) or unmap uploads
// bytes it did not write: without INVALIDATE_RANGE/_BUFFER the
// range keeps its contents, and with FLUSH_EXPLICIT only the
// flushed ranges go back (as guest/gl/gexgl.c, a355a62e, and
// pewasm's host_wgl.c map_buffer).
let read = access & 1 != 0 || access & (0x4 | 0x8 | 0x10) == 0;
let addr = map_buffer(c, t, off.max(0) as u64, Some(len.max(0) as u64), read, access & 2 != 0, access & 0x10 != 0)?;
p.set_ptr(c, 5, addr)?;
}
@@ -2260,6 +2410,9 @@ fn one(c: &mut Call<'_>, name: &str, p: &Params) -> R<()> {
}
s.bound.insert(t, b);
}
if t == 0x8892 {
publish_words(c)?;
}
cmd(c, op::BIND_BUFFER, &[t, b], &[])?;
}
"glBufferData" => {
@@ -2503,6 +2656,21 @@ mod tests {
assert_eq!(table::FUNCS[def(f) as usize].0, name, "{f}");
assert_eq!(def(o), op as u64, "{o}");
}
assert_eq!(def("GL_FAST_STATE"), GL_FAST_STATE);
for (name, v) in [("FS_ARRAY_BUFFER", FS_ARRAY_BUFFER), ("FS_ATTRIB_CLIENT", FS_ATTRIB_CLIENT), ("FS_ARRAYS", FS_ARRAYS), ("FS_ARRAY_WORDS", FS_ARRAY_WORDS)] {
assert_eq!(def(name), v, "{name}");
}
// The pointer calls and glDrawArrays the provider takes, and the
// commands it writes for them (as upload_arrays_instanced does).
for (f, name) in [("FN_glVertexPointer", "glVertexPointer"), ("FN_glColorPointer", "glColorPointer"), ("FN_glTexCoordPointer", "glTexCoordPointer"), ("FN_glDrawArrays", "glDrawArrays")] {
assert_eq!(table::FUNCS[def(f) as usize].0, name, "{f}");
}
for (o, op) in [("OP_BIND_BUFFER", op::BIND_BUFFER), ("OP_DRAW_ARRAYS", op::DRAW_ARRAYS), ("OP_VERTEX_POINTER", op::VERTEX_POINTER), ("OP_COLOR_POINTER", op::COLOR_POINTER), ("OP_TEX_COORD_POINTER", op::TEX_COORD_POINTER), ("OP_CLIENT_ACTIVE_TEXTURE", op::CLIENT_ACTIVE_TEXTURE), ("OP_CLIENT_ARRAY", op::CLIENT_ARRAY)] {
assert_eq!(def(o), op as u64, "{o}");
}
for sig in ["piiip"] {
assert!(layout(sig, Abi::P32)[..5].iter().enumerate().all(|(i, o)| *o == 4 * i 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);
@@ -2517,6 +2685,11 @@ mod tests {
("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),
("FN_glBlendFunc", "OP_BLEND_FUNC", "glBlendFunc", op::BLEND_FUNC),
("FN_glBlendFuncSeparate", "OP_BLEND_FUNC_SEPARATE", "glBlendFuncSeparate", op::BLEND_FUNC_SEPARATE),
("FN_glViewport", "OP_VIEWPORT", "glViewport", op::VIEWPORT),
("FN_glTexParameteri", "OP_TEX_PARAMETER_I", "glTexParameteri", op::TEX_PARAMETER_I),
("FN_glUniform1f", "OP_UNIFORM_1F", "glUniform1f", op::UNIFORM_1F),
] {
let code = def(f) as usize;
assert_eq!(table::FUNCS[code].0, name, "{f}");
@@ -2525,7 +2698,7 @@ mod tests {
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"] {
for sig in ["pi", "pii", "piff", "piiii", "pf", "pif"] {
let offs = layout(sig, Abi::P32);
for (i, o) in offs[..sig.len()].iter().enumerate() {
assert_eq!(*o, 4 * i as u64, "{sig}");
+284
View File
@@ -0,0 +1,284 @@
# The browser GL path: cost against pewasm's (stream GL)
Stream GL closes the GL/compositor cost gap between gex's browser page and
pewasm's (OPTIMIZATION-BACKLOG.md E1-E4, E10, W1). Branch `merge/glpath`,
worktree `wt/glpath`, scratch `~/gex-sandbox/scratch/GL` (`$G` below).
## Resume
- State (2026-10-07 evening): E2 done; CLIENT_ARRAY (bdb682d9), the
decoder report fix and opt-in WebGPU counters (21baa051), E3 map rules
(bcc93e30) and five more fast-path GL calls (430acec7) committed on
merge/glpath. E4 (web/mesa-wasm-cross.ini: SIMD, ThinLTO, -O3 link) is in
the worktree, uncommitted: commit after a browser GL gate with a Mesa
built from it. Not yet measured: the normal gexc page (needs a unit cache
at merge/glpath's keys on edger), and 430acec7 on the page (the eject
project must rebuild the nt_unixcall provider and the trampolines' unit).
- Live A/B harness for the ejected page: `$G/patch-site.mjs SITE PAGE OUT
LAUNCHER` swaps a site's page (shim, kernel, Mesa, JS) for one built by
web/build-eject-page.sh, keeping its units; the page must come from the
tree the site's units were built at (site30: wt/eject9 52bb4b96, or the
NT trampolines' unit misses). `wt/gl-e9` (branch glpath-e9) = 52bb4b96 +
this stream's commits; `bshim-e9.sh` builds its shim/kernel/seed/gexc.
`glegs.sh TAG LEG...` (pew | SITE[@ENV]) measures world fps and the
compositor's gex_gl_submit ms per frame.
- Tools (all in `$G`, run on edger through `scratch/bench/bench.sh`; never on
gooner):
- `wgpu-count.js`: a WebGPU call counter (calls, ms inside, bytes) by
prototype wrapping. Injected into pewasm's page (`run-pw-count.sh TAG
SECS`, a copy of `port/browser.mjs` with `--inject`) and prepended to the
Mesa worker's module in an eject site copy (`site32c`).
- `gl-run.mjs`: eject9's page-run.mjs plus `--every N` (counter snapshots
from the compositor worker with the world frame) and `--capture A,FILE`
(freezes the worker's `?glcapture=` recording A s after world-up and
pulls it over CDP in 8 MiB chunks).
- `pwcount.py LOG [SPAN]`: WebGPU calls per world frame from either log.
- `replay/index.html` + `replay-run.mjs MODS TRACE`: replays a trace
through Mesa builds in `$G/mods/NAME/` on hardware WebGPU, timing every
submit; prints CPU ms per frame and WebGPU calls per frame over the last N
frames; `--profile` writes a V8 CPU profile of the measured window.
- `ops.mjs TRACE [N]`: opcode histogram per frame.
- `bm.sh NAME ROOT [CROSS2]`: builds gex-wgpu-replay from
`ROOT/third_party/mesa` (a worktree of `$G/mesa-wink`) into
`$G/mesa-NAME`. Roots: `roots/cur` (wink 5f5572e6, gex's pin) and
`roots/pw` (wink 1f16bf09, pewasm's).
- `prof/top.py`, `prof/callers.py`: V8 profile summaries.
- mesa-wink changes are prepared as commits in `$G/mesa-wink` (a blobless
clone, origin GitHub) and never pushed without the user's approval.
## E2: where the compositor's time goes (2026-10-07)
All numbers from edger (Quadro P620, WebGPU over Vulkan, headless Chromium
153, BENCH_CORES default, world seed 1767225600, 4x clock).
### Live counts, per world frame (WebGPU calls)
pewasm's page (`out/pwc1.log`, 4641 world frames over 100 s, 46.3 world fps)
against the ejected page (site30 + counter, `out/cap1.txt`, 7026 world frames
over 100 s, 70.1 world fps). Calls per world frame, and ms of JS inside them:
| call | pewasm | gex (eject) |
| --- | --- | --- |
| beginRenderPass | 15.0 (0.16 ms) | 52.0 (0.55 ms) |
| createView | 16.0 (0.16 ms) | 78.0 (0.53 ms) |
| writeBuffer | 87.0 (0.21 ms, 19 KiB) | 179.3 (0.39 ms, 21 KiB) |
| copyBufferToBuffer | 0 | 123.3 (0.13 ms) |
| createBuffer | 0 | 15.0 (0.08 ms) |
| createRenderPipeline | 0 | 4.0 (0.16 ms) |
| setBindGroup | 64 | 108 |
| setViewport / setScissorRect | 9 / 9 | 45 / 45 |
| draw + drawIndexed | 12 + 35 | 12.9 + 35.1 |
| queue submit | 14.0 | 2.7 |
| writeTexture | 5.0 (839 KiB) | 5.0 (841 KiB) |
| all calls, JS ms | 1.85 | 3.13 |
The draws are the same (47 a frame). Everything around them is two to five
times as much in gex.
### The trace
`gl-run.mjs --capture` took the ejected page's GL stream to 30 s after
world-up: 1.59 GB, 3403 batches, 2777 frames (`out/noita-world.gexgltr` on
edger and gooner). Per world frame (last 300): one batch, 3321 commands,
901 KiB, of which 839 KiB is five glTexSubImage2D (the world's pixels). 47
glDrawArrays, 18 glBindFramebuffer, and **142 glBufferData of 17 KiB in all:
the NT layer's copies of Noita's client-side vertex arrays** (three arrays a
draw, `upload_scratch`). No buffer maps, no glBufferStorage (so E3 and
pewasm's `-GL_ARB_buffer_storage` cannot matter for Noita's frame).
### Replays of the same trace (`replay-run.mjs`, last 600 frames, CPU ms of
`gex_gl_submit` per frame, counters off; two passes, in and reversed order)
| Mesa build | ms per frame | passes | copies | submits |
| --- | --- | --- | --- | --- |
| site30's (wink 5f5572e6, as shipped) | 8.90, 9.08 | 51 | 123 | 2 |
| wink 5f5572e6 + the decoder fix below | 8.87, 9.01 | 51 | 123 | 2 |
| pewasm's pin (wink 1f16bf09), same flags | 33.9, 34.3 | 51 | 0 | 49 |
So pewasm's older driver is *slower* on gex's stream (34 against 9 ms a frame: it
submits on every rewrite of a buffer the batch reads). The gap is not Mesa's
version or build: it is **the stream**. pewasm passes client arrays to Mesa
as pointers (st/mesa uploads them through its stream uploader into fresh
ranges); gex rewrites one scratch buffer per array with glBufferData before
every draw, and since the pending batch already reads that buffer the driver
stages each rewrite and copies it inside the batch, which ends the render
pass: 142 uploads, 123 copies, 51 passes for 47 draws.
### Smaller findings
- **The decoder printed ten report lines every frame**
(`web/mesa/gexgl-mesa.cpp` display_backend: `gex_wgpu_report_draw_targets()`
sat outside its `if (frames % 600 == 0)` for want of braces): about
0.2 ms a frame of vsnprintf, stdio and console plumbing (display 0.27 ->
0.09 ms in the replay). Fixed.
- **The WebGPU call counter in mesa-gl.js ran always**: a performance.now
pair per call, 2.5% of the compositor worker in F's co1 profile (and 13%
of the replay's CPU per frame). Now opt-in, `?wgpustats=1`.
- **Per-draw debug labels in the driver**: six snprintf per draw for WebGPU
object labels (`gex_wgpu_draw_vbo_inner`, "draw %s/%s" and its group and
layout labels), about 0.3 ms a frame in the profile. Gated behind an env
variable and replayed (r5): 5.44 and 5.30 ms against 5.13 and 5.32 ms
without the change, i.e. inside the noise. Dropped; no mesa-wink change.
- **Attachment views are made per render pass** (createView 78 a frame), and
2-4 render pipelines are created every frame in the live page.
- E4 (SIMD + ThinLTO) was measured by E on the page before: no gain (control
53.6, 56.8 vs 51.5, 56.9 world fps). By replay of the CLIENT_ARRAY stream
(r4, 600 frames, two passes): release (gex's flags) 5.45 and 5.47 ms a
frame; with eject9's mesa-o3.ini (`-msimd128 -flto=thin`, link `-O3`,
which runs binaryen at link) 5.09 and 4.99 ms, 7% less; a further
`wasm-opt -O2` over that 5.28 and 5.04 ms (nothing more).
### CLIENT_ARRAY, by replay
The NT layer now sends client arrays as `op::CLIENT_ARRAY` (bdb682d9).
`to-client-array.mjs` rewrites the recorded trace into what the new layer
sends, and the replay (r3, 600 frames, two passes; the last frame's
readback digest equal on both):
| stream | ms per frame | passes | views | writeBuffer | copies | createBuffer |
| --- | --- | --- | --- | --- | --- | --- |
| as recorded (scratch buffers) | 8.17, 8.45 | 51 | 77 | 178 | 123 | 15 |
| CLIENT_ARRAY | 5.32, 5.33 | 15 | 16 | 83 | 0 | 0 |
The counts are now pewasm's live ones (15 passes, 16 views, 87 writeBuffer,
no copies).
### CLIENT_ARRAY, live (ejected page, site-gl2 = site30 + this stream's
page; the old path by `GEX_NT_GL_CLIENT_ARRAYS=buffer` on the same site)
| leg order | CLIENT_ARRAY | scratch buffers (old) | pewasm |
| --- | --- | --- | --- |
| j2: A, B, pew | 67.2 fps, 4.64 ms | 64.0 fps, 7.03 ms | 48.5 fps |
| j4: pew, B, A | 65.2 fps, 4.99 ms | 65.8 fps, 6.88 ms | 49.4 fps |
| mean | 66.2 fps, 4.82 ms | 64.9 fps, 6.96 ms | 48.95 fps |
ms = the compositor's gex_gl_submit wall time per produced frame (world-up
+90 s to 330 s; one submit a frame). The compositor's time per frame falls
by 31% (6.96 -> 4.82 ms); world fps moves by 2% (66.2 against 64.9),
inside the legs' spread: as eject-pe.md found, the compositor worker is not
the page's critical path (the main thread is). The ejected page with
CLIENT_ARRAY is 1.35 times pewasm's world fps (66.2 against 48.95).
### What still makes NT syscalls (GEX_NT_GL_STATS, site-gl2, per frame)
623 GL calls a frame reach the layer (the eject9 tree has no client-state
fast path, which master has: glEnable/DisableClientState are 284 of them
there). The rest: glDrawArrays 47, glVertexPointer/glColorPointer/
glTexCoordPointer 142 (pure layer state), glBlendFunc 39, glBindFramebuffer
18, glUniform1f 14, glTexParameteri 12, glViewport 9, glBlendFuncSeparate 8,
glTexSubImage2D 5, wglSwapBuffers 1. 430acec7 moves glBlendFunc,
glBlendFuncSeparate, glViewport, glTexParameteri and glUniform1f to the
fast path. The pointer calls need the layer's array state in guest memory
(as GL_FAST_ARRAYS holds the enables) before the provider can take them.
### After CLIENT_ARRAY (replay profile p3)
About 5.5 ms of CPU a frame: writeTexture 0.85 ms (the five world-pixel
uploads, 839 KiB; pewasm's page pays 0.52 ms for the same), native WebGPU
time 1.1 ms, the rest spread thin over st/mesa and the driver. The largest
named item left is the per-draw label formatting (about 0.23 ms a frame in
the profile; not separable from noise by replay).
## Findings
## Unity's GL stream (Hollow Knight's menu, 2026-10-07 night)
Captured natively on edger (`GEX_GL_CAPTURE` on U's harness:
`tests/games/unity-bench.sh hk ... --fjit`, warm fjit cache; trace
`out/hk-menu.gexgltr`, 587 MB, 3166 frames; the run's menu was 25.6
presents/s with the capture on). Per frame over the last 300 (`ops.mjs`):
- one submit (the frame's PRESENT; GEXGL_SUBMIT_TRACE agrees: 81% of
submits follow PRESENT), 999 commands, 78 KiB;
- 45 draws (35 glDrawElementsBaseVertex, 10 glDrawElements), 47
glBindFramebuffer, 186 glBindBuffer, 160 glTexParameteri, 92
glBindTexture, 58 glUniform4fv;
- 77 glBufferSubData, 58 KiB, all to GL_COPY_WRITE_BUFFER, 53 of them under
256 bytes (per-draw uniform blocks); 54 rewrite a buffer a draw of the
same frame already read, 14 at unaligned offsets (`subdata.mjs`).
**Natively the GL stream is not what limits the menu.** The same trace
replayed on edger's NVIDIA through gex-gl's host (`gl-replay-native`, the
new `crates/gex-gl/examples/replay.rs` with per-opcode timing): 0.84 and
1.03 ms a frame in `Gl::run`, of which glBindFramebuffer is 0.30-0.39 ms
(47 binds) and the draws 0.15-0.20 ms; the whole 3166-frame trace replays in
under 3 s. One round trip a frame. At 25-28 presents/s (35-40 ms a frame)
that is a few percent: U's thread profile (main thread 59-69% of a core,
render thread 25%, no thread saturated) is the place to look, not gex-gl.
**In the browser (Mesa on WebGPU, the same trace, replay page) it is waste,
and fixable in the driver.** Base (wink 5f5572e6 + E4 flags): 4.65 and 4.71
ms of CPU a frame, with 23.5 queue submits, 42 render passes for 45 draws,
12 createRenderPipeline, 66 views, 32 copyBufferToBuffer and 6
copyTextureToTexture a frame. Causes found:
1. Pipelines rebuilt every frame: the pipeline cache's entries were
invalidated by an epoch it shares with the bind group cache, which moves
on every deleted sampler view or sampler state (Unity deletes some every
frame).
2. Unaligned glBufferSubData into a buffer the batch reads submitted the
batch (in-batch staging took whole words only).
3. Feedback snapshots (a draw sampling its own attachment) went through
copy_texture_region, which submits the batch and then its own command
buffer: two submits each.
4. Every rewrite of a uniform buffer the batch reads was staged and copied
in the batch, ending the render pass, although the bytes written are
never the bytes a pending draw reads.
Each was tried as a mesa-wink commit and replayed (r=urep3, last 600
frames, CPU ms of gex_gl_submit per frame, two passes in and reversed order;
the last frame's readback digest equal for every build):
| build | ms a frame | submits | pipelines | copies (buffer) |
| --- | --- | --- | --- | --- |
| base (5f5572e6 + E4) | 5.00, 4.72 | 23.5 | 12 | 32 |
| + pipeline epoch | 4.42, 4.16 | 23.5 | 0 | 32 |
| + unaligned staging | 5.34, 4.82 | 12 | 0 | 44 |
| + snapshots in the batch | 5.39, 5.79 | 2 | 0 | 54 |
| + read/write ranges | 5.30, 5.44 | 2 | 0 | 54 |
Only the pipeline epoch pays: 12% less CPU a frame and no pipeline
creation (which also costs Dawn's GPU-process time the CPU column does not
show). Fewer submits bought by staging more copies is a loss here, and the
range check never applied (Unity's rewrites hit buffers also bound as vertex
or index data, which count as whole-buffer reads). Kept: `$G/mesa-wink`
branch `gl/perf` = 6d3a4cfd only (not pushed; needs the user's approval);
the other three are on `gl/experiments`.
## Noita: E1 on the ejected page, the normal page, and what limits world fps
**E1 (fast-path draws and pointers), ejected page** (site-gl4 = site-gl3 +
F's client-state fast path + 6dd6b3fc; ABBA j7/j8): site-gl4 65.0, 65.1
world fps against site-gl3 70.8, 65.6 and pewasm 48.0, 48.4. No measurable
gain: the main thread is not bound by the remaining GL syscalls.
**What limits world fps (ejected page, worker profiles 100-120 s, site-gl3,
`out/prof-gl3`):** the compositor worker is 66% idle. Noita's main thread is
fully busy, 37% of it parked in `guest_yield` (NtHost::park: waiting for its
job threads), the rest guest code and NT calls (mono_ns clock reads 2.4%,
dlmalloc 1.2%, syscall glue). The seven job workers each spend 55% in the
ConcRT condvar provider (`cond_wait_`) and ~13% in f_724580 (the world
simulation). So the frame is bound by the hand-off between the main thread
and the simulation workers, not by GL and not by one thread's code: the
next lever is the guest's own threading (F's side: wake latency of
cond_wait/park, and the simulation's per-frame critical path).
**The normal gexc page** (tests/nt/browser-noita.mjs through
`$G/np/nplegs.sh`: wt/glpath at merge/glpath, units built at the branch's
keys into target/bm-gl (`$G/build-bm.sh`, seeded from F's bm-s objects:
all 3127 noita.exe objects reused), seed pinned, 4x clock; world fps from
the stats lines, world-up+30 s to the end; legs n1/n2 ran 213 s with F's
default worker profile at 60-90 s, n3/n4 only ~50 s because `PROF=none`
trips browser-noita's AOT-share check, which then ends the run):
| arm | n1 | n2 | n3 | n4 | mean |
| --- | --- | --- | --- | --- | --- |
| CLIENT_ARRAY (default) | 48.6 | 59.8 | 61.0 | 52.5 | 55.5 |
| `GEX_NT_GL_CLIENT_ARRAYS=buffer` | 49.3 | 49.9 | 48.9 | 56.1 | 51.1 |
| pewasm | 45.9 | 45.2 | 49.5 | 48.0 | 47.2 |
The normal page is 1.18 times pewasm's world fps (55.5 against 47.2), and
the new path's mean is 9% above the old one's, but the legs spread over
48-61 world fps, so that difference is not established. A proper ABBA
needs longer legs without the profile and with the AOT check satisfied
(keep a short profile window, or relax NOITA_MIN_AOT for such runs).
+108 -3
View File
@@ -1204,6 +1204,104 @@ crypt (no provider registered), pipe (the x64 architecture check).
build tree must not mix objects from before and after a plugin change.
- **No Wine fork patch** for GL.
**Child processes without the session's shared memory (2026-10-07).**
The user's decision: no cross-process shared memory in gex-eject; Wine is
patched instead. Scope, as found:
- win32u reads the session in six places: the **user handle table**
(`get_user_entry*`, `is_valid_entry`: every HWND check, IsWindow,
GetWindowThreadProcessId, the handle-table walks), **window** objects
(`get_shared_window`: class, DPI, wndproc, GetWindowLong's extra bytes),
**class** objects (class.c: names, GetClassLong), the **desktop**
(`get_shared_desktop`: async key state, cursor position and clip, the
monitor serial), the **queue** (`get_shared_queue`: wake and changed
bits, hook counts) and the thread's **input** (`get_shared_input`:
foreground, active, focus, capture, caret, key state). user32 reads
none directly.
- Wine 11.15 still has requests for part of it (get_key_state for one key,
get_queue_status, set_cursor's position and clip, get_window_info and
get_class_info for one offset, get_window_rectangles), but the forms
that returned the rest were cut when shared memory came in (9.0's
get_key_state returned the whole key array, get_thread_input the
foreground/focus/capture/caret handles, get_window_info the pid, tid and
atom). A fallback per site would re-add those replies across six files.
- So one generic request instead: `read_session_data(offset, size)`
returns the bytes of the session mapping, and a child keeps a local copy
laid out like the mapping; each read sequence refreshes the bytes it is
about to read (a seqlock acquire: the object and up to 512 extra bytes; a
user entry), so the seqlock check that follows passes as it would on the
real mapping. ~60 lines of hand-written change in win32u, server and
protocol.def (128 with the generated protocol files). The process hosting
the server keeps its direct read.
- Two more fork patches came with it: a child process is a copy of the
first, in-process server state included, so `wineserver_session_data`
answers only in the process the session was created in (a child got a
frozen snapshot, and a seqlock caught odd spun for ever); and one
desktop thread per session (a named mutant): with children able to read
the session each started its own, and both waited for the other's shell
window.
- Toolchain fixes found on the way (merge/w3-boot): pelink no longer makes
trapping stubs for clang's other `main` names (crt_main references
them weakly; services.exe and wineboot.exe called a trap), gex defines
them null in the PIC module; and a native module now loads its **delay
imports** at load time (wineboot delay-imports user32 and gdi32: nothing
loaded them, win32u's syscall table was missing in its process and
every NtUser call returned STATUS_INVALID_SYSTEM_SERVICE).
- More, found by the tests (fork, `wasm32-w3`): a child's seqlock wait
refreshes its copy (a copy caught mid-write would spin for ever); a Unix
call with no library handle returns STATUS_DLL_NOT_FOUND instead of
trapping (wineps.drv's DllMain failure path, during DefaultInstall).
- Tests: `tests/nt` **childgui** (user_win as a child: CreateProcess, wait;
`GEX_NT_CHILDREN=1`, native Wine only) passes all 34 checks in the child,
5 runs of 5. The launcher calls GetDesktopWindow first, as a GUI
launcher has a desktop: the desktop thread then lives in the process
hosting the server.
- **Known limit: a desktop hosted in a child** (a console launcher whose
first GUI is its child) is not reliable: about one run in three, a
thread of the child's desktop process (the display settings restorer)
holds the USER lock into a message callback (`user_check_not_lock`
asserts) and the child's first window hangs. Not yet understood.
- wineboot's full install with its steps as children
(`$W/w3tests/install-as-children.sh`: the launcher runs `wineboot.exe
-u`, which runs rundll32 for PreInstall and DefaultInstall) **completes
the install**: the registry has the crypto providers (16 keys), and
tests/nt `crypt` passes on that prefix (6 of 6). wineboot itself then
does not finish in a child: after the install it calls into wininet
(`initialize_internet`), and wininet's code (wininet+0x1d594) calls
TlsGetValue millions of times, each through gex's function-table lookup
(the probe cache never holds it: 3.5 million lookups of one address,
`GEX_FTAB_TRACE=1`). InternetOpenW alone in a child works, so it is
that state of wineboot's process; not yet found. The test is judged by
the registry, not wineboot's exit. As the first process wineboot
finishes.
- **Fixed: wineboot's garbage wine.inf mtime** (5242148, so every first
process reinstalled the prefix): pelink bound wineboot's `_wopen` to
ucrtbase and its `_fstat32` to msvcrt, two descriptor tables; a module's
calls now go to its own C runtime first. `.update-timestamp` now holds
wine.inf's real mtime.
- The registry-fixture idea is dropped.
- Cost, on edger through bench.sh (native gexc; a Windows server call,
GetExitCodeThread, is 74.5 us there): in a child, IsWindow 97.8 us (0.30
us in the process hosting the server), GetAsyncKeyState 572 us (0.76 us:
it refreshes the queue and the desktop object, ~1 KiB each), a frame
polling all 256 keys 157 ms (0.19 ms). The transport alone, as a
pipe-and-epoll round trip like a server request's: 24.7 us between
threads natively and 45.7 us in the browser runtime (Node,
`run-node.mjs`: the page's shim and glue, Workers as worker_threads);
between processes 13.5 us natively, 27.8 us in the browser runtime.
(`$W/rtt/bench-rtt.sh`, `$W/rtt/bench3.txt`.) Not yet measured in a
Chromium page: W3 does not run in the page yet, and no page harness
runs a wasm32 program by itself.
- Who would feel it: only a **child** process. A game started directly is
the process hosting the server and reads directly. A game started by a
launcher (launcher.exe → game.exe, `start /wait`) would be a child: per
frame, GetAsyncKeyState over many keys (Unity's and SDL's input polling,
GameMaker's keyboard_check over the key range) costs ~0.6 ms a key, so a
full 256-key poll is unplayable (157 ms) and a few keys a frame is
~2-5 ms; IsWindow/GetWindowLong on hot paths ~0.1 ms each. Next steps
if a launcher-started game needs it: refresh only the bytes a read uses
(the key byte, not the object), and cache the desktop key state per
keystate_serial.
#### Fork-patch inventory (W3, branch `wasm32-w3` over `wasm32`)
Every change is behind `__wasm32__` (or only built for it). What, why, and
@@ -1229,8 +1327,12 @@ what would let it go:
| user callbacks return through gex's pad | gex watches call-out returns by dispatch slot | never (port) |
| **win32u reads the session from the in-process server** (`wineserver_session_data`) | a wasm memory cannot map the server's pages twice | coherent shared memory (below) |
| **explorer's desktop as a thread** (`gexp_desktop_thread`, win32u starts it once, suspended so it knows the thread, which does not wait for itself; callers wait for the shell window; no shell in that thread; returns, no ExitProcess) | a second Windows process cannot see the session (§3.5) | coherent shared memory across gex processes, so explorer can be its own process again |
| **a child process reads the session through requests** (`read_session_data`, server/mapping.c; win32u keeps a local copy laid out as the mapping, refreshed per seqlock acquire and per user handle entry; the handle-table walk skips entries the process has no object for) | a child has its own wasm memory and cannot map the session the server thread writes (user 2026-10-07: gex-eject never ships cross-process shared memory) | never under that decision; the process hosting the server keeps its direct read |
| `wineserver_session_data` answers only in the process the session was created in | a child process starts as a copy of the first, server state included: it read a frozen snapshot | with the request path (never, under the no-shared-memory decision) |
| one desktop thread per session (a named mutant held by its host process); the wait for its shell window is 60 s | with children reading the session, each process started a desktop thread of its own | explorer as its own process (needs cross-process shared memory) |
| ntdll's Unix-call dispatcher returns STATUS_DLL_NOT_FOUND for a null library handle | a DLL whose Unix library is not linked into the closed-world program still makes calls (wineps.drv's DllMain failure path) and trapped | every such Unix library linked in |
| winebuild (wasm32 export list): a renamed `-import` export is a forward | pelink resolves a call by the export list (user32's ClipCursor is win32u's NtUserClipCursor) | never (tool side of the port) |
| win32u `kbd_tables_wchar_to_vkey` skips an SGCAPS pair's Caps Lock row | VkKeyScan swapped Shift for letters under winewayland (every letter an SGCAPS pair) | the fix upstream (it is not wasm32-specific): upstream-ready, ungated, in `docs/wine-upstream/0001-win32u-VkKeyScan-skip-SGCAPS-caps-row.patch`, **not submitted** (needs the user's OK) |
| win32u `kbd_tables_wchar_to_vkey` skips an SGCAPS pair's Caps Lock row | VkKeyScan swapped Shift for letters under winewayland (every letter an SGCAPS pair) | the fix upstream (it is not wasm32-specific): **not being upstreamed (user 2026-10-07)**: kept as a fork patch; the ungated patch stays in `docs/wine-upstream/0001-win32u-VkKeyScan-skip-SGCAPS-caps-row.patch` for reference |
**Future option for the last two (and the server's 16 MiB block)**, not
built: a SharedArrayBuffer-backed extra memory (wasm multi-memory) holding
@@ -1743,10 +1845,13 @@ Heads:
compositor (`$W/pfx-w3wl`). Output modes (Wine's EmulateModeset) and VkKeyScan done; W3.4 GL: opengl32 on gex-gl through winewayland, glcompat and glfast pass (34 of 40 on `$W/pfx-w3wl`). Wine fork head 74de38ed1ba (mw).
**gate2 landed** (master 36f55bb6), and w3-gate1 (wp_viewporter, master
1b4bd047, merged into merge/w3). **merge/w3 gate head** sent after
`$W/check-w3-gate.sh` (output `$W/w3chk.out`): crates green, elf-prov 5/5
`$W/check-w3-gate.sh` (output `$W/w3chk.out`; the child-session head on
master ac269692: `$W/w3chk2.out`, the same counts plus childgui and the
install-as-children leg): crates green, elf-prov 5/5
native and in the browser, x86-programs 82, nt --no-cgen 83/83 on x86 and
pfx-gui, cgen-prefix 83/83, wasm32 4/4 + busybox both, browser nt 49/49,
W3 i386 32 of 40 (null driver) and 34 of 40 (Wayland + GL). The old merge/elf-prov-gate is deleted. To
W3 i386 32 of 40 (null driver; 33 of 41 with childgui) and 34 of 40
(Wayland + GL); wineboot's install as children leaves the full registry. The old merge/elf-prov-gate is deleted. To
rebuild after new dev commits: same path split (`git checkout <dev
head> -- <paths>` per commit), rerun the checks, merge master once
integ-g lands, send the head.
+1
View File
@@ -198,6 +198,7 @@
#define GEXGL_ALPHA_FUNC 197
#define GEXGL_PUSH_ATTRIB 198
#define GEXGL_POP_ATTRIB 199
#define GEXGL_CLIENT_ARRAY 200 /* the NT layer's; see proto.rs */
#define GEXGL_SURFACE_WAYLAND 0
#define GEXGL_SURFACE_X 1
+1
View File
@@ -319,6 +319,7 @@ mod imp {
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);
nt::opengl::FAST_CLIENT_STATE.store(var("GEX_NT_GL_FAST").as_deref() != Some("nocs"), Ordering::Relaxed);
nt::opengl::CLIENT_ARRAY_OP.store(var("GEX_NT_GL_CLIENT_ARRAYS").as_deref() != Some("buffer"), Ordering::Relaxed);
TRAP_PRINT.store(flag("GEX_NT_TRAP_PRINT"), Ordering::Relaxed);
crate::cgen::TRAP_CALLOUTS.store(flag("GEX_NT_CALLOUT_TRAP"), Ordering::Relaxed);
if let Some(k) = var("GEX_NT_TICK_EVERY").and_then(|v| v.parse().ok()) {
+22
View File
@@ -344,6 +344,28 @@ fn instantiate(
}
}
jit_stats::INSTALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
// clang's three names for a C main (`main`, `__main_argc_argv`,
// `__original_main`): Wine's crt_main references all of them weakly and a
// program defines one. In a position-independent module the others stay
// imports, their address through `GOT.func`: null, as a static link
// leaves them, and the direct calls behind the null check a trap.
const MAIN_NAMES: [&str; 3] = ["main", "__main_argc_argv", "__original_main"];
for imp in module.imports() {
let (m, name) = (imp.module(), imp.name());
if !MAIN_NAMES.contains(&name) || l.get(&mut c, m, name).is_ok() {
continue;
}
if m == "GOT.func" {
let g = i32c(&mut c, 0u64, wasmtime::Mutability::Var)?;
l.define(&mut c, m, name, g)?;
} else if let (true, wasmtime::ExternType::Func(ft)) = (m == f::PIC_MODULE, imp.ty()) {
let label = name.to_string();
let func = wasmtime::Func::new(&mut c, ft, move |_, _, _| {
Err(wasmtime::Error::msg(format!("{label}: a main the program does not define was called")))
});
l.define(&mut c, m, name, func)?;
}
}
let inst = l.instantiate(&mut c, module).context("instantiating a function-table module")?;
if let Some(x) = stateful {
crate::libprov::relocate_once(&mut c, &inst, x)?;
+1
View File
@@ -275,6 +275,7 @@ fn start_ticker(memory: gex_mem::GuestMemory, p: std::sync::Weak<nt::Process>, s
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);
nt::opengl::FAST_CLIENT_STATE.store(std::env::var("GEX_NT_GL_FAST").map_or(true, |v| v != "nocs"), std::sync::atomic::Ordering::Relaxed);
nt::opengl::CLIENT_ARRAY_OP.store(std::env::var("GEX_NT_GL_CLIENT_ARRAYS").map_or(true, |v| v != "buffer"), 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),
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+4
View File
@@ -108,6 +108,10 @@ for arch in x64 x86; do
check $arch/icall 0 "icall: 1284803844 -1238658015 2 3 4" "$b/icall.exe"
# N4: USER without a display.
check $arch/user_win 0 "$allok" "$b/user_win.exe"
# The same as a child process (CreateProcess, waited for): native Wine
# (W3) only, where a child reads the session through requests; the NT
# layer has no child processes. GEX_NT_CHILDREN=1 runs it.
[ -n "${GEX_NT_CHILDREN:-}" ] && check $arch/childgui 0 "${allok}${nl}child exit 0" "$b/childgui.exe"
check $arch/gdi 0 "$allok" "$b/gdi.exe"
check $arch/input 0 "$allok" "$b/input.exe"
check $arch/filemap 0 "$allok" "$b/filemap.exe"
+27
View File
@@ -0,0 +1,27 @@
/* A launcher: a GUI test (user_win.exe, beside this one) as a child process,
* waited for, its exit code passed on. Under native Wine on gex (W3) the
* child cannot map the session's shared memory and reads it through
* wineserver requests (docs/wine-native.md). Its stdout is inherited.
* As a GUI launcher is, this one is a user32 program that has the desktop
* before it starts the child (GetDesktopWindow): the desktop then lives in
* the process hosting the server.
* gex-nt-cc: clang --target=ARCH-w64-mingw32 -fuse-ld=lld */
#include <windows.h>
#include <stdio.h>
int main(int argc, char **argv) {
char cmd[MAX_PATH + 16];
STARTUPINFOA si = {sizeof si};
PROCESS_INFORMATION pi;
DWORD code = 99;
snprintf(cmd, sizeof cmd, "%s", argc > 1 ? argv[1] : "user_win.exe");
GetDesktopWindow();
fflush(stdout);
if (!CreateProcessA(0, cmd, 0, 0, TRUE, 0, 0, 0, &si, &pi)) {
printf("FAIL CreateProcess %lu\n", GetLastError());
return 98;
}
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &code);
printf("child exit %lu\n", code);
return code;
}
+62 -4
View File
@@ -1,7 +1,9 @@
/* 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,
* glEnableClientState, glDisableClientState (which also leave the arrays'
* glBlendFunc, glBlendFuncSeparate, glViewport, glTexParameteri,
* glUniform1f, glEnableClientState, glDisableClientState, glVertexPointer,
* glColorPointer, glTexCoordPointer (into client memory) and glDrawArrays (which also leave the arrays'
* flags where the layer reads them before a draw: client-memory arrays are
* uploaded only when the layer sees them enabled).
* Each is checked by a query (which the NT layer answers after a submit),
@@ -33,9 +35,14 @@ PROC(void, glUseProgram, (GLuint))
PROC(GLint, glGetUniformLocation, (GLuint, const char *))
PROC(void, glUniform1i, (GLint, GLint))
PROC(void, glUniform2f, (GLint, float, float))
PROC(void, glUniform1f, (GLint, float))
PROC(void, glBlendFuncSeparate, (GLenum, GLenum, GLenum, GLenum))
PROC(void, glGetUniformfv, (GLuint, GLint, float *))
PROC(void, glGetUniformiv, (GLuint, GLint, GLint *))
PROC(void, glClientActiveTexture, (GLenum))
PROC(void, glGenBuffers, (GLsizei, GLuint *))
PROC(void, glBindBuffer, (GLenum, GLuint))
PROC(void, glBufferData, (GLenum, ptrdiff_t, const void *, GLenum))
#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); }
@@ -68,8 +75,8 @@ int main(void) {
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);
LOAD(glClientActiveTexture);
LOAD(glGetUniformLocation); LOAD(glUniform1i); LOAD(glUniform2f); LOAD(glUniform1f); LOAD(glBlendFuncSeparate); LOAD(glGetUniformfv); LOAD(glGetUniformiv);
LOAD(glClientActiveTexture); LOAD(glGenBuffers); LOAD(glBindBuffer); LOAD(glBufferData);
CHECK("GL 2.0 entry points", pglActiveTexture && pglUseProgram && pglUniform2f);
if (!pglActiveTexture || !pglUseProgram || !pglUniform2f) return 1;
@@ -80,7 +87,7 @@ int main(void) {
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); }";
const char *fs = "#version 110\nuniform vec2 off; uniform int k; uniform float a; void main() { gl_FragColor = vec4(off, float(k), a); }";
GLuint v = pglCreateShader(0x8B31), fr = pglCreateShader(0x8B30), prog = pglCreateProgram();
pglShaderSource(v, 1, &vs, 0);
pglShaderSource(fr, 1, &fs, 0);
@@ -104,6 +111,36 @@ int main(void) {
pglGetUniformiv(prog, s, &iv);
CHECK("glUniform2f", fv[0] == 0.25f && fv[1] == -1.5f);
CHECK("glUniform1i", iv == 5);
if (pglUniform1f) {
GLint a = pglGetUniformLocation(prog, "a");
float av = 0;
pglUniform1f(a, 0.75f);
pglGetUniformfv(prog, a, &av);
CHECK("glUniform1f", av == 0.75f);
}
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
GLint bs = 0, bd = 0;
glGetIntegerv(GL_BLEND_SRC, &bs);
glGetIntegerv(GL_BLEND_DST, &bd);
CHECK("glBlendFunc", bs == GL_SRC_ALPHA && bd == GL_ONE_MINUS_SRC_ALPHA);
if (pglBlendFuncSeparate) {
GLint v4[4] = {0, 0, 0, 0};
pglBlendFuncSeparate(GL_ONE, GL_ZERO, GL_DST_ALPHA, GL_ONE);
glGetIntegerv(0x80C9, &v4[0]); /* GL_BLEND_SRC_RGB */
glGetIntegerv(0x80C8, &v4[1]); /* GL_BLEND_DST_RGB */
glGetIntegerv(0x80CB, &v4[2]); /* GL_BLEND_SRC_ALPHA */
glGetIntegerv(0x80CA, &v4[3]); /* GL_BLEND_DST_ALPHA */
CHECK("glBlendFuncSeparate", v4[0] == GL_ONE && v4[1] == GL_ZERO && v4[2] == GL_DST_ALPHA && v4[3] == GL_ONE);
}
glViewport(3, 5, 40, 30);
GLint vp[4] = {0, 0, 0, 0};
glGetIntegerv(GL_VIEWPORT, vp);
CHECK("glViewport", vp[0] == 3 && vp[1] == 5 && vp[2] == 40 && vp[3] == 30);
glBindTexture(GL_TEXTURE_2D, tex[0]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
GLint mf = 0;
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, &mf);
CHECK("glTexParameteri", mf == GL_NEAREST);
/* 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
@@ -156,6 +193,27 @@ int main(void) {
glReadPixels(32, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
CHECK("the colour array off", px[0] == 255 && px[1] == 255 && px[2] == 255);
if (px[1] != 255 || px[2] != 255) printf(" pixel %d,%d,%d\n", px[0], px[1], px[2]);
/* Vertices from a buffer (the pointer set while it is bound: the layer's
* command) and colours from client memory (set on the fast path once
* it is unbound): the fast draw uploads only the client one. */
if (pglGenBuffers && pglBindBuffer && pglBufferData) {
static const unsigned char green[12] = {0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255};
GLuint vbo = 0;
pglGenBuffers(1, &vbo);
pglBindBuffer(0x8892, vbo);
pglBufferData(0x8892, sizeof tri, tri, 0x88E4);
glVertexPointer(2, GL_FLOAT, 0, 0);
pglBindBuffer(0x8892, 0);
glEnableClientState(GL_COLOR_ARRAY);
glColorPointer(4, GL_UNSIGNED_BYTE, 0, green);
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3);
glReadPixels(32, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
CHECK("vertices from a buffer, colours from client memory", px[0] == 0 && px[1] == 255 && px[2] == 0);
if (px[0] || px[1] != 255 || px[2]) printf(" pixel %d,%d,%d\n", px[0], px[1], px[2]);
glDisableClientState(GL_COLOR_ARRAY);
glVertexPointer(2, GL_FLOAT, 0, tri);
}
glDisableClientState(GL_VERTEX_ARRAY);
/* The texture-coordinate array of the client active unit (which the
* layer sets, and the fast path reads back). */
+20 -1
View File
@@ -143,6 +143,12 @@ def main():
is_crt = lambda n: n.startswith(('msvcr', 'ucrt')) or n == 'crtdll'
crt = next((i for i in eager if is_crt(i)), None if is_crt(mod.split('.')[0]) else 'ucrtbase')
eager += [c for c in (crt, 'kernel32', 'ntdll') if c and c not in eager]
# and its delay imports, after them: a native module's calls are
# bound in the one module, so nothing would load a delay-loaded
# DLL on first call, and a DLL never loaded never ran its DllMain
# (wineboot's user32 and gdi32: win32u's syscall table missing,
# every NtUser call STATUS_INVALID_SYSTEM_SERVICE)
eager += [i.lstrip('-') for i in makefile_var(mk, 'DELAYIMPORTS') if i.lstrip('-') not in eager]
EAGER[mod] = [i if '.' in i else i + '.dll' for i in eager]
if not dlls:
sys.exit('no DLL objects found (build with winegcc for wasm32)')
@@ -255,7 +261,11 @@ def link(dlls, out, gexrt, others=None):
owners = [o for o in owners if (name, 'defined') in sigs[o]] or owners
if mod in owners:
continue # its own: already resolved inside it
pick = next((o for o in imports if o in owners), owners[0])
# (its own C runtime first, which its import list does not
# name: ucrtbase's _wopen and msvcrt's _fstat32 are two fd
# tables, and wineboot's fstat of a file it had just opened
# failed)
pick = next((o for o in EAGER.get(mod, []) + imports if o in owners), owners[0])
# a caller declaring the function otherwise than its
# definition (an adapter from the .spec: user32's
# NotifyWinEvent is void, win32u's stub returns a word) goes
@@ -413,6 +423,15 @@ def link(dlls, out, gexrt, others=None):
for (n, k), v in sigs[mod].items():
if k == 'undefined' and n in missing:
callers.setdefault(n, v)
# Only weakly referenced (msvcrt's crt_main asks whether the program
# defined __main_argc_argv or __original_main): left undefined, so the
# reference stays null. A trapping stub would make it non-null, and a
# program with the other form of main would call the trap.
strong = set()
for mod, ro, ex, imports in dlls:
nm = subprocess.run(['llvm-nm', '--undefined-only', ro], capture_output=True, text=True).stdout
strong |= {l.split()[-1] for l in nm.splitlines() if l.split()[:1] == ['U']}
callers = {n: v for n, v in callers.items() if n in strong}
gaps = sorted(callers.items())
if gaps:
ctype = {'i': 'int', 'I': 'long long', 'f': 'float', 'd': 'double', 'v': 'void'}
+1 -1
View File
@@ -93,7 +93,7 @@
// `?direct=0` composites always.
if (m.direct && q.get('direct') !== '0') p.set('direct', '1');
// Diagnostics for the page itself, passed through.
for (const k of ['present', 'pointerlock', 'wgslcache', 'mesaenv', 'trapstack', 'sharetrace']) if (q.has(k)) p.set(k, q.get(k));
for (const k of ['present', 'pointerlock', 'wgslcache', 'mesaenv', 'trapstack', 'sharetrace', 'glcapture', 'wgpustats']) if (q.has(k)) p.set(k, q.get(k));
location.replace(`nt.html?${p}`);
} catch (e) {
fail(String(e?.message ?? e));
+8 -2
View File
@@ -14,6 +14,12 @@ endian = 'little'
[properties]
needs_exe_wrapper = true
# SIMD and ThinLTO, and binaryen at -O3 when linking (emcc runs it for a
# linking -O3): pewasm's Mesa overlay (tools/optimization_config.py --opt 3
# --simd --thin-lto). A replayed Noita frame costs 7% less CPU in
# gex_gl_submit (5.09 and 4.99 ms against 5.45 and 5.47; docs/gl-path.md).
[built-in options]
c_args = ['-D_GNU_SOURCE', '-D_POSIX_C_SOURCE=200809L', '-pthread']
cpp_args = ['-D_GNU_SOURCE', '-D_POSIX_C_SOURCE=200809L', '-pthread']
c_args = ['-D_GNU_SOURCE', '-D_POSIX_C_SOURCE=200809L', '-pthread', '-msimd128', '-flto=thin']
cpp_args = ['-D_GNU_SOURCE', '-D_POSIX_C_SOURCE=200809L', '-pthread', '-msimd128', '-flto=thin']
c_link_args = ['-msimd128', '-flto=thin', '-O3']
cpp_link_args = ['-msimd128', '-flto=thin', '-O3']
+41 -1
View File
@@ -172,6 +172,9 @@ enum Op : uint32_t {
ALPHA_FUNC = 197,
/* Protocol 12: the attribute stack. */
PUSH_ATTRIB = 198, POP_ATTRIB = 199,
/* A client-memory vertex array's bytes with its pointer command; see
* crates/gex-gl/src/proto.rs. */
CLIENT_ARRAY = 200,
};
struct Reader {
@@ -208,6 +211,10 @@ struct Reader {
};
std::vector<uint8_t> reply;
/* CLIENT_ARRAY's bytes, one copy per array slot. GL keeps the pointer, so the
* bytes stay until the slot's next CLIENT_ARRAY replaces them -- which the
* guest sends before every draw that reads a client array. */
std::unordered_map<uint32_t, std::vector<uint8_t>> client_arrays;
std::unordered_map<uint32_t, GLuint> buffers, textures, shaders, programs;
/* The head of each shader's source, kept only so a failed compile can show
* what it was handed. A GLSL error names a line and column and nothing else,
@@ -1341,7 +1348,10 @@ bool display_backend() {
* against how many were declined. A frame presents either way, so this
* is what distinguishes "drawing nothing" from "not started". */
static unsigned frames;
if ((frames++ % 600) == 0)
/* Braced: the report below was once outside the `if` by mistake, so
* its ten formatted stderr lines (and the page's console plumbing
* for each) ran on every displayed frame. */
if ((frames++ % 600) == 0) {
fprintf(stderr,
"gex-wgpu: frame %u -- issued %u, declined %u "
"(constant %u, texture %u, vertex %u; last: %s)\n",
@@ -1351,6 +1361,7 @@ bool display_backend() {
gex_wgpu_declined_last_why ? gex_wgpu_declined_last_why
: "-");
gex_wgpu_report_draw_targets();
}
}
GEX_PRESENT_MARK(copy_begin);
WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, nullptr);
@@ -2485,6 +2496,35 @@ bool one(uint32_t op, Reader& a) {
break;
}
case CLIENT_ACTIVE_TEXTURE: glClientActiveTexture(a.u32()); break;
/* { slot, op, n, n words, bytes }: a client-memory array, as pewasm's
* GL takes them -- a pointer into memory, which st/mesa uploads with its
* stream uploader. The guest used to copy each one into a buffer of its
* own with glBufferData before every draw, and a buffer the pending batch
* already reads cannot be rewritten in place: every such upload became a
* staged copy that split the render pass (Noita: 142 a frame, 52 passes
* for 47 draws). `op` is the pointer command (glVertexPointer's,
* glVertexAttribPointer's, ...) and the words are its arguments before
* the pointer, which becomes the bytes' address here; it runs with no
* GL_ARRAY_BUFFER bound, and the guest rebinds its own afterwards. */
case CLIENT_ARRAY: {
const uint32_t slot = a.u32(), inner = a.u32(), n = a.u32();
if (!a.ok || n > 8 || inner == CLIENT_ARRAY) return false;
uint32_t words[8];
for (uint32_t i = 0; i < n; ++i) words[i] = a.u32();
uint32_t len = 0;
const uint8_t* bytes = a.blob(len);
if (!a.ok) return false;
std::vector<uint8_t>& mem = client_arrays[slot];
mem.assign(bytes, bytes + len);
if (mem.empty()) mem.push_back(0);
uint8_t body[8 * 4 + 8];
std::memcpy(body, words, n * 4);
const uint64_t at = reinterpret_cast<uintptr_t>(mem.data());
std::memcpy(body + n * 4, &at, 8);
glBindBuffer(GL_ARRAY_BUFFER, 0);
Reader r{body, body + n * 4 + 8};
return one(inner, r);
}
case ORTHO: {
const GLdouble l = a.f64(), r = a.f64(), b = a.f64();
const GLdouble t = a.f64(), n = a.f64(), f = a.f64();
+1
View File
@@ -1413,6 +1413,7 @@ async function startGl(m, canvas) {
env: m.mesaEnv,
capture: m.glCapture,
wgslCache: m.wgslCache,
wgpuStats: m.wgpuStats,
gpu: await requestGexDevice(),
});
if (!glDevice.ok) throw new Error(glDevice.error || 'the GL device would not start');
+5 -1
View File
@@ -73,8 +73,12 @@ export class Compositor {
// `?wgslcache=all|site|0`: which shader translations Mesa starts with
// (mesa-gl.js #loadWgslCache).
const wgslCache = q?.get('wgslcache');
// `?wgpustats=1`: count Mesa's WebGPU calls for the stats line
// (mesa-gl.js countWebGpu); off by default, it costs time.
const wgpuStats = q?.get('wgpustats') === '1';
this.#worker.postMessage({ kind: 'gl', channel, ...opts, ...(mesaEnv ? { mesaEnv } : {}),
...(glCapture ? { glCapture } : {}), ...(wgslCache ? { wgslCache } : {}) });
...(glCapture ? { glCapture } : {}), ...(wgslCache ? { wgslCache } : {}),
...(wgpuStats ? { wgpuStats } : {}) });
});
}
+1
View File
@@ -195,6 +195,7 @@ export const op = {
ALPHA_FUNC: 197,
PUSH_ATTRIB: 198,
POP_ATTRIB: 199,
CLIENT_ARRAY: 200,
};
export const MAGIC = 0x47584547;
+15 -4
View File
@@ -63,9 +63,10 @@ async function loadGlue(url) {
/** The driver's WebGPU calls by name: [calls, ms inside, bytes, ms waited]
* ("waited" is a promise's time to settle: mapAsync, onSubmittedWorkDone).
* Mesa's own wasm is a few percent of a frame; its cost is these calls, so
* they are counted always -- one performance.now pair per call -- and
* `glState` reports them per frame since it was last asked. */
* `glState` reports them per frame since it was last asked. Only when asked
* for (`?wgpustats=1`, the `wgpuStats` option): the wrapper is a
* performance.now pair and a Map lookup on each of a few thousand calls a
* frame, which was 2.5% of the GL worker's time in Noita's page. */
const wgpuCalls = new Map();
function countWebGpu() {
if (countWebGpu.done || typeof GPUQueue === 'undefined') return;
@@ -124,6 +125,16 @@ function countWebGpu() {
* message to whatever is listening (DevTools, a harness over CDP), on the
* thread that runs Mesa: two thousand batches in a seven-minute Noita run.
* `?gltrace=1` keeps them all. */
/** A page's `?NAME=1`. In a Worker `location` is the worker script's, so
* the compositor passes its page's switches in as options instead. */
function pageFlag(name) {
try {
return globalThis.document !== undefined && new URLSearchParams(location.search).get(name) === '1';
} catch {
return false;
}
}
function quietReports(out) {
let all = false;
try {
@@ -224,7 +235,7 @@ export class MesaGlDevice {
const { adapter, device } = options.gpu ?? await requestGexDevice();
out.#adapter = adapter;
out.#device = device;
countWebGpu();
if (options.wgpuStats ?? pageFlag('wgpustats')) countWebGpu();
// And out of reach of the collector for as long as the page lives.
//
// The fields above were supposed to be enough, and are not: the device