mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
Merge branch 'master' of /home/claude/gex-sandbox/gex into merge/unity
This commit is contained in:
commit
83548eac18
26 files changed
+1187
-47
No files matched your search
@@ -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)) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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),
|
||||
|
||||
Executable
BIN
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||
|
||||
@@ -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
@@ -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). */
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 } : {}) });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user