From c547a92e5a8606bd69f8bf93ef385b32d2227698 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 10:03:05 +0000 Subject: [PATCH 1/9] Name the tutorial web step as the source of the empty CEF window Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014gSmkXCUwMW8pvXTj7msSU --- docs/STATUS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/STATUS.md b/docs/STATUS.md index 1bbe3d6..921c6f2 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -49,7 +49,7 @@ python recomp\web\serve.py --game "C:\Program Files (x86)\Nostale" --user Date: Fri, 30 Oct 2026 10:58:06 +0000 Subject: [PATCH 2/9] Transform, light and fog vertex-buffer draws in the vertex shader on a GPU Vertex and index buffers are uploaded to GL once, and again after a writable Lock; the matrices, material and lights travel as uniforms and a vertex shader generated per layout does what fetch() does. Pretransformed, UP, dynamic or locked draws keep the CPU path, and so does every draw under a software rasteriser, where the shader path was slower on small maps. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014gSmkXCUwMW8pvXTj7msSU --- docs/STATUS.md | 2 +- recomp/port/hle_d3d9.c | 1 + recomp/port/render.c | 7 +- recomp/port/render_gl.inc | 283 ++++++++++++++++++++++++++++++++++---- 4 files changed, 266 insertions(+), 27 deletions(-) diff --git a/docs/STATUS.md b/docs/STATUS.md index 921c6f2..d36b305 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -29,7 +29,7 @@ python recomp\web\serve.py --game "C:\Program Files (x86)\Nostale" --user M32(THIS + 36) || (size && off + size > M32(THIS + 36))) { RET(D3DERR_INVALIDCALL); return; } M32(out) = M32(THIS + 28) + off; M32(THIS + 64)++; /* buffer locks nest */ + if (!(ARG(4) & 0x10)) M32(THIS + 56) = 1; /* not D3DLOCK_READONLY: the GPU copy is stale */ RET(0); } static void b_Unlock(CPU* c) { diff --git a/recomp/port/render.c b/recomp/port/render.c index 13d8b56..49abb33 100644 --- a/recomp/port/render.c +++ b/recomp/port/render.c @@ -730,6 +730,7 @@ static void log_state_combo(void) { } static void gl_draw_list(uint32_t prim, Vtx* v, uint32_t n); +static int gl_buffer_draw(uint32_t prim, uint32_t n, uint32_t first, int32_t base_vertex, int indexed); /* Frame dump (F9 in the page): every draw of the next frame with its state, to debug a scene that renders wrong. */ static int dump_state; /* 1: armed, dump from the next Present; 2: dumping this frame */ @@ -787,7 +788,8 @@ static int reserve(Vtx** buf, uint32_t* cap, uint32_t n) { static void draw_impl_draw(uint32_t prim, uint32_t start, uint32_t count) { layout_begin(); uint32_t n = vertex_count(prim, count); - if (!R.vb || !n || !R.vb_stride || !reserve(&vbuf, &vbuf_cap, n)) return; + if (!R.vb || !n || !R.vb_stride) return; + if (gl_buffer_draw(prim, n, start, 0, 0) || !reserve(&vbuf, &vbuf_cap, n)) return; uint32_t base = M32(R.vb + 28) + R.vb_offset; for (uint32_t k = 0; k < n; k++) fetch(base + (start + k) * R.vb_stride, &vbuf[k]); draw_list(prim, count, vbuf, n); @@ -824,7 +826,8 @@ static void draw_indexed_cached(uint32_t prim, uint32_t count, uint32_t n, uint3 static void draw_impl_draw_indexed(uint32_t prim, int32_t base_vertex, uint32_t start_index, uint32_t count) { layout_begin(); uint32_t n = vertex_count(prim, count); - if (!R.vb || !R.ib || !n || !reserve(&vbuf, &vbuf_cap, n)) return; + if (!R.vb || !R.ib || !n) return; + if (gl_buffer_draw(prim, n, start_index, base_vertex, 1) || !reserve(&vbuf, &vbuf_cap, n)) return; int idx32 = M32(R.ib + 20) == 102; uint32_t ib = M32(R.ib + 28) + start_index * (idx32 ? 4 : 2); draw_indexed_cached(prim, count, n, M32(R.vb + 28) + R.vb_offset, R.vb_stride, ib, idx32, base_vertex); diff --git a/recomp/port/render_gl.inc b/recomp/port/render_gl.inc index ca2065f..09fa7a6 100644 --- a/recomp/port/render_gl.inc +++ b/recomp/port/render_gl.inc @@ -1,5 +1,7 @@ -/* WebGL2 backend, included at the end of render.c (it needs R, Vtx and texel). In the browser worker the vertices - are still transformed by fetch(); the GPU rasterises, shades the two texture stages, blends and depth-tests. */ +/* WebGL2 backend, included at the end of render.c (it needs R, Vtx and texel). On a GPU, draws from vertex buffers + are transformed, lit and fogged by the vertex shader (gl_buffer_draw); everything else, and every draw under a + software rasteriser, is transformed by fetch(). The GPU rasterises, shades the two texture stages, blends and + depth-tests. */ #ifdef __EMSCRIPTEN__ #include @@ -10,11 +12,79 @@ EM_JS(int, js_gl_init, (int w, int h), { const canvas = new OffscreenCanvas(w, h); const gl = canvas.getContext('webgl2', { alpha: false, antialias: false, preserveDrawingBuffer: false, depth: true }); if (!gl) return 0; - postMessage({ type: 'log', text: 'WebGL renderer: ' + report(gl) }); - const vs = '#version 300 es\n' + + const renderer = report(gl); + postMessage({ type: 'log', text: 'WebGL renderer: ' + renderer }); + const STREAM = 1 << 24; + const vsStream = '#version 300 es\n' + 'in vec4 aPos; in vec4 aCol; in vec4 aUV; in float aFog;\n' + 'out vec4 vCol; out vec4 vUV; out float vFog;\n' + 'void main() { gl_Position = aPos; gl_PointSize = 1.0; vCol = aCol.bgra; vUV = aUV; vFog = aFog; }\n'; + // Fixed-function vertex processing for draws from a vertex buffer, the same arithmetic as render.c fetch(). The + // key (gl_buffer_draw) is baked in as constants; uC holds the matrices, viewport, fog, material and lights. + const vsBuffer = (k) => '#version 300 es\n' + + 'precision highp float; precision highp int;\n' + + 'const bool LIT = ' + ((k & 1) !== 0) + ', DIFFUSE = ' + ((k >> 15 & 1) !== 0) + ', NORMAL = ' + ((k >> 16 & 1) !== 0) + ';\n' + + 'const bool DVC = ' + ((k >> 17 & 1) !== 0) + ', AVC = ' + ((k >> 18 & 1) !== 0) + ', EVC = ' + ((k >> 19 & 1) !== 0) + ';\n' + + 'const int FOG = ' + (k >> 1 & 3) + ', LIGHTS = ' + (k >> 20 & 15) + ';\n' + + 'const ivec2 GEN = ivec2(' + (k >> 3 & 3) + ', ' + (k >> 5 & 3) + ');\n' + + 'const ivec2 TT = ivec2(' + (k >> 7 & 7) + ', ' + (k >> 11 & 7) + ');\n' + + 'const bvec2 PROJ = bvec2(' + ((k >> 10 & 1) !== 0) + ', ' + ((k >> 14 & 1) !== 0) + ');\n' + + 'const ivec2 TN = ivec2(' + (k >> 25 & 7) + ', ' + (k >> 28 & 7) + ');\n' + + 'in vec3 aPos; in vec4 aCol; in vec4 aT0; in vec4 aT1; in vec3 aNrm;\n' + + 'uniform vec4 uC[75];\n' + + 'out vec4 vCol; out vec4 vUV; out float vFog;\n' + + 'mat4 M(int i) { return mat4(uC[i], uC[i + 1], uC[i + 2], uC[i + 3]); }\n' + + 'vec4 coords(vec4 t, int n) {\n' + + ' return n == 1 ? vec4(t.x, 0.0, 1.0, 0.0) : n == 2 ? vec4(t.xy, 1.0, 0.0) : n == 3 ? vec4(t.xyz, 1.0) : n == 4 ? t : vec4(0.0, 0.0, 1.0, 0.0);\n' + + '}\n' + + 'vec2 stageUV(int st, vec4 t, vec4 cam, vec4 camN) {\n' + + ' vec4 tc = GEN[st] == 2 ? cam : GEN[st] == 1 ? camN : coords(t, TN[st]);\n' + + ' if (TT[st] == 0) return tc.xy;\n' + + ' vec4 r = M(12 + 4 * st) * tc;\n' + + ' float q = PROJ[st] ? r[TT[st] - 1] : 1.0;\n' + + ' return r.xy / q;\n' + + '}\n' + + 'vec4 light(vec4 p, vec4 vc) {\n' + + ' vec3 n = mat3(M(8)) * aNrm;\n' + + ' float len = length(n);\n' + + ' if (len > 0.0) n /= len;\n' + + ' vec3 wp = (M(8) * p).xyz;\n' + + ' vec4 dsrc = DVC ? vc : uC[23], asrc = AVC ? vc : uC[24], esrc = EVC ? vc : uC[25];\n' + + ' vec3 o = esrc.rgb + uC[26].rgb * asrc.rgb;\n' + + ' for (int i = 0; i < LIGHTS; i++) {\n' + + ' int b = 27 + 6 * i;\n' + + ' vec3 dir; float atten = 1.0;\n' + + ' if (uC[b].x == 3.0) dir = -uC[b + 4].xyz;\n' + + ' else {\n' + + ' dir = uC[b + 3].xyz - wp;\n' + + ' float d = length(dir);\n' + + ' if (uC[b].y > 0.0 && d > uC[b].y) continue;\n' + + ' float a = uC[b + 5].x + uC[b + 5].y * d + uC[b + 5].z * d * d;\n' + + ' atten = a > 0.0 ? 1.0 / a : 1.0;\n' + + ' }\n' + + ' float dl = length(dir);\n' + + ' float ndotl = dl > 0.0 ? max(dot(n, dir) / dl, 0.0) : 0.0;\n' + + ' o += atten * (uC[b + 2].rgb * asrc.rgb + ndotl * uC[b + 1].rgb * dsrc.rgb);\n' + + ' }\n' + + ' return vec4(clamp(o, 0.0, 1.0), clamp(dsrc.a, 0.0, 1.0));\n' + + '}\n' + + 'void main() {\n' + + ' vec4 p = vec4(aPos, 1.0);\n' + + ' vec4 cam = M(4) * p, camN = NORMAL ? M(4) * vec4(aNrm, 0.0) : vec4(0.0);\n' + + ' vec4 vc = DIFFUSE ? aCol.bgra : vec4(1.0);\n' + + ' vCol = LIT ? light(p, vc) : vc;\n' + + ' float d = abs(cam.z), f = 1.0;\n' + + ' if (FOG == 3) f = uC[22].y != uC[22].x ? (uC[22].y - d) / (uC[22].y - uC[22].x) : 1.0;\n' + + ' else if (FOG == 1) f = exp(-uC[22].z * d);\n' + + ' else if (FOG == 2) f = exp(-(uC[22].z * d) * (uC[22].z * d));\n' + + ' vFog = clamp(f, 0.0, 1.0);\n' + + ' vUV = vec4(stageUV(0, aT0, cam, camN), stageUV(1, aT1, cam, camN));\n' + + ' vec4 t = M(0) * p;\n' + + ' float w = t.w != 0.0 ? t.w : 1.0;\n' + + ' vec2 s = vec2(uC[20].x + (t.x / w + 1.0) * 0.5 * uC[20].z, uC[20].y + (1.0 - t.y / w) * 0.5 * uC[20].w);\n' + + ' gl_Position = vec4(((s.x + 0.5) / uC[21].x * 2.0 - 1.0) * w, (1.0 - (s.y + 0.5) / uC[21].y * 2.0) * w, (t.z / w * 2.0 - 1.0) * w, w);\n' + + ' gl_PointSize = 1.0;\n' + + '}\n'; // The texture stage, alpha test and fog setup are constants of each program, one program per distinct setup: // a software rasteriser runs every branch of a shader that reads them from uniforms, several times slower. const fsHead = (k) => '#version 300 es\n' + @@ -80,24 +150,31 @@ EM_JS(int, js_gl_init, (int w, int h), { if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s)); return s; }; - const vsShader = sh(gl.VERTEX_SHADER, vs); + const vertexShaders = new Map(); + const vertexShader = (k) => { + let v = vertexShaders.get(k); + if (!v) vertexShaders.set(k, v = sh(gl.VERTEX_SHADER, k === STREAM ? vsStream : vsBuffer(k))); + return v; + }; + // k: the fragment setup (17 values), then the vertex key (STREAM for recorded vertices). const programs = new Map(); const program = (k) => { const key = k.join(','); let p = programs.get(key); if (p) return p; const prog = gl.createProgram(); - gl.attachShader(prog, vsShader); + gl.attachShader(prog, vertexShader(k[17])); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, fsHead(k) + fsBody)); gl.bindAttribLocation(prog, 0, 'aPos'); gl.bindAttribLocation(prog, 1, 'aCol'); - gl.bindAttribLocation(prog, 2, 'aUV'); - gl.bindAttribLocation(prog, 3, 'aFog'); + gl.bindAttribLocation(prog, 2, k[17] === STREAM ? 'aUV' : 'aT0'); + gl.bindAttribLocation(prog, 3, k[17] === STREAM ? 'aFog' : 'aT1'); + gl.bindAttribLocation(prog, 4, 'aNrm'); gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog)); gl.useProgram(prog); - p = { prog, u: {}, tex0: k[12] === 1, tex1: k[13] === 1 }; - for (const n of ['uTex0', 'uTex1', 'uTFactor', 'uARef', 'uFog']) p.u[n] = gl.getUniformLocation(prog, n); + p = { prog, u: {}, tex0: k[12] === 1, tex1: k[13] === 1, vkey: k[17] }; + for (const n of ['uTex0', 'uTex1', 'uTFactor', 'uARef', 'uFog', 'uC']) p.u[n] = gl.getUniformLocation(prog, n); gl.uniform1i(p.u.uTex0, 0); gl.uniform1i(p.u.uTex1, 1); programs.set(key, p); @@ -112,8 +189,24 @@ EM_JS(int, js_gl_init, (int w, int h), { gl.enableVertexAttribArray(3); gl.vertexAttribPointer(3, 1, gl.FLOAT, false, 40, 36); gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); gl.enable(gl.SCISSOR_TEST); - const G = self.__gl = { canvas, gl, program, cur: null, textures: new Map(), w, h, last: 0 }; - return 1; + const G = self.__gl = { canvas, gl, program, STREAM, vbo, cur: null, textures: new Map(), buffers: [new Map(), new Map()], + vao: gl.createVertexArray(), w, h, last: 0 }; + // 2: a GPU, which takes the vertex work too; 1: a software rasteriser, where the CPU path is faster on small maps. + return /SwiftShader|llvmpipe|lavapipe|Software|Basic Render/i.test(renderer) ? 1 : 2; +}); + +/* A vertex (index 0) or index (1) buffer's contents, keyed by its object. */ +EM_JS(void, js_gl_buffer, (uint32_t id, int index, const uint8_t* data, int size), { + const G = self.__gl, gl = G.gl, map = G.buffers[index]; + let b = map.get(id); + if (!b) { b = gl.createBuffer(); map.set(id, b); } + const target = index ? gl.ELEMENT_ARRAY_BUFFER : gl.ARRAY_BUFFER; + if (index) gl.bindVertexArray(G.vao); + gl.bindBuffer(target, b); + gl.bufferData(target, HEAPU8.subarray(data, data + size), gl.STATIC_DRAW); + if (index) gl.bindVertexArray(null); + gl.bindBuffer(gl.ARRAY_BUFFER, G.vbo); + G.vaoOn = false; }); EM_JS(void, js_gl_texture, (uint32_t id, int w, int h, const uint8_t* rgba), { @@ -145,11 +238,17 @@ EM_JS(void, js_gl_texture_lod, (uint32_t id, int lod), { 24 z func, 25..28 scissor x y w h, 29..33 stage 0 sampler (mag min mip addressu addressv), 34..38 stage 1, 38 fog colour, 39 fog on, 40 cull mode */ /* Runs one recorded command list: the vertices of all its draws are uploaded at once, then each command (GL - primitive, first vertex, count, then 40 state words) sets what changed and draws. */ -EM_JS(void, js_gl_run, (const uint8_t* verts, int nverts, const int32_t* cmds, int ncmds), { + primitive, first vertex, count, 41 state words, then 16 words for a draw from a vertex buffer: see gl_buffer_draw) + sets what changed and draws. */ +EM_JS(void, js_gl_run, (const uint8_t* verts, int nverts, const int32_t* cmds, int ncmds, const float* consts), { const G = self.__gl, gl = G.gl; const P = G.prev || (G.prev = new Int32Array(41).fill(-1)); + const A = G.attribs || (G.attribs = new Int32Array(11).fill(-1)); G.drawn = true; + G.run = (G.run || 0) + 1; + gl.bindVertexArray(null); + gl.bindBuffer(gl.ARRAY_BUFFER, G.vbo); + G.vaoOn = false; // One large buffer takes every list; when it fills up it is orphaned, so the GPU never waits on a range still // being read. const RING = 1 << 24, bytes = nverts * 40; @@ -169,14 +268,15 @@ EM_JS(void, js_gl_run, (const uint8_t* verts, int nverts, const int32_t* cmds, i gl.DST_ALPHA, gl.ONE_MINUS_DST_ALPHA, gl.DST_COLOR, gl.ONE_MINUS_DST_COLOR, gl.SRC_ALPHA_SATURATE]; const eq = [gl.FUNC_ADD, gl.FUNC_ADD, gl.FUNC_SUBTRACT, gl.FUNC_REVERSE_SUBTRACT, gl.MIN, gl.MAX]; for (let k = 0; k < ncmds; k++) { - const cmd = (cmds >> 2) + 44 * k; + const cmd = (cmds >> 2) + 60 * k, g = cmd + 44, buffered = HEAP32[g] !== 0; + const vkey = buffered ? HEAP32[g + 14] : G.STREAM; base = cmd + 3; // Groups are tested one by one (no short-circuit) so the cache stays in step with GL. const s0 = changed(0, 6), s1 = changed(6, 12), at = changed(15, 17), fogOn = changed(39, 40); let switched = false; - if (s0 || s1 || at || fogOn || !G.cur || (S(12) !== 0) !== G.cur.tex0 || (S(13) !== 0) !== G.cur.tex1) { + if (s0 || s1 || at || fogOn || !G.cur || vkey !== G.cur.vkey || (S(12) !== 0) !== G.cur.tex0 || (S(13) !== 0) !== G.cur.tex1) { const setup = [S(0), S(1), S(2), S(3), S(4), S(5), S(6), S(7), S(8), S(9), S(10), S(11), - S(12) ? 1 : 0, S(13) ? 1 : 0, S(15), S(16), S(39) ? 1 : 0]; + S(12) ? 1 : 0, S(13) ? 1 : 0, S(15), S(16), S(39) ? 1 : 0, vkey]; const p = G.program(setup); if (p !== G.cur) { gl.useProgram(p.prog); @@ -228,8 +328,41 @@ EM_JS(void, js_gl_run, (const uint8_t* verts, int nverts, const int32_t* cmds, i if (S(40) === 2 || S(40) === 3) { gl.enable(gl.CULL_FACE); gl.cullFace(S(40) === 2 ? gl.BACK : gl.FRONT); } else gl.disable(gl.CULL_FACE); } - gl.drawArrays(HEAP32[cmd], first + HEAP32[cmd + 1], HEAP32[cmd + 2]); + if (!buffered) { + if (G.vaoOn) { gl.bindVertexArray(null); gl.bindBuffer(gl.ARRAY_BUFFER, G.vbo); G.vaoOn = false; } + gl.drawArrays(HEAP32[cmd], first + HEAP32[cmd + 1], HEAP32[cmd + 2]); + continue; + } + 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; + // The constants block each program last received, so a program switch or a new block sets uC again. + const block = G.run * 65536 + HEAP32[g + 13]; + if (G.cur.block !== block) { + gl.uniform4fv(u.uC, HEAPF32, (consts >> 2) + 300 * HEAP32[g + 13], 4 * HEAP32[g + 15]); + G.cur.block = block; + } + 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; } + if (!same) { + const at = HEAP32[g + 4], stride = HEAP32[g + 5]; + gl.bindBuffer(gl.ARRAY_BUFFER, vb); + gl.enableVertexAttribArray(0); + gl.vertexAttribPointer(0, 3, gl.FLOAT, false, stride, at); + const attr = (loc, off, size, type, norm, x, y, z, w) => { + if (off >= 0) { gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, size, type, norm, stride, at + off); } + else { gl.disableVertexAttribArray(loc); gl.vertexAttrib4f(loc, x, y, z, w); } + }; + attr(1, HEAP32[g + 7], 4, gl.UNSIGNED_BYTE, true, 1, 1, 1, 1); + attr(2, HEAP32[g + 8], HEAP32[g + 9], gl.FLOAT, false, 0, 0, 1, 0); + attr(3, HEAP32[g + 10], HEAP32[g + 11], gl.FLOAT, false, 0, 0, 1, 0); + attr(4, HEAP32[g + 6], 3, gl.FLOAT, false, 0, 0, 0, 0); + if (ib) gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ib); + } + if (ib) gl.drawElements(HEAP32[cmd], HEAP32[cmd + 2], HEAP32[g + 3] ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT, HEAP32[g + 12]); + else gl.drawArrays(HEAP32[cmd], HEAP32[cmd + 1], HEAP32[cmd + 2]); } + if (G.vaoOn) { gl.bindVertexArray(null); gl.bindBuffer(gl.ARRAY_BUFFER, G.vbo); G.vaoOn = false; } }); EM_JS(void, js_gl_clear, (int flags, uint32_t color, float z, int x, int y, int w, int h), { @@ -332,12 +465,16 @@ static uint8_t* gl_verts; static uint8_t* gl_rgba; static uint32_t gl_rgba_size; +static int gl_gpu_vertices; + static void gl_start(uint32_t backbuffer) { gl_backbuffer = backbuffer; - gl_on = js_gl_init((int)SW(backbuffer), (int)SH(backbuffer)); + int r = js_gl_init((int)SW(backbuffer), (int)SH(backbuffer)); + gl_on = r != 0; + gl_gpu_vertices = r == 2; if (gl_on) { gl_verts = malloc(FRAME_VERTS * 40); - port_log("render: WebGL2"); + port_log("render: WebGL2, vertices on the %s", gl_gpu_vertices ? "GPU" : "CPU"); } } @@ -364,28 +501,35 @@ static uint32_t gl_texture(uint32_t tex) { identical state share one command. Anything that must see the drawing so far (a texture upload, a clear, a readback, present) runs the list first. */ #define MAX_CMDS 8192 -typedef struct { int32_t prim, first, count, st[41]; } GlCmd; +/* g: a draw straight from a vertex buffer (gl_buffer_draw), all zero for one from the recorded vertices. */ +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; 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); + js_gl_run(gl_verts, (int)gl_nverts, (const int32_t*)gl_cmds, (int)gl_ncmds, gl_consts[0]); prof_submit += prof_mark() - t; - gl_ncmds = gl_nverts = 0; + gl_ncmds = gl_nverts = gl_nconsts = 0; } /* The command a draw appends to: the current one when it is a triangle list with the same state, else a new one. */ static GlCmd* gl_command(int32_t prim, const int32_t* st, uint32_t verts) { if (gl_nverts + verts > FRAME_VERTS || gl_ncmds == MAX_CMDS) gl_flush(); GlCmd* last = gl_ncmds ? &gl_cmds[gl_ncmds - 1] : 0; - if (last && prim == 4 && last->prim == 4 && !memcmp(last->st, st, sizeof last->st)) return last; + if (last && prim == 4 && last->prim == 4 && !last->g[0] && !memcmp(last->st, st, sizeof last->st)) return last; GlCmd* c = &gl_cmds[gl_ncmds++]; c->prim = prim; c->first = (int32_t)gl_nverts; c->count = 0; memcpy(c->st, st, sizeof c->st); + memset(c->g, 0, sizeof c->g); return c; } @@ -480,6 +624,93 @@ static void gl_draw_list(uint32_t prim, Vtx* v, uint32_t n) { } } +/* A vertex or index buffer's copy on the GPU, uploaded again after a Lock that could write it: +56 is 2 while current. */ +static void gl_buffer(uint32_t b, int index) { + if (M32(b + 56) == 2) return; + gl_flush(); + double t0 = prof_mark(); + js_gl_buffer(b, index, GPTR(M32(b + 28)), (int)M32(b + 36)); + M32(b + 56) = 2; + prof_upload += prof_mark() - t0; +} + +static void put_mat(float* c, const Mat* m) { memcpy(c, m->m, sizeof m->m); } +static void put_color(float* c, uint32_t argb) { + c[0] = ((argb >> 16) & 255) / 255.0f; c[1] = ((argb >> 8) & 255) / 255.0f; c[2] = (argb & 255) / 255.0f; c[3] = (argb >> 24) / 255.0f; +} + +/* Draws from the bound vertex (and index) buffer with the transform, lighting, fog and texture coordinates done by + the vertex shader, so static geometry is uploaded once instead of transformed on the CPU every frame. Returns 0 for + a draw this path does not cover, which then goes through fetch(). */ +static int gl_buffer_draw(uint32_t prim, uint32_t n, uint32_t first, int32_t base_vertex, int indexed) { + if (!gl_gpu_vertices || R.target != gl_backbuffer || dump_state == 2 || LY.pos != 2 || prim < 1 || prim > 6) return 0; + uint32_t vb = R.vb, ib = indexed ? R.ib : 0, stride = R.vb_stride; + if (M32(vb + 64) || (M32(vb + 48) & 0x200) || stride & 3) return 0; /* locked, D3DUSAGE_DYNAMIC, unaligned */ + if (ib && (M32(ib + 64) || (M32(ib + 48) & 0x200))) return 0; + int64_t vbase = (int64_t)R.vb_offset + (int64_t)base_vertex * stride; + if (vbase < 0 || (vbase & 3)) return 0; + if (LY.fog_mode > 3) return 0; + int idx32 = ib && M32(ib + 20) == 102; + int32_t key = LY.lit | LY.fog_mode << 1; + int32_t at[2] = {-1, -1}, tn[2] = {0, 0}; + for (int st = 0; st < STAGES; st++) { + if (LY.stage_gen[st] > 2) return 0; + uint32_t ttff = LY.ttff[st], count = ttff & 7, set = (uint32_t)LY.stage_set[st]; + if (count > 4) return 0; + key |= LY.stage_gen[st] << (3 + 2 * st) | (int32_t)(count | (ttff & 256 ? 8 : 0)) << (7 + 4 * st); + if (!LY.stage_gen[st] && LY.tex_n[set]) { at[st] = (int32_t)LY.tex_at[set]; tn[st] = (int32_t)LY.tex_n[set]; } + key |= tn[st] << (25 + 3 * st); + } + int colorvertex = R.rs[141] && LY.has_diffuse; + int nlights = 0; + for (int i = 0; i < 8; i++) nlights += LY.lit && R.light_on[i]; + key |= LY.has_diffuse << 15 | ((R.fvf & 0x10) != 0) << 16 | (colorvertex && R.rs[145] == 1) << 17 | + (colorvertex && R.rs[147] == 1) << 18 | (colorvertex && R.rs[148] == 1) << 19 | nlights << 20; + + gl_buffer(vb, 0); + if (ib) gl_buffer(ib, 1); + int32_t st[41]; + gl_state(st); + if (gl_ncmds == MAX_CMDS) gl_flush(); + + float* c = gl_consts[gl_nconsts]; + memset(c, 0, sizeof gl_consts[0]); + 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); + c[88] = LY.fog_start; c[89] = LY.fog_end; c[90] = LY.fog_density; + memcpy(c + 92, R.material + 0, 16); memcpy(c + 96, R.material + 4, 16); memcpy(c + 100, R.material + 12, 16); + put_color(c + 104, R.rs[139]); + for (int i = 0, k = 0; i < 8 && LY.lit; i++) { + if (!R.light_on[i]) continue; + const float* L = R.light[i]; + float* d = c + 108 + 24 * k++; + d[0] = L[0]; d[1] = L[19]; + 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++; + + 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 */ + cmd->prim = gl_prim[prim]; + cmd->first = (int32_t)first; + cmd->count = (int32_t)n; + memcpy(cmd->st, st, sizeof cmd->st); + int32_t* g = cmd->g; + g[0] = 1; g[1] = (int32_t)vb; g[2] = (int32_t)ib; g[3] = idx32; g[4] = (int32_t)vbase; g[5] = (int32_t)stride; + g[6] = R.fvf & 0x10 ? (int32_t)LY.normal_at : -1; + g[7] = LY.has_diffuse ? (int32_t)LY.diffuse_at : -1; + g[8] = at[0]; g[9] = tn[0]; g[10] = at[1]; g[11] = tn[1]; + g[12] = (int32_t)(first * (idx32 ? 4u : 2u)); + g[13] = (int32_t)block; g[14] = key; g[15] = 27 + 6 * nlights; + S.draws++; + prof_fetch += prof_mark() - fetch_t0; + return 1; +} + static void gl_clear(int flags, uint32_t color, float z, int x, int y, int w, int h) { gl_flush(); js_gl_clear(flags, color, z, x, y, w, h); @@ -532,6 +763,10 @@ void r_write_back(uint32_t surface) { (void)surface; } int r_on_gpu(uint32_t surface) { (void)surface; return 0; } void r_fill(uint32_t color, int x, int y, int w, int h) { (void)color; (void)x; (void)y; (void)w; (void)h; } static void gl_draw_list(uint32_t prim, Vtx* v, uint32_t n) { (void)prim; (void)v; (void)n; } +static int gl_buffer_draw(uint32_t prim, uint32_t n, uint32_t first, int32_t base_vertex, int indexed) { + (void)prim; (void)n; (void)first; (void)base_vertex; (void)indexed; + return 0; +} static void gl_clear(int flags, uint32_t color, float z, int x, int y, int w, int h) { (void)flags; (void)color; (void)z; (void)x; (void)y; (void)w; (void)h; } From 74d0d2a5262973a3438040aa1dea00ac8baf189e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 11:49:20 +0000 Subject: [PATCH 3/9] Lift accessors after 'mov eax, eax' padding and methods that save Self The gap absorber skipped any 8B or C0 byte as padding, so a routine starting 'mov eax, [eax + n]' (8B 80 ...) right after a function was decoded from its second byte. The misaligned fragment then owned the real routine's call, and the character-info window died on 'no recompiled code for 0065f25e'. Only whole 'mov eax, eax' pairs count as padding now. A relocated pointer to 'push ebx; mov ebx, eax' (a method keeping Self) is now an entry too. One window's handler, placed after a string literal, was otherwise missed (0x5b2fe0). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014gSmkXCUwMW8pvXTj7msSU --- recomp/lift.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/recomp/lift.py b/recomp/lift.py index aac3357..80ebb08 100644 --- a/recomp/lift.py +++ b/recomp/lift.py @@ -881,6 +881,7 @@ def memval(op): PROLOGUE_REGS = {"ebp", "ebx", "esi", "edi"} +SELF_SAVES = {f"{d}, {r}" for d in ("ebx", "esi", "edi") for r in ("eax", "edx", "ecx")} def reloc_code_seeds(L, fns): @@ -940,11 +941,13 @@ def entry_shape(t): for t in sorted(L.reloc_targets): if t in stored or covered(t) or not entry_shape(t): continue - # An aligned pointer to a Delphi prologue is an entry however long the routine is before its ret. + # An aligned pointer to a Delphi prologue is an entry however long the routine is before its ret. A method + # that keeps Self opens with 'push ebx; mov ebx, eax'. head = L.disasm_range(t, t + 2) if t % 4 == 0 and len(head) == 2 and head[0].mnemonic == "push" and head[0].op_str in PROLOGUE_REGS and ( head[1].mnemonic == "push" and head[1].op_str in PROLOGUE_REGS or (head[1].mnemonic, head[1].op_str) == ("mov", "ebp, esp") + or head[1].mnemonic == "mov" and head[1].op_str in SELF_SAVES or head[1].mnemonic in ("add", "sub") and head[1].op_str.startswith("esp, ")): out.append(t) continue @@ -1093,6 +1096,20 @@ def owned(x): "lds", "into", "sldt", "lcall", "ljmp", "retf", "iretd", "int1", "salc", "aam", "aad"} +def skip_padding(L, start, end): + """Past the alignment filler after a function: single nop, int3 or zero bytes and whole 'mov eax, eax' (8B C0). + Skipping a lone 8B would start decoding inside the next function's first 'mov eax, [eax + n]'.""" + while start < end: + b = L.code[start - L.code_lo] + if b in (0x00, 0x90, 0xCC): + start += 1 + elif b == 0x8B and start + 1 < end and L.code[start + 1 - L.code_lo] == 0xC0: + start += 2 + else: + break + return start + + def absorb_gaps(L, fns): """Unowned runs between functions that decode as clean code (no junk opcodes, ending in ret or jmp) are code only reached in ways static analysis misses. Each joins the function before it, with a label at its start.""" @@ -1102,8 +1119,7 @@ def absorb_gaps(L, fns): added = [] for (lo1, hi1, k1), (lo2, _, _) in zip(spans, spans[1:]): start, end = hi1, lo2 - while start < end and L.code[start - L.code_lo] in (0x00, 0x90, 0xCC, 0x8B, 0xC0): - start += 1 + start = skip_padding(L, start, end) if end - start < 4: continue if sum(1 for a in range(start, end - 3) if a in L.reloc_slots) * 4 >= (end - start) // 2: From 18a8f94c52806efba40a640012904d783c3e74fb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 14:52:52 +0000 Subject: [PATCH 4/9] Keep the GPU from falling behind in the browser client Each run's vertex-shader constants go up in one uniform buffer and every draw binds its range, instead of a uniform call per draw that filled the GPU process's command buffer. Frames reach the page through four reused shared-memory slots, read through pixel buffers a frame late, instead of an ImageBitmap per frame that stayed in GPU memory until garbage collection. Textures keep their storage across uploads, and a vertex buffer with a stride the GPU path cannot express falls back to fetch(). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014gSmkXCUwMW8pvXTj7msSU --- recomp/port/render_gl.inc | 116 ++++++++++++++++++++++++++++---------- recomp/web/index.html | 25 ++++---- 2 files changed, 100 insertions(+), 41 deletions(-) diff --git a/recomp/port/render_gl.inc b/recomp/port/render_gl.inc index 09fa7a6..1ad5dd1 100644 --- a/recomp/port/render_gl.inc +++ b/recomp/port/render_gl.inc @@ -20,7 +20,7 @@ EM_JS(int, js_gl_init, (int w, int h), { 'out vec4 vCol; out vec4 vUV; out float vFog;\n' + 'void main() { gl_Position = aPos; gl_PointSize = 1.0; vCol = aCol.bgra; vUV = aUV; vFog = aFog; }\n'; // Fixed-function vertex processing for draws from a vertex buffer, the same arithmetic as render.c fetch(). The - // key (gl_buffer_draw) is baked in as constants; uC holds the matrices, viewport, fog, material and lights. + // key (gl_buffer_draw) is baked in as constants; the block C holds the matrices, viewport, fog, material and lights. const vsBuffer = (k) => '#version 300 es\n' + 'precision highp float; precision highp int;\n' + 'const bool LIT = ' + ((k & 1) !== 0) + ', DIFFUSE = ' + ((k >> 15 & 1) !== 0) + ', NORMAL = ' + ((k >> 16 & 1) !== 0) + ';\n' + @@ -31,7 +31,7 @@ EM_JS(int, js_gl_init, (int w, int h), { 'const bvec2 PROJ = bvec2(' + ((k >> 10 & 1) !== 0) + ', ' + ((k >> 14 & 1) !== 0) + ');\n' + 'const ivec2 TN = ivec2(' + (k >> 25 & 7) + ', ' + (k >> 28 & 7) + ');\n' + 'in vec3 aPos; in vec4 aCol; in vec4 aT0; in vec4 aT1; in vec3 aNrm;\n' + - 'uniform vec4 uC[75];\n' + + 'layout(std140) uniform C { vec4 uC[75]; };\n' + 'out vec4 vCol; out vec4 vUV; out float vFog;\n' + 'mat4 M(int i) { return mat4(uC[i], uC[i + 1], uC[i + 2], uC[i + 3]); }\n' + 'vec4 coords(vec4 t, int n) {\n' + @@ -174,7 +174,8 @@ EM_JS(int, js_gl_init, (int w, int h), { if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog)); gl.useProgram(prog); p = { prog, u: {}, tex0: k[12] === 1, tex1: k[13] === 1, vkey: k[17] }; - for (const n of ['uTex0', 'uTex1', 'uTFactor', 'uARef', 'uFog', 'uC']) p.u[n] = gl.getUniformLocation(prog, n); + for (const n of ['uTex0', 'uTex1', 'uTFactor', 'uARef', 'uFog']) p.u[n] = gl.getUniformLocation(prog, n); + if (k[17] !== STREAM) gl.uniformBlockBinding(prog, gl.getUniformBlockIndex(prog, 'C'), 0); gl.uniform1i(p.u.uTex0, 0); gl.uniform1i(p.u.uTex1, 1); programs.set(key, p); @@ -190,7 +191,9 @@ EM_JS(int, js_gl_init, (int w, int h), { gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); gl.enable(gl.SCISSOR_TEST); const G = self.__gl = { canvas, gl, program, STREAM, vbo, cur: null, textures: new Map(), buffers: [new Map(), new Map()], - vao: gl.createVertexArray(), w, h, last: 0 }; + vao: gl.createVertexArray(), ubo: gl.createBuffer(), w, h, last: 0 }; + // Constant blocks sit 256 bytes apart (gl_buffer_draw); a GPU that needs a coarser offset keeps the CPU path. + if (gl.getParameter(gl.UNIFORM_BUFFER_OFFSET_ALIGNMENT) > 256) return 1; // 2: a GPU, which takes the vertex work too; 1: a software rasteriser, where the CPU path is faster on small maps. return /SwiftShader|llvmpipe|lavapipe|Software|Basic Render/i.test(renderer) ? 1 : 2; }); @@ -211,11 +214,22 @@ EM_JS(void, js_gl_buffer, (uint32_t id, int index, const uint8_t* data, int size EM_JS(void, js_gl_texture, (uint32_t id, int w, int h, const uint8_t* rgba), { const G = self.__gl, gl = G.gl; + // Textures the client rewrites every frame (text, the minimap) keep their storage, so an upload never reallocates. let t = G.textures.get(id); - if (!t) { t = gl.createTexture(); G.textures.set(id, t); } - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, t); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, HEAPU8.subarray(rgba, rgba + w * h * 4)); + if (!t || t.w !== w || t.h !== h) { + if (t) gl.deleteTexture(t); + t = gl.createTexture(); + t.w = w; + t.h = h; + G.textures.set(id, t); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, t); + gl.texStorage2D(gl.TEXTURE_2D, Math.floor(Math.log2(Math.max(w, h))) + 1, gl.RGBA8, w, h); + } else { + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, t); + } + gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, HEAPU8.subarray(rgba, rgba + w * h * 4)); gl.generateMipmap(gl.TEXTURE_2D); if (G.lods && G.lods.has(id)) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_BASE_LEVEL, G.lods.get(id)); if (G.prev) { G.prev[12] = -1; G.prev[13] = -1; } @@ -240,7 +254,7 @@ EM_JS(void, js_gl_texture_lod, (uint32_t id, int lod), { /* Runs one recorded command list: the vertices of all its draws are uploaded at once, then each command (GL primitive, first vertex, count, 41 state words, then 16 words for a draw from a vertex buffer: see gl_buffer_draw) sets what changed and draws. */ -EM_JS(void, js_gl_run, (const uint8_t* verts, int nverts, const int32_t* cmds, int ncmds, const float* consts), { +EM_JS(void, js_gl_run, (const uint8_t* verts, int nverts, const int32_t* cmds, int ncmds, const uint8_t* consts, int cbytes), { const G = self.__gl, gl = G.gl; const P = G.prev || (G.prev = new Int32Array(41).fill(-1)); const A = G.attribs || (G.attribs = new Int32Array(11).fill(-1)); @@ -256,6 +270,13 @@ EM_JS(void, js_gl_run, (const uint8_t* verts, int nverts, const int32_t* cmds, i gl.bufferSubData(gl.ARRAY_BUFFER, G.ring, HEAPU8, verts, bytes); const first = G.ring / 40; G.ring += bytes; + // Every constant block of the run goes up in one upload; each draw then binds its range, instead of thousands of + // uniform calls that filled the GPU process's command buffer. + if (cbytes) { + gl.bindBuffer(gl.UNIFORM_BUFFER, G.ubo); + gl.bufferData(gl.UNIFORM_BUFFER, HEAPU8.subarray(consts, consts + cbytes), gl.STREAM_DRAW); + } + let block = -1; let base = 0; const S = (i) => HEAP32[base + i]; const changed = (from, to) => { @@ -335,12 +356,7 @@ 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; - // The constants block each program last received, so a program switch or a new block sets uC again. - const block = G.run * 65536 + HEAP32[g + 13]; - if (G.cur.block !== block) { - gl.uniform4fv(u.uC, HEAPF32, (consts >> 2) + 300 * HEAP32[g + 13], 4 * HEAP32[g + 15]); - G.cur.block = block; - } + if (block !== HEAP32[g + 13]) { block = HEAP32[g + 13]; gl.bindBufferRange(gl.UNIFORM_BUFFER, 0, G.ubo, block, 1200); } 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; } @@ -434,15 +450,47 @@ EM_JS(void, js_gl_present, (void), { gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, G.keptRb); gl.bindFramebuffer(gl.FRAMEBUFFER, null); } + // Frames go to the page as pixels in shared memory, four slots used in turn: an ImageBitmap per frame stays in GPU + // memory until the page collects garbage, which piled up gigabytes and stalled the GPU, and the worker never + // yields for a canvas transferred from the page to show anything. Each frame is read into one of two pixel buffers + // and fetched a frame later, by when the GPU has finished it, so the read does not wait for the frame just drawn. + const size = G.w * G.h * 4; + if (!G.slots || G.slotW !== G.w || G.slotH !== G.h) { + G.slots = [0, 1, 2, 3].map(() => new Uint8Array(new SharedArrayBuffer(size))); + G.slotW = G.w; G.slotH = G.h; + if (!G.flip) { G.flip = gl.createFramebuffer(); G.flipRb = gl.createRenderbuffer(); G.pbo = [gl.createBuffer(), gl.createBuffer()]; } + gl.bindRenderbuffer(gl.RENDERBUFFER, G.flipRb); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, G.w, G.h); + gl.bindFramebuffer(gl.FRAMEBUFFER, G.flip); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, G.flipRb); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + for (const b of G.pbo) { gl.bindBuffer(gl.PIXEL_PACK_BUFFER, b); gl.bufferData(gl.PIXEL_PACK_BUFFER, size, gl.STREAM_READ); } + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); + G.pending = -1; + postMessage({ type: 'slots', slots: G.slots.map((v) => v.buffer), w: G.w, h: G.h }); + } gl.disable(gl.SCISSOR_TEST); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, G.kept); gl.blitFramebuffer(0, 0, G.w, G.h, 0, 0, G.w, G.h, gl.COLOR_BUFFER_BIT, gl.NEAREST); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, G.flip); + gl.blitFramebuffer(0, 0, G.w, G.h, 0, G.h, G.w, 0, gl.COLOR_BUFFER_BIT, gl.NEAREST); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, G.flip); + const into = G.pending === 0 ? 1 : 0; + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, G.pbo[into]); + gl.readPixels(0, 0, G.w, G.h, gl.RGBA, gl.UNSIGNED_BYTE, 0); + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null); gl.enable(gl.SCISSOR_TEST); G.drawn = false; - const bmp = G.canvas.transferToImageBitmap(); - postMessage({ type: 'bitmap', bmp }, [bmp]); - G.posted = (G.posted || 0) + 1; + if (G.pending >= 0) { + const slot = (G.posted || 0) & 3; + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, G.pbo[G.pending]); + gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, G.slots[slot]); + postMessage({ type: 'pixels', slot }); + G.posted = (G.posted || 0) + 1; + } + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); + G.pending = into; }); /* The frame as 32-bit BGRA rows, top row first. Presenting hands the drawing buffer to the page and leaves it blank, @@ -505,18 +553,21 @@ 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. */ +/* Vertex-shader constants of buffer draws: one block of up to GL_CONSTS vec4s per draw, each starting on a 256-byte + boundary so a draw can bind its own range, and followed by enough room for the full block the shader declares. 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; +#define GL_CONST_SIZE (GL_CONSTS * 16) +static float gl_consts[(MAX_CMDS * 1280 + GL_CONST_SIZE) / 4] __attribute__((aligned(16))); +static uint32_t gl_cbytes, gl_clast, gl_clen; 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]); + js_gl_run(gl_verts, (int)gl_nverts, (const int32_t*)gl_cmds, (int)gl_ncmds, (const uint8_t*)gl_consts, + gl_cbytes ? (int)(gl_cbytes + GL_CONST_SIZE) : 0); prof_submit += prof_mark() - t; - gl_ncmds = gl_nverts = gl_nconsts = 0; + gl_ncmds = gl_nverts = gl_cbytes = gl_clen = 0; } /* The command a draw appends to: the current one when it is a triangle list with the same state, else a new one. */ @@ -645,7 +696,7 @@ static void put_color(float* c, uint32_t argb) { static int gl_buffer_draw(uint32_t prim, uint32_t n, uint32_t first, int32_t base_vertex, int indexed) { if (!gl_gpu_vertices || R.target != gl_backbuffer || dump_state == 2 || LY.pos != 2 || prim < 1 || prim > 6) return 0; uint32_t vb = R.vb, ib = indexed ? R.ib : 0, stride = R.vb_stride; - if (M32(vb + 64) || (M32(vb + 48) & 0x200) || stride & 3) return 0; /* locked, D3DUSAGE_DYNAMIC, unaligned */ + if (!stride || M32(vb + 64) || (M32(vb + 48) & 0x200) || stride & 3) return 0; /* GL reads stride 0 as packed */ if (ib && (M32(ib + 64) || (M32(ib + 48) & 0x200))) return 0; int64_t vbase = (int64_t)R.vb_offset + (int64_t)base_vertex * stride; if (vbase < 0 || (vbase & 3)) return 0; @@ -673,8 +724,8 @@ static int gl_buffer_draw(uint32_t prim, uint32_t n, uint32_t first, int32_t bas gl_state(st); if (gl_ncmds == MAX_CMDS) gl_flush(); - float* c = gl_consts[gl_nconsts]; - memset(c, 0, sizeof gl_consts[0]); + float* c = gl_consts + gl_cbytes / 4; + memset(c, 0, GL_CONST_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); @@ -689,9 +740,12 @@ 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 len = (27u + 6u * (uint32_t)nlights) * 16u, block = gl_clast; + if (len != gl_clen || memcmp(c, gl_consts + gl_clast / 4, len)) { + block = gl_clast = gl_cbytes; + gl_clen = len; + gl_cbytes += (len + 255) & ~255u; + } 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 */ @@ -705,7 +759,7 @@ static int gl_buffer_draw(uint32_t prim, uint32_t n, uint32_t first, int32_t bas g[7] = LY.has_diffuse ? (int32_t)LY.diffuse_at : -1; g[8] = at[0]; g[9] = tn[0]; g[10] = at[1]; g[11] = tn[1]; g[12] = (int32_t)(first * (idx32 ? 4u : 2u)); - g[13] = (int32_t)block; g[14] = key; g[15] = 27 + 6 * nlights; + g[13] = (int32_t)block; g[14] = key; S.draws++; prof_fetch += prof_mark() - fetch_t0; return 1; diff --git a/recomp/web/index.html b/recomp/web/index.html index ca3b812..a25d4b7 100644 --- a/recomp/web/index.html +++ b/recomp/web/index.html @@ -45,8 +45,8 @@