mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
gex-gl: client-memory arrays as CLIENT_ARRAY, not scratch buffers rewritten per draw
The NT layer copied each of Noita's client-side vertex arrays into a scratch buffer of its own with glBufferData before every draw: 142 a frame. The batched gex_wgpu driver cannot rewrite a buffer the pending batch reads, so each became a staged copy that ended the render pass: 51 passes, 123 copyBufferToBuffer, 77 views and 15 createBuffer a frame for 47 draws, against pewasm's 15 passes and no copies (pewasm hands Mesa the pointers). op::CLIENT_ARRAY (200) carries the bytes with their pointer command; the host keeps one copy per array slot and runs the pointer command with the bytes' address and no GL_ARRAY_BUFFER bound, so st/mesa uploads them through its stream uploader like any user array. Both hosts (the browser's Mesa decoder and gex-gl's native one) implement it. The replayed Noita frame (edger, 600 frames, two passes): 8.17 and 8.45 ms of CPU before, 5.32 and 5.33 after; 15 passes, 16 views, 83 writeBuffer, no copies, no buffers made: pewasm's counts. GEX_NT_GL_CLIENT_ARRAYS= buffer keeps the old path for A/B runs. Also in the decoder: the periodic draw-target report sat outside its if for want of braces, so ten formatted stderr lines (and the page's console plumbing for each) ran every displayed frame. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_d2453757b8a84fdbb4ee7c9e093141df
This commit is contained in:
1 parent
21baa051c1
commit
bdb682d9f4
8 files changed
+210
-22
No files matched your search
@@ -462,6 +462,9 @@ 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>>,
|
||||
}
|
||||
|
||||
/// A presented frame: RGBA rows, top first.
|
||||
@@ -600,6 +603,7 @@ impl Gl {
|
||||
keep_frames: false,
|
||||
frame: None,
|
||||
presents: 0,
|
||||
client_arrays: std::collections::HashMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1033,6 +1037,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);
|
||||
@@ -2185,6 +2215,67 @@ mod tests {
|
||||
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.
|
||||
#[test]
|
||||
fn client_array_draws_from_the_bytes_it_carries() {
|
||||
let mut gl = match Gl::new(16, 16) {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
eprintln!("skipping: no host GL ({e})");
|
||||
return;
|
||||
}
|
||||
};
|
||||
const GL_VERTEX_ARRAY: u32 = 0x8074;
|
||||
const GL_COLOR_ARRAY: u32 = 0x8076;
|
||||
const GL_QUADS: u32 = 0x0007;
|
||||
let mut b = Batch::new();
|
||||
b.cmd(op::CONTEXT_CREATE, &[client_api::DESKTOP, 2, 1]);
|
||||
let _ = gl.run(b.bytes());
|
||||
if !gl.is_desktop() {
|
||||
eprintln!("skipping: the host has no desktop OpenGL");
|
||||
return;
|
||||
}
|
||||
let f = |v: f32| v.to_bits();
|
||||
// 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);
|
||||
let mut b = Batch::new();
|
||||
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
|
||||
/// separate blend functions (opcodes that already existed).
|
||||
#[test]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1039,35 +1039,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 +1123,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 +1135,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 +1159,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 {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
+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();
|
||||
|
||||
@@ -195,6 +195,7 @@ export const op = {
|
||||
ALPHA_FUNC: 197,
|
||||
PUSH_ATTRIB: 198,
|
||||
POP_ATTRIB: 199,
|
||||
CLIENT_ARRAY: 200,
|
||||
};
|
||||
|
||||
export const MAGIC = 0x47584547;
|
||||
|
||||
Reference in new issue
Block a user