From 2f6f41eb0528d8f42ddbe460de80a5794e0fd449 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 11:49:34 +0000 Subject: [PATCH] Cheaper height ray and per-draw work in NosVille The height ray is straight up in practice, so most ground triangles are rejected on the ray/triangle test's first edge value, now computed from four coordinates; the difftest gains upright rays and grid-snapped geometry to cover it. Draw constants are packed by the number of lights actually used instead of a fixed 75-texel block, setting a transform to the matrix it already holds no longer recomputes world-view-projection, and the setup loggers check the last match first. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014gSmkXCUwMW8pvXTj7msSU --- recomp/difftest/overrides.c | 12 +++++--- recomp/port/render.c | 3 +- recomp/port/render_gl.inc | 56 +++++++++++++++++++++---------------- recomp/runtime/rt_game.c | 11 ++++++++ 4 files changed, 53 insertions(+), 29 deletions(-) diff --git a/recomp/difftest/overrides.c b/recomp/difftest/overrides.c index ef56e59..8b77fd8 100644 --- a/recomp/difftest/overrides.c +++ b/recomp/difftest/overrides.c @@ -89,13 +89,13 @@ static long test_ray(int cases) { /* ---- height ray over a model's node tree ---- */ static uint32_t height_root, height_from, height_to, height_frame; -static int index_lists; +static int index_lists, upright, grid; static uint32_t make_node(int depth) { uint32_t node = alloc(0x40), move = alloc(12), turn = alloc(8); - for (int k = 0; k < 3; k++) MF32(move + 4 * k) = rnd(depth ? 4 : 0.5f); + for (int k = 0; k < 3; k++) MF32(move + 4 * k) = grid ? rand() % 3 - 1 : rnd(depth ? 4 : 0.5f); for (int k = 0; k < 4; k++) M16(turn + 2 * k) = (uint16_t)((rand() % 65536 - 32768) / (rand() % 4 + 1)); - if (rand() % 3 == 0) { M16(turn) = 0; M16(turn + 2) = 0; M16(turn + 4) = 0; M16(turn + 6) = 32767; } + if (upright || rand() % 3 == 0) { M16(turn) = 0; M16(turn + 2) = 0; M16(turn + 4) = 0; M16(turn + 6) = 32767; } int groups = rand() % 4; uint32_t g = alloc(2 + 7 * groups); M16(g) = (uint16_t)groups; @@ -121,9 +121,11 @@ static long test_height(int cases) { bump = HEAP + 0x1000; uint32_t model = alloc(0x40); int verts = 3 + rand() % 40; + grid = n % 8 >= 6; + upright = n % 4 >= 2; uint32_t vb = alloc(2 + 12 * verts); for (int k = 0; k < verts; k++) - for (int j = 0; j < 3; j++) MF32(vb + 2 + 12 * k + 4 * j) = rnd(j == 1 ? 1 : 6); + for (int j = 0; j < 3; j++) MF32(vb + 2 + 12 * k + 4 * j) = grid ? rand() % 5 - 2 : rnd(j == 1 ? 1 : 6); index_lists = 1 + rand() % 4; uint32_t lists = make_list(index_lists); for (int i = 0; i < index_lists; i++) { @@ -143,6 +145,8 @@ static long test_height(int cases) { MF32(height_to) = MF32(height_from) + rnd(n % 2 ? 4 : 0.4f); MF32(height_to + 4) = MF32(height_from + 4) - 1 + (n % 2 ? rnd(4) : 0); MF32(height_to + 8) = MF32(height_from + 8) + rnd(0.4f); + if (grid) { MF32(height_from) = rand() % 5 - 2; MF32(height_from + 8) = rand() % 5 - 2; } + if (upright) { MF32(height_to) = MF32(height_from); MF32(height_to + 8) = MF32(height_from + 8); } M8(FDIV_CHECKED) = (n & 7) ? 1 : 0; bad += !same(height_setup, f_00489ad0, f_00489ad0_lifted, 0x00489ad0u, 0x00489ce3u, 0); } diff --git a/recomp/port/render.c b/recomp/port/render.c index 49abb33..6c8e9b7 100644 --- a/recomp/port/render.c +++ b/recomp/port/render.c @@ -177,7 +177,8 @@ void r_set_render_target(uint32_t surface) { } void r_set_transform(uint32_t type, uint32_t addr) { Mat* m = type == 256 ? &R.world : type == 2 ? &R.view : type == 3 ? &R.proj : (type >= 16 && type < 24) ? &R.tex[type - 16] : 0; - if (m) memcpy(m->m, GPTR(addr), 64); + if (!m || !memcmp(m->m, GPTR(addr), 64)) return; + memcpy(m->m, GPTR(addr), 64); if (type == 256 || type == 2 || type == 3) wvp_dirty = 1; } void r_get_transform(uint32_t type, uint32_t addr) { diff --git a/recomp/port/render_gl.inc b/recomp/port/render_gl.inc index 4ad926d..507c22e 100644 --- a/recomp/port/render_gl.inc +++ b/recomp/port/render_gl.inc @@ -197,8 +197,8 @@ EM_JS(int, js_gl_init, (int w, int h), { const G = self.__gl = { canvas, gl, program, STREAM, vbo, cur: null, textures: new Map(), buffers: [new Map(), new Map()], vao: gl.createVertexArray(), consts: gl.createTexture(), kring: 0, w, h, last: 0, direct: canvas === self.pageCanvas }; - // Draw constants: 27 blocks of 75 texels per row, and room for two frames' worth so a run never rewrites rows the - // GPU may still be reading. + // Draw constants: rows of 2025 texels, and room for two frames' worth so a run never rewrites rows the GPU may + // still be reading. gl.activeTexture(gl.TEXTURE2); gl.bindTexture(gl.TEXTURE_2D, G.consts); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, 27 * 75, 608); @@ -285,13 +285,13 @@ EM_JS(void, js_gl_run, (const uint8_t* verts, int nverts, const int32_t* cmds, i // names its block. A uniform buffer bound per draw cost the D3D11 backend a constant-buffer copy each time. let kbase = 0; if (nconsts) { - const rows = Math.ceil(nconsts / 27); + const W = 2025, rows = Math.ceil(nconsts / W); if (G.kring + rows > 608) G.kring = 0; - kbase = G.kring * 27; + kbase = G.kring; gl.activeTexture(gl.TEXTURE2); - const full = Math.floor(nconsts / 27), rest = nconsts - full * 27; - if (full) gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, G.kring, 27 * 75, full, gl.RGBA, gl.FLOAT, HEAPF32, consts >> 2); - if (rest) gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, G.kring + full, rest * 75, 1, gl.RGBA, gl.FLOAT, HEAPF32, (consts >> 2) + full * 27 * 300); + const full = Math.floor(nconsts / W), rest = nconsts - full * W; + if (full) gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, G.kring, W, full, gl.RGBA, gl.FLOAT, HEAPF32, consts >> 2); + if (rest) gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, G.kring + full, rest, 1, gl.RGBA, gl.FLOAT, HEAPF32, (consts >> 2) + full * W * 4); gl.activeTexture(gl.TEXTURE0); G.kring += rows; } @@ -374,8 +374,8 @@ EM_JS(void, js_gl_run, (const uint8_t* verts, int nverts, const int32_t* cmds, i } const vb = G.buffers[0].get(HEAP32[g + 1] >>> 0), ib = HEAP32[g + 2] ? G.buffers[1].get(HEAP32[g + 2] >>> 0) : null; if (!vb || (HEAP32[g + 2] && !ib)) continue; - const block = kbase + HEAP32[g + 13]; - if (G.cur.block !== block) { G.cur.block = block; gl.uniform2i(u.uB, (block % 27) * 75, Math.floor(block / 27)); } + const block = HEAP32[g + 13], row = kbase + Math.floor(block / 2025), col = block % 2025; + if (G.cur.block !== block || G.cur.row !== row) { G.cur.block = block; G.cur.row = row; gl.uniform2i(u.uB, col, row); } if (!G.vaoOn) { gl.bindVertexArray(G.vao); G.vaoOn = true; A.fill(-1); } let same = true; for (let i = 0; i < 11; i++) if (A[i] !== HEAP32[g + 1 + i]) { A[i] = HEAP32[g + 1 + i]; same = false; } @@ -595,18 +595,20 @@ static uint32_t gl_texture(uint32_t tex) { typedef struct { int32_t prim, first, count, st[41], g[16]; } GlCmd; static GlCmd gl_cmds[MAX_CMDS]; static uint32_t gl_ncmds, gl_nverts; -/* Vertex-shader constants of buffer draws, GL_CONSTS vec4s per block; a draw whose constants match the previous - block's reuses it. */ -#define GL_CONSTS 75 -static float gl_consts[MAX_CMDS][GL_CONSTS * 4]; -static uint32_t gl_nconsts; +/* Vertex-shader constants of buffer draws, packed as vec4s into rows of GL_KROW (the constant texture's width); a + block holds 27 vec4s plus 6 per light and never straddles a row. A draw whose constants match the previous block's + reuses it. */ +#define GL_KROW 2025 +#define GL_KROWS 300 +static float gl_consts[GL_KROWS * GL_KROW * 4]; +static uint32_t gl_nconsts, gl_klast, gl_klast_size; static void gl_flush(void) { if (!gl_ncmds) return; double t = prof_mark(); - js_gl_run(gl_verts, (int)gl_nverts, (const int32_t*)gl_cmds, (int)gl_ncmds, gl_consts[0], (int)gl_nconsts); + js_gl_run(gl_verts, (int)gl_nverts, (const int32_t*)gl_cmds, (int)gl_ncmds, gl_consts, (int)gl_nconsts); prof_submit += prof_mark() - t; - gl_ncmds = gl_nverts = gl_nconsts = 0; + gl_ncmds = gl_nverts = gl_nconsts = gl_klast_size = 0; } /* The command a draw appends to: the current one when it is a triangle list with the same state, else a new one. */ @@ -642,7 +644,9 @@ static void log_stage_setup(void) { static int nseen; uint32_t key[10] = {R.tss[0][1], R.tss[0][2] << 8 | R.tss[0][3], R.tss[0][4], R.tss[0][11], R.tss[0][24], R.tss[1][1], R.tss[1][2] << 8 | R.tss[1][3], R.tss[1][4], R.tss[1][11], R.tss[1][24]}; - for (int k = 0; k < nseen; k++) if (!memcmp(seen[k], key, sizeof key)) return; + static int last; + if (nseen && !memcmp(seen[last], key, sizeof key)) return; + for (int k = 0; k < nseen; k++) if (!memcmp(seen[k], key, sizeof key)) { last = k; return; } if (nseen >= 48) return; memcpy(seen[nseen++], key, sizeof key); port_log("stage setup: s0 op %u args %04x aop %u tci %x ttff %x | s1 op %u args %04x aop %u tci %x ttff %x | fvf %x", @@ -656,7 +660,9 @@ static void log_raster_setup(void) { uint32_t tex = R.tex_obj[0], s = tex && M32(tex + 8) == 3 ? M32(M32(tex + 40)) : tex; uint32_t key[6] = {R.rs[27] << 16 | R.rs[19] << 8 | R.rs[20], R.rs[15] << 16 | R.rs[25] << 8 | (R.rs[24] & 255), R.rs[7] << 16 | R.rs[14] << 8 | R.rs[23], s ? SFMT(s) : 0, R.rs[28], R.rs[22]}; - for (int k = 0; k < nseen; k++) if (!memcmp(seen[k], key, sizeof key)) return; + static int last; + if (nseen && !memcmp(seen[last], key, sizeof key)) return; + for (int k = 0; k < nseen; k++) if (!memcmp(seen[k], key, sizeof key)) { last = k; return; } if (nseen >= 64) return; memcpy(seen[nseen++], key, sizeof key); port_log("raster setup: blend %u %u->%u | alpha test %u func %u ref %u | z %u write %u func %u | tex fmt %u | fog %u cull %u", @@ -761,10 +767,12 @@ static int gl_buffer_draw(uint32_t prim, uint32_t n, uint32_t first, int32_t bas if (ib) gl_buffer(ib, 1); int32_t st[41]; gl_state(st); - if (gl_ncmds == MAX_CMDS) gl_flush(); + uint32_t size = 27 + 6 * (uint32_t)nlights, at4 = gl_nconsts; + if (at4 % GL_KROW + size > GL_KROW) at4 += GL_KROW - at4 % GL_KROW; + if (gl_ncmds == MAX_CMDS || at4 + size > GL_KROWS * GL_KROW) { gl_flush(); at4 = 0; } - float* c = gl_consts[gl_nconsts]; - memset(c, 0, sizeof gl_consts[0]); + float* c = gl_consts + 4 * at4; + memset(c, 0, 16 * size); put_mat(c, &wvp); put_mat(c + 16, &wv); put_mat(c + 32, &R.world); put_mat(c + 48, &R.tex[0]); put_mat(c + 64, &R.tex[1]); c[80] = LY.vx; c[81] = LY.vy; c[82] = LY.vw; c[83] = LY.vh; c[84] = (float)SW(R.target); c[85] = (float)SH(R.target); @@ -779,9 +787,9 @@ static int gl_buffer_draw(uint32_t prim, uint32_t n, uint32_t first, int32_t bas memcpy(d + 4, L + 1, 12); memcpy(d + 8, L + 9, 12); memcpy(d + 12, L + 13, 12); memcpy(d + 16, L + 16, 12); memcpy(d + 20, L + 21, 12); } - uint32_t block = gl_nconsts; - if (block && !memcmp(c, gl_consts[block - 1], sizeof gl_consts[0])) block--; - else gl_nconsts++; + uint32_t block = at4; + if (gl_klast_size == size && !memcmp(c, gl_consts + 4 * gl_klast, 16 * size)) block = gl_klast; + else { gl_klast = at4; gl_klast_size = size; gl_nconsts = at4 + size; } GlCmd* cmd = &gl_cmds[gl_ncmds++]; static const int32_t gl_prim[7] = {0, 0, 1, 3, 4, 5, 6}; /* POINTS LINES LINE_STRIP TRIANGLES _STRIP _FAN */ diff --git a/recomp/runtime/rt_game.c b/recomp/runtime/rt_game.c index e700996..a063866 100644 --- a/recomp/runtime/rt_game.c +++ b/recomp/runtime/rt_game.c @@ -173,11 +173,22 @@ static int height_node(CPU* RT_RESTRICT c, uint32_t node, uint32_t from, uint32_ guest_call(c, f_0045ba60, 0x0045ba60u, 0x00489b58u, diff, dir_n, c->ecx); Vec3 orig = vload(from_t), dir = vload(dir_n); uint32_t verts = M32(model + 0x10) + 2, out = M32(frame + 8); + int vertical = dir.x == 0 && dir.z == 0; for (uint32_t g = 0; g < count; g++) { uint32_t idx = list_at(lists, (int16_t)M16(groups + 2 + 7 * g + 5)); for (int32_t k = 0; k < (int32_t)M16(idx); k += 3) { uint32_t tri = idx + 2 + 2 * (uint32_t)k; float t; + /* The ray is straight up in practice, and then ray_triangle's u is the 2D cross product of the origin and + the second vertex around the first: the same value bit for bit (but the sign of a zero) whenever u is + not NaN, so rejecting here on u < 0 or NaN rejects exactly what it would. */ + if (vertical) { + uint32_t p0 = verts + 12 * M16(tri), pa = verts + 12 * M16(tri + 4); + float sx = (float)((double)orig.x - MF32(p0)), sz = (float)((double)orig.z - MF32(p0 + 8)); + float px = (float)((double)dir.y * (float)((double)MF32(pa + 8) - MF32(p0 + 8))); + float pz = (float)-((double)dir.y * (float)((double)MF32(pa) - MF32(p0))); + if (!((float)((double)sx * px + (double)sz * pz) >= 0.0)) continue; + } if (!ray_triangle(orig, dir, vload(verts + 12 * M16(tri)), vload(verts + 12 * M16(tri + 4)), vload(verts + 12 * M16(tri + 2)), &t)) continue;