From ee821a71b1a9e5389aa3329ed5a330f18dafd1f6 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 8 Mar 2026 18:51:34 +0100 Subject: [PATCH] [fix](trx-frontend-http): avoid per-draw GPU realloc in WebGL renderer Safari stalls noticeably on gl.bufferData (which reallocates the GPU buffer) when called multiple times per frame. Replace with a pre- allocated scratch Float32Array and gl.bufferSubData, which only uploads new data without reallocating. The GPU buffer is grown with bufferData only when the scratch outgrows it (amortised doubling). Also eliminate the per-draw-call `new Float32Array(vertices)` allocation in favour of scratch.set() + subarray(), removing per-frame GC pressure. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Stan Grams --- .../assets/web/webgl-renderer.js | 76 +++++++++++++------ 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/webgl-renderer.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/webgl-renderer.js index a640d91..4f24645 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/webgl-renderer.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/webgl-renderer.js @@ -184,6 +184,11 @@ null; this.ready = !!this.gl; this.textures = new Map(); + // Reusable scratch buffers — avoids per-draw-call Float32Array allocation + // and lets us use bufferSubData instead of bufferData (no GPU realloc). + this._colorScratch = new Float32Array(4096 * 6); // grows as needed + this._colorGpuSize = 0; // current GPU buffer size (floats) + this._texScratch = new Float32Array(6 * 4); // fixed: 6 verts × (xy+uv) if (!this.ready) return; const gl = this.gl; @@ -233,6 +238,9 @@ this.colorProgram = createProgram(gl, colorVertexSrc, colorFragmentSrc); this.colorBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer); + gl.bufferData(gl.ARRAY_BUFFER, this._colorScratch, gl.DYNAMIC_DRAW); + this._colorGpuSize = this._colorScratch.length; this.colorLoc = { pos: gl.getAttribLocation(this.colorProgram, "a_pos"), color: gl.getAttribLocation(this.colorProgram, "a_color"), @@ -241,6 +249,8 @@ this.textureProgram = createProgram(gl, textureVertexSrc, textureFragmentSrc); this.textureBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer); + gl.bufferData(gl.ARRAY_BUFFER, this._texScratch, gl.DYNAMIC_DRAW); this.textureLoc = { pos: gl.getAttribLocation(this.textureProgram, "a_pos"), uv: gl.getAttribLocation(this.textureProgram, "a_uv"), @@ -282,17 +292,37 @@ _drawColorGeometry(vertices, mode) { if (!this.ready || !vertices || vertices.length === 0) return; const gl = this.gl; - const program = this.colorProgram; - gl.useProgram(program); + const count = vertices.length; + + // Grow scratch buffer if needed (doubles each time to amortise copies). + if (count > this._colorScratch.length) { + let newLen = this._colorScratch.length; + while (newLen < count) newLen *= 2; + this._colorScratch = new Float32Array(newLen); + } + + // Copy into scratch (set() is a fast typed memcpy; avoids new allocation). + this._colorScratch.set(vertices); + const view = this._colorScratch.subarray(0, count); + + gl.useProgram(this.colorProgram); gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer); - const arr = vertices instanceof Float32Array ? vertices : new Float32Array(vertices); - gl.bufferData(gl.ARRAY_BUFFER, arr, gl.STREAM_DRAW); + + // Only reallocate the GPU buffer when it is too small; otherwise use + // bufferSubData which avoids a GPU reallocation (Safari is sensitive to this). + if (count > this._colorGpuSize) { + gl.bufferData(gl.ARRAY_BUFFER, this._colorScratch, gl.DYNAMIC_DRAW); + this._colorGpuSize = this._colorScratch.length; + } else { + gl.bufferSubData(gl.ARRAY_BUFFER, 0, view); + } + gl.enableVertexAttribArray(this.colorLoc.pos); gl.vertexAttribPointer(this.colorLoc.pos, 2, gl.FLOAT, false, 24, 0); gl.enableVertexAttribArray(this.colorLoc.color); gl.vertexAttribPointer(this.colorLoc.color, 4, gl.FLOAT, false, 24, 8); gl.uniform2f(this.colorLoc.resolution, this.canvas.width, this.canvas.height); - gl.drawArrays(mode, 0, arr.length / 6); + gl.drawArrays(mode, 0, count / 6); } fillRect(x, y, w, h, color) { @@ -453,26 +483,26 @@ const entry = this.textures.get(name); if (!entry) return; const gl = this.gl; - const v = flipY - ? [ - x, y, 0, 1, - x + w, y, 1, 1, - x + w, y + h, 1, 0, - x, y, 0, 1, - x + w, y + h, 1, 0, - x, y + h, 0, 0, - ] - : [ - x, y, 0, 0, - x + w, y, 1, 0, - x + w, y + h, 1, 1, - x, y, 0, 0, - x + w, y + h, 1, 1, - x, y + h, 0, 1, - ]; + const s = this._texScratch; + const x2 = x + w, y2 = y + h; + if (flipY) { + s[0]=x; s[1]=y; s[2]=0; s[3]=1; + s[4]=x2; s[5]=y; s[6]=1; s[7]=1; + s[8]=x2; s[9]=y2; s[10]=1;s[11]=0; + s[12]=x; s[13]=y; s[14]=0;s[15]=1; + s[16]=x2;s[17]=y2;s[18]=1;s[19]=0; + s[20]=x; s[21]=y2;s[22]=0;s[23]=0; + } else { + s[0]=x; s[1]=y; s[2]=0; s[3]=0; + s[4]=x2; s[5]=y; s[6]=1; s[7]=0; + s[8]=x2; s[9]=y2; s[10]=1;s[11]=1; + s[12]=x; s[13]=y; s[14]=0;s[15]=0; + s[16]=x2;s[17]=y2;s[18]=1;s[19]=1; + s[20]=x; s[21]=y2;s[22]=0;s[23]=1; + } gl.useProgram(this.textureProgram); gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer); - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(v), gl.STREAM_DRAW); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, s); gl.enableVertexAttribArray(this.textureLoc.pos); gl.vertexAttribPointer(this.textureLoc.pos, 2, gl.FLOAT, false, 16, 0); gl.enableVertexAttribArray(this.textureLoc.uv);