subagent transcript

session 17f09f89 · workflow wf_2d2ac42d
delegated task
CONTEXT — FreeCAD is ported to WebAssembly (emscripten 4.0.12, clang-22, JSPI, native wasm-EH). 3D rendering is Coin3D (Open Inventor) via Quarter, re
▸ Magik
CONTEXT — FreeCAD is ported to WebAssembly (emscripten 4.0.12, clang-22, JSPI, native wasm-EH). 3D rendering is Coin3D (Open Inventor) via Quarter, rendered to an OFFSCREEN FBO then blitted into a Qt widget via QPainter (Qt-for-wasm has NO native GL widget compositing — the whole Qt UI is a SOFTWARE raster surface blitted to an HTML canvas). WebGL2 via emscripten (FULL_ES3, LEGACY_GL_EMULATION off but Coin uses a GLES2 path). COIN_VBO=0 (client vertex arrays), COIN_VERTEX_ARRAYS=1 (VA fast path default-on for indexed face/line sets). qtFontDpi=80. KEY SOURCE LOCATIONS (find others yourself with grep/find): - FreeCAD 3D viewer + FBO->QPainter blit: /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/View3DInventorViewer.cpp (also SoQtQuarterAdaptor / Quarter / SoFCVectorizeAction). The wasm viewport work is documented — grep for __EMSCRIPTEN__ / FC_OS_WASM / grabFramebuffer / QOpenGLFramebufferObject / drawImage / readPixels in src/Gui. - Coin3D source: locate it (find / -name SoAsciiText.cpp -o -name so_glglue.c 2>/dev/null; try /home/magik6k/lcad-wasm/freecad-artifacts, pyside-port/build/pivy-coin, /opt/toolchains). The GLES2/immediate path is in src/rendering/ (SoGL*.cpp) + src/glue/gl.cpp (cc_glglue). SoAsciiText is src/nodes/SoAsciiText.cpp. - Emscripten GL emulation (the ffVertex/ffNormal3/ffSyncContext/emitImm JS functions seen in the profile are emscripten's FIXED-FUNCTION immediate-mode GLES2 emulation): /opt/toolchains/emsdk/upstream/emscripten/src/ (library_webgl.js / library_glemu.js / lib/libglemu / GLImmediate). Confirm where ff*/emitImm come from. - Qt raster paint (RGBA64 conversions): /opt/toolchains/qtsrc/qtbase/src/gui/painting/qdrawhelper.cpp (storeRGBA8888FromRGBA64PM / fetchRGBA8888ToRGBA64PM / comp_func_SourceOver_rgb64). Qt-wasm backingstore: /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmbackingstore.cpp / qwasmwindow.cpp / qwasmscreen.cpp. - The relink/build to test a fix: cmake build at /opt/toolchains/src/freecad-gui-build; relink via /tmp/claude-1000/.../scratchpad/femrelink-s2.sh (this session's scratchpad). Do NOT run builds — analysis only. THE PROFILE (bottom-up, self-time, ~10s capture during hover/pointer interaction at 3-4 fps): Qt SOFTWARE COMPOSITING (~2.3s total): storeRGBA8888FromRGBA64PM 363ms, fetchRGBA8888ToRGBA64PM 250ms, comp_func_SourceOver_rgb64 104ms, fetchTransformed64<TextureBlendType 2> 224ms, qt_rectfill_nonpremul_rgba 132ms, blend_color_generic 147ms, QSvgNode::draw 1092ms(total) / QSvgG::drawCommand 1087ms / QSvgStrokeStyle::apply / QStroker::processCurrentSubpath 124ms, gray_raster_render 247ms, readPixels 113ms, texSubImage2D 83ms, do_mirror 33ms, QImage::copy 66ms. 3D SCENE (Coin, ~1.7s): SoAsciiText::GLRender 314+40ms + SoAsciiTextP::setUpGlyphs + cc_glyph3d_ref + cc_flw_get_vector_advance, immediate-mode glue ffVertex 268ms + ffNormal3 40ms + ffSyncContext 86ms + emitImm + glVertex3f/glVertex3fv, getParameter 390ms + getError 42ms (redundant GL state queries via wasm->JS->WebGL), bufferData 465ms (client-array re-upload every draw?), sogl_render_sphere 130ms + SoSphere::GLRender, SoSeparator::GLRenderBelowPath 1335ms(total) / SoAction::traverse 1562ms(total) / SoSeparator::cullTest 51ms. TIMERS: setTimeout 382ms + clearTimeout 137ms. Event: pointerenter 635ms self / 2730ms total. You have Read/Grep/Glob/Bash (read-only: grep/find/nm ok; do NOT edit files or run builds). Return ONLY the requested structured fields. Be concrete: cite file:line, the exact mechanism, and a SPECIFIC code-level fix (what to change), with an impact estimate (ms saved / % of frame) and effort. LANE: Redundant GL state queries + buffer churn: getParameter 390ms, getError 42ms, bufferData 465ms, ffSyncContext 86ms. Investigate: synchronous glGet*/glGetError and per-draw bufferData are very expensive in wasm (each is a wasm->JS->WebGL round-trip, and glGetError forces a GPU flush/sync). - getParameter/getError: who calls them per-frame? Coin's cc_glglue queries GL limits/state; SoGLLazyElement or the error-checking in the GL glue. Is there per-draw glGetError (Coin's COIN_GLERROR_CHECKING or a debug path) that can be disabled? Is cc_glglue re-querying context params instead of caching? grep Coin src/glue/gl.cpp + rendering for glGetError/glGetInteger per frame. - bufferData 465ms: with COIN_VBO=0 (client arrays), emscripten's GL layer uploads client vertex arrays to a temp VBO via bufferData on EVERY draw call (FULL_ES3 client-array emulation). Confirm this is the mechanism. Could persistent VBOs for static geometry (enable COIN_VBO with bufferData, avoiding glMapBuffer) eliminate per-frame re-upload? Or does emscripten offer a client-array fast path? Check emscripten library_webgl.js client-array handling. Propose: (a) disable any per-frame glGetError; (b) ensure cc_glglue caches state; (c) evaluate persistent-VBO for static geometry to kill per-frame bufferData. Estimate ms saved + risk.
Fable
thinking
I need to dig into Coin3D's GL integration code and how emscripten handles client arrays to verify these specific claims.
Fable
⚙ Bash {"command": "find / -name \"gl.cpp\" -path \"*glue*\" 2>/dev/null; echo \"---\"; find / -name \"so_glglue.c\" 2>/dev/null; echo \"---COIN ROOT---\"; find / -type d -name \"Coin*\" 2>/dev/null | head; find / -name \"SoGLLazyElement.cpp\" 2>/…
Fable
⚙ Bash {"command": "find / -name \"library_webgl.js\" 2>/dev/null; echo \"---glemu---\"; find / -name \"library_glemu.js\" 2>/dev/null; find /opt/toolchains/emsdk -name \"*.js\" -path \"*emscripten*\" 2>/dev/null | grep -iE \"glemu|webgl\" | head"…
tool result
---glemu---
/opt/toolchains/emsdk/upstream/emscripten/src/webGLClient.js
/opt/toolchains/emsdk/upstream/emscripten/src/webGLWorker.js
/opt/toolchains/emsdk/upstream/emscripten/test/minimal_webgl/library_js.js
/opt/toolchains/emsdk/upstream/emscripten/src/lib/libhtml5_webgl.js
/opt/toolchains/emsdk/upstream/emscripten/src/lib/libwebgl.js
/opt/toolchains/emsdk/upstream/emscripten/src/lib/libwebgl2.js
/opt/toolchains/emsdk/upstream/emscripten/src/lib/libglemu.js
/opt/toolchains/emsdk/upstream/emscr  …[truncated]
tool result
/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin/src/glue/gl.cpp
---
---COIN ROOT---
/opt/toolchains/src/freecad-gui-build/src/3rdParty/coin/src/Coin_autogen
/opt/toolchains/src/freecad-gui-build/src/3rdParty/coin/src/CMakeFiles/Coin_autogen.dir
/opt/toolchains/src/freecad-gui-build/src/3rdParty/coin/src/CMakeFiles/Coin.dir
/opt/toolchains/src/freecad-gui-build/src/3rdParty/coin/src/CMakeFiles/Coin.dir/Coin_autogen
/home/magik6k/lcad-wasm/freecad-artifacts.bad/opt-toolchains/src/fr  …[truncated]
Fable
thinking
I'm examining how Coin GL handles glGetError and glGet* calls, and looking into emscripten's approach to managing client-side arrays.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -rn \"glGetError\\|GLERROR_CHECKING\\|cc_glerror\" src/glue/gl.cpp | head -40", "description": "Find glGetError usage in Coin glue"}
tool result
src/glue/gl.cpp:2229:      This problem manifests itself in the form of a glGetError()
src/glue/gl.cpp:2381:    glerr = glGetError();
src/glue/gl.cpp:2387:      glerr = glGetError();
src/glue/gl.cpp:2399:    assert(glGetError() == GL_NO_ERROR && "GL error when calling glGetString() -- no current GL context?");
src/glue/gl.cpp:2496:         glGetError() to GL_INVALID_VALUE if this size is attempted
src/glue/gl.cpp:2515:         glGetError() to GL_INVALID_VALUE if this size is attempted
src/glue/g  …[truncated]
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -rn \"glGetError\\|GLERROR_CHECKING\\|glFlush\\|glFinish\" src/rendering/*.cpp | head -40", "description": "Find glGetError in Coin rendering"}
tool result
src/rendering/CoinOffscreenGLCanvas.cpp:403:  glFlush(); glFinish();
src/rendering/CoinOffscreenGLCanvas.cpp:430:  glFlush(); glFinish();
src/rendering/SoGLNurbs.cpp:955:  // We check for glGetError() at the end of this function, so we
src/rendering/SoGLNurbs.cpp:963:                         "pre GLU-calls, glGetError()s => '%s'",
src/rendering/SoGLNurbs.cpp:1253:                         "post GLU-calls, glGetError()s => '%s'",
src/rendering/SoGLNurbs.cpp:1285:  // We check for glGetError() at t  …[truncated]
Fable
thinking
I'm investigating the cc_glglue_glerror_check function and tracing where that 390ms getParameter bottleneck is coming from — specifically looking for which code is calling glGetInteger or glGetFloat repeatedly during rendering.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -rn \"cc_glglue_glerror_check\\|glglue_glerror\\|COIN_GLERROR\\|GLERROR_CHECKING\" src/ | head -30", "description": "Find glerror check usage across Coin"}
tool result
src/caches/SoGLCacheList.cpp:212:        // COIN_GLERROR_DEBUGGING to "1" to see if the error comes
src/doc/Coin_environment.dox:216:  \li \ref COIN_GLERROR_DEBUGGING
src/doc/Coin_environment.dox:429:EnvironmentVariable COIN_GLERROR_DEBUGGING;
src/doc/Coin_environment.dox:849:  \var EnvironmentVariable COIN_GLERROR_DEBUGGING
src/nodes/SoGroup.cpp:663:      // variable COIN_GLERROR_DEBUGGING to "1" to see exactly which
src/shaders/SoGLSLShaderObject.cpp:208:  // only do a glFlush if COIN_GLERROR_  …[truncated]
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -rn \"glGetInteger\\|glGetFloat\\|glGetBoolean\\|glIsEnabled\\|glGetString\" src/elements/GL/SoGLLazyElement.cpp src/rendering/SoGL.cpp | head -40", "descr…
tool result
src/elements/GL/SoGLLazyElement.cpp:428:  glGetBooleanv(GL_RGBA_MODE, &rgba);
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
COIN_GLERROR_DEBUGGING is disabled by default, so those timing spikes for glGetError and getParameter are coming from emscripten's GL layer rather than Coin itself. I need to dig into emscripten's libwebgl.js and libglemu.js to see where these calls are happening in the client-array and immediate-mode rendering paths, particularly around the ffVertex and emitImm functions in GLImmediate.

The bufferData call taking 465ms suggests that client array emulation is uploading to a temporary VBO on every draw call, so let me search through libglemu.js to confirm this behavior.
Fable
⚙ Bash {"command": "cd /opt/toolchains/emsdk/upstream/emscripten/src/lib; grep -n \"bufferData\\|getError\\|getParameter\\|clientBuffers\\|ffVertex\\|emitImm\\|ffSyncContext\\|preDrawHandleClientVertexAttribBindings\\|MAX_TEMP\" libglemu.js | head…
tool result
351:            var result = GLctx.getParameter(GLctx.MAX_VERTEX_UNIFORM_VECTORS);
356:            var result = GLctx.getParameter(GLctx.MAX_FRAGMENT_UNIFORM_VECTORS);
361:            var result = GLctx.getParameter(GLctx.MAX_VARYING_VECTORS);
1572:            maxTexUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
2423:            var maxVertexAttribs = GLctx.getParameter(GLctx.MAX_VERTEX_ATTRIBS);
2454:            var prevBoundProg = GLctx.getParameter(GLctx.CURRENT_PROGRAM);
2533:           …[truncated]
Fable
⚙ Bash {"command": "cd /opt/toolchains/emsdk/upstream/emscripten/src/lib; grep -n \"bufferData\\|getError\\|getParameter\\|clientBuffers\\|__glGetBufferBinding\\|preDrawHandleClientVertexAttribBindings\\|tempVertexBuffers\\|_glDrawArrays\\|_glDraw…
tool result
415:      context.tempVertexBuffers1 = [];
416:      context.tempVertexBuffers2 = [];
417:      context.tempVertexBuffers1.length = context.tempVertexBuffers2.length = largestIndex+1;
424:        context.tempVertexBuffers1[i] = [];
425:        context.tempVertexBuffers2[i] = [];
426:        var ringbuffer1 = context.tempVertexBuffers1[i];
427:        var ringbuffer2 = context.tempVertexBuffers2[i];
456:        context.GLctx.bufferData(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/, quadIndexes, 0x88E4 /*GL_  …[truncated]
Fable
thinking
I'm diving into the client-array upload mechanism, specifically examining how the temporary vertex buffer gets created and bound during each draw call, along with how the client vertex attribute bindings are handled beforehand.
Fable
⚙ Read {"file_path": "/opt/toolchains/emsdk/upstream/emscripten/src/lib/libwebgl.js", "offset": 460, "limit": 140}
tool result
460	
461	    getTempVertexBuffer: (sizeBytes) => {
462	      var idx = GL.log2ceilLookup(sizeBytes);
463	      var ringbuffer = GL.currentContext.tempVertexBuffers1[idx];
464	      var nextFreeBufferIndex = GL.currentContext.tempVertexBufferCounters1[idx];
465	      GL.currentContext.tempVertexBufferCounters1[idx] = (GL.currentContext.tempVertexBufferCounters1[idx]+1) & (GL.numTempVertexBuffersPerSize-1);
466	      var vbo = ringbuffer[nextFreeBufferIndex];
467	      if (vbo) {
468	        retur  …[truncated]
Fable
thinking
The key mechanism here is how client-side vertex attribute arrays are handled in FULL_ES2/FULL_ES3 emulation. On every draw call, the system runs through each enabled client-side attribute, grabs a temporary vertex buffer, and uploads the client array data via bufferSubData. Since the ring buffer gets pre-allocated after the first frame, subsequent calls just reuse the cached buffer instead of allocating new ones.

But the profile is showing bufferData taking 465ms, not bufferSubData, which is puzzling. Let me reconsider whether there's a different code path at play here — maybe the immediate mode path in libglemu.js is the culprit. The immediate mode rendering (used by things like SoAsciiTe  …
Fable
⚙ Bash {"command": "cd /opt/toolchains/emsdk/upstream/emscripten/src/lib; sed -n '2500,2560p' libglemu.js", "description": "Inspect glemu immediate flush buffer upload"}
tool result
            this.hasClipPlane = (this.hasClipPlane || this.clipPlaneEquationLocation[clipPlaneId]);
          }

          this.hasLighting = GLEmulation.lightingEnabled;
          this.lightModelAmbientLocation = GLctx.getUniformLocation(this.program, 'u_lightModelAmbient');
          this.materialAmbientLocation = GLctx.getUniformLocation(this.program, 'u_materialAmbient');
          this.materialDiffuseLocation = GLctx.getUniformLocation(this.program, 'u_materialDiffuse');
          this.mate  …[truncated]
Fable
⚙ Bash {"command": "cd /opt/toolchains/emsdk/upstream/emscripten/src/lib; sed -n '2560,2610p' libglemu.js", "description": "Immediate flush bufferSubData"}
tool result
            GLctx.bufferSubData(GLctx.ARRAY_BUFFER, start, GLImmediate.vertexData.subarray(start >> 2, end >> 2));
          }
#if GL_UNSAFE_OPTS
          if (canSkip) return;
          GLImmediate.lastRenderer = this;
          GLImmediate.lastProgram = GL.currProgram || this.program;
          GLImmediate.lastStride = GLImmediate.stride;
          GLImmediate.matricesModified = false;
#endif

          if (!GL.currProgram) {
            if (GLImmediate.fixedFunctionProgram != this.program) {
  …[truncated]
Fable
thinking
I'm seeing that the immediate mode rendering path through glBegin/glVertex3f is the main bottleneck — each vertex call in JavaScript adds up to 268ms of overhead as it populates GLImmediate.vertexData. Let me check how FreeCAD configures vertex arrays versus immediate mode in the SoGL implementation to understand whether it's using client-side arrays or staying in immediate mode. I should search through Coin's codebase to find where getParameter is being called frequently in the rendering pipeline, particularly in hot paths like SoGLRenderAction and viewport setup during traversal.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -rn \"glGetIntegerv\\|glGetFloatv\\|glGetBooleanv\\|glGetString\\b\" src/nodes/SoAsciiText.cpp src/shapenodes/*.cpp src/elements/GL/*.cpp src/actions/SoGLR…
tool result
src/elements/GL/SoGLClipPlaneElement.cpp:119:  glGetIntegerv(GL_MAX_CLIP_PLANES, &val);
src/shapenodes/SoIndexedMarkerSet.cpp:220:  glGetIntegerv(GL_MAX_CLIP_PLANES, &numPlanes);
src/shapenodes/SoImage.cpp:456:    glGetFloatv(GL_ZOOM_X, &oldzx);
src/shapenodes/SoImage.cpp:457:    glGetFloatv(GL_ZOOM_Y, &oldzy);
src/elements/GL/SoGLMultiTextureImageElement.cpp:381:  calling glGetIntegerv(GL_MAX_TEXTURE_SIZE).
src/elements/GL/SoGLMultiTextureImageElement.cpp:402:  glGetIntegerv(GL_MAX_TEXTURE_SIZE  …[truncated]
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -rln \"glGetIntegerv\\|glGetFloatv\\|glGetBooleanv\" src/elements/ | head; echo \"=== SoAsciiText glBegin/glVertex ===\"; grep -n \"glBegin\\|glVertex\\|gl…
tool result
src/elements/GL/SoGLPointSizeElement.cpp
src/elements/GL/SoGLLazyElement.cpp
src/elements/GL/SoGLClipPlaneElement.cpp
src/elements/GL/SoGLLightIdElement.cpp
src/elements/GL/SoGLMultiTextureImageElement.cpp
src/elements/GL/SoGLLineWidthElement.cpp
=== SoAsciiText glBegin/glVertex ===
ugrep: warning: src/nodes/SoAsciiText.cpp: No such file or directory
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I'm noticing some potential performance issues with OpenGL calls that aren't compatible with WebGL or GLES2 — specifically `glGetBooleanv(GL_RGBA_MODE)` in SoGLLazyElement that could trigger errors each time it runs. I should check how frequently this gets called and look at the surrounding code. There are also similar concerns with `GL_MAX_CLIP_PLANES` and `GL_MAX_LIGHTS` queries in the clip plane and light ID elements that seem to be executing repeatedly.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; sed -n '410,445p' src/elements/GL/SoGLLazyElement.cpp", "description": "Context of GL_RGBA_MODE query"}
tool result
  this->glstate.transpnodeid = 0;
  this->packedpointer = NULL;
  // when doing screen door rendering, we need to always supply 0xff as alpha.
  this->transpmask = (this->coinstate.transptype == SoGLRenderAction::SCREEN_DOOR) ? 0xff : 0x00;
  this->colorpacker = NULL;
  this->precachestate = NULL;
  this->postcachestate = NULL;
  this->opencacheflags = 0;

  // initialize this here to avoid UMR reports from
  // Purify. cachebitmask is updated even when there are no open
  // caches. It is only   …[truncated]
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; find src -name \"SoAsciiText.cpp\"; find src -iname \"*asciitext*\"", "description": "Find SoAsciiText source"}
tool result
src/shapenodes/SoAsciiText.cpp
src/shapenodes/SoAsciiText.cpp
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I'm noticing that `glGetBooleanv(GL_RGBA_MODE)` in the lazy element initialization is causing GL errors in GLES2/WebGL since those constants don't exist there, and each call triggers a round-trip that returns undefined. This happens during init rather than per draw call, so it's probably not the source of the 390ms bottleneck. Now I'm shifting focus to examine SoAsciiText.

GLRender, which is the largest self-time consumer at 314+40ms — it uses immediate mode rendering with vector glyphs through glBegin/glVertex calls.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -n \"glBegin\\|glVertex\\|glNormal\\|glEnd\\|GLRender\\|render(\\|beginShape\\|glGenBuffers\\|glBindBuffer\\|glDrawArrays\\|COIN_VBO\\|SoVertexArray\\|vbo\…
tool result
151:#include <Inventor/actions/SoGLRenderAction.h>
320:SoAsciiText::GLRender(SoGLRenderAction * action)
322:  if (!this->shouldGLRender(action)) 
335:      SoDebugError::postWarning("SoAsciiText::GLRender",
364:      SoDebugError::postWarning("SoAsciiText::GLRender",
372:  glBegin(GL_TRIANGLES);
373:  glNormal3f(0.0f, 0.0f, 1.0f);
435:        glVertex3f(v0[0] * fontspec->size + xpos, v0[1] * fontspec->size + ypos, 0.0f);
440:        glVertex3f(v1[0] * fontspec->size + xpos, v1[1] * fontspec->siz  …[truncated]
Fable
thinking
So the bottleneck is in how SoAsciiText renders glyphs—each triangle vertex triggers a JavaScript call through emscripten that accumulates vertex data, and when the glyph finishes rendering, it flushes that data to a GPU buffer. The 268ms cost comes from this repeated cycle of building and flushing vertex buffers for every glyph. Now I need to figure out which text strings are actually being rendered to understand where the time is going.

During interaction in the 3D view, dimension labels and UI elements like the navigation cube are constantly re-rendering, and across those 30-40 frames captured, each frame spends roughly 8-10ms just on this text rendering. The bigger issue is likely the b  …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; echo \"=== SoVBO / vertexarray decision ===\"; grep -rn \"COIN_VERTEX_ARRAYS\\|COIN_VBO\\|glDrawArrays\\|glDrawElements\\|glGenBuffers\\|SoGLVBOElement\\|should…
tool result
=== SoVBO / vertexarray decision ===
=== SoVBO render ===
src/rendering/SoVBO.cpp
src/elements/GL/SoGLVBOElement.cpp
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -rn \"glDrawElements\\|glDrawArrays\\|glVertexPointer\\|glNormalPointer\\|glEnableClientState\\|bindBuffer\\|glBindBuffer\" src/rendering/SoGL.cpp | head; …
tool result
=== how VA path calls GL ===
src/rendering/SoVertexArrayIndexer.cpp
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -n \"glDrawElements\\|glVertexPointer\\|glNormalPointer\\|glEnableClientState\\|bindBuffer\\|VBO\\|vbo\\|bufferData\\|render(\" src/rendering/SoVertexArray…
tool result
50:#include "rendering/SoVBO.h"
65:    vbo(NULL),
75:  delete this->vbo;
248:SoVertexArrayIndexer::render(SoState * state, const SbBool renderasvbo, const uint32_t contextid)
258:    if (renderasvbo) {
259:      if (this->vbo == NULL) {
260:        this->vbo = new SoVBO(GL_ELEMENT_ARRAY_BUFFER);
263:            (this->vbo->allocBufferData(this->indexarray.getLength()*sizeof(GLushort)));
270:          this->vbo->setBufferData(this->indexarray.getArrayPtr(),
274:      this->vbo->bindBuffer(context  …[truncated]
Fable
thinking
I'm tracing through the render function to understand how it decides between using persistent VBO indices versus client-side arrays, which depends on the shouldCreateVBO logic in SoGLVBOElement. Now I need to look at how vertex coordinates are being handled in the shape rendering pipeline, particularly in SoShape and SoIndexedFaceSet.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -rn \"renderasvbo\\|shouldCreateVBO\\|->render(state\" src/shapenodes/SoShape.cpp src/rendering/*.cpp | head -30", "description": "Find renderasvbo decisio…
tool result
src/rendering/SoVBO.cpp:393:SoVBO::shouldCreateVBO(SoState * state, const uint32_t contextid, const int numdata)
src/rendering/SoVertexArrayIndexer.cpp:248:SoVertexArrayIndexer::render(SoState * state, const SbBool renderasvbo, const uint32_t contextid)
src/rendering/SoVertexArrayIndexer.cpp:258:    if (renderasvbo) {
src/rendering/SoVertexArrayIndexer.cpp:313:  if (this->next) this->next->render(state, renderasvbo, contextid);
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; sed -n '385,460p' src/rendering/SoVBO.cpp", "description": "shouldCreateVBO logic"}
tool result
 */
int
SoVBO::getVertexCountMaxLimit(void)
{
  return vbo_vertex_count_max_limit;
}

SbBool
SoVBO::shouldCreateVBO(SoState * state, const uint32_t contextid, const int numdata)
{
  if (!vbo_enabled || !vbo_render_as_vertex_arrays) return FALSE;
  int minv = SoVBO::getVertexCountMinLimit();
  int maxv = SoVBO::getVertexCountMaxLimit();
  return
    (numdata >= minv) &&
    (numdata <= maxv) &&
    SoVBO::isVBOFast(contextid) &&
    !(SoShapeStyleElement::get(state)->getFlags() & SoShapeStyleElem  …[truncated]
Fable
thinking
I'm tracing through how the VBO configuration works—when COIN_VBO is disabled, the vbo_enabled flag stays false, which prevents vertex buffer objects from being created, so the system falls back to client-side arrays that get uploaded to the GPU via bufferSubData on each draw call. The vertex array rendering path, though, depends on a separate vbo_render_as_vertex_arrays flag rather than vbo_enabled.

The core issue is that with COIN_VBO=0, Emscripten's client-array emulation re-uploads all the vertex, normal, and color data to temporary VBOs every single frame through bufferSubData calls—that's where the 465ms bottleneck is coming from. The solution is to enable COIN_VBO=1, which makes Coin  …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -n \"vbo_enabled\\|vbo_render_as_vertex_arrays\\|COIN_VBO\\|COIN_VERTEX_ARRAY\\|coin_getenv\" src/rendering/SoVBO.cpp | head; echo \"=== bindBuffer/setBuff…
tool result
63:static int vbo_render_as_vertex_arrays = -1;
64:static int vbo_enabled = -1;
132:  vbo_render_as_vertex_arrays = -1;
133:  vbo_enabled = -1;
144:  // use COIN_VBO_MAX_LIMIT to set the largest VBO we create
146:    const char * env = coin_getenv("COIN_VBO_MAX_LIMIT");
155:  // use COIN_VBO_MIN_LIMIT to set the smallest VBO we create
157:    const char * env = coin_getenv("COIN_VBO_MIN_LIMIT");
166:  // use COIN_VERTEX_ARRAYS to globally disable vertex array rendering
167:  if (vbo_render_as_ve  …[truncated]
Fable
thinking
Looking at how SoVBO::bindBuffer works — it caches the GL buffer per context in vbohash, so the expensive operations like glBufferData only happen once, and then subsequent frames just call glBindBuffer. That's the persistent-VBO optimization in action. Now I need to check the environment variable gating around line 166-190 to see how vbo_enabled is controlled.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; sed -n '160,200p' src/rendering/SoVBO.cpp", "description": "vbo_enabled env default"}
tool result
    }
    else {
      vbo_vertex_count_min_limit = DEFAULT_MIN_LIMIT;
    }
  }

  // use COIN_VERTEX_ARRAYS to globally disable vertex array rendering
  if (vbo_render_as_vertex_arrays < 0) {
    const char * env = coin_getenv("COIN_VERTEX_ARRAYS");
    if (env) {
      vbo_render_as_vertex_arrays = atoi(env);
    }
    else {
      vbo_render_as_vertex_arrays = 1;
    }
  }

  // use COIN_VBO to globally disable VBOs when doing vertex array rendering
  if (vbo_enabled < 0) {
    const char *   …[truncated]
Fable
thinking
COIN_VBO defaults to 1, but the environment variable is explicitly set to 0, which forces the use of client arrays and causes emscripten to re-upload buffers on each draw call. The solution is to remove or override that COIN_VBO=0 setting—I need to locate where it's being set in the FreeCAD wasm code.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port; grep -rn \"COIN_VBO\\|COIN_VERTEX_ARRAYS\" FreeCAD/src --include=*.cpp --include=*.h --include=*.js --include=*.html 2>/dev/null | grep -iv \"3rdParty/coin\" | head -20; echo \"=== searc…
tool result
(eval):1: no matches found: --include=*.cpp
=== search wider ===
(eval):1: no matches found: FreeCAD/*.html
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port; grep -rn \"COIN_VBO\" FreeCAD/src 2>/dev/null | grep -v 3rdParty/coin | head -20; echo \"=== also artifacts/html/js ===\"; grep -rln \"COIN_VBO\" . 2>/dev/null | grep -viE \"3rdParty/coi…
tool result
FreeCAD/src/Gui/Application.cpp:569:            (void)coin_setenv("COIN_VBO", "1", true);
=== also artifacts/html/js ===
FreeCAD/src/Gui/Application.cpp
deploy-va/index.html
deploy/index.html
deploy-final/index.html
deploy-p/index.html
deploy-parity/index.html
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I see that Application.cpp already sets COIN_VBO=1 at line 569, but the deployed HTML/JavaScript might be overriding it with COIN_VBO=0. Let me look at the context around that line to check if there's any conditional logic or competing settings.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port; sed -n '550,590p' FreeCAD/src/Gui/Application.cpp; echo \"=== deploy html COIN_VBO ===\"; grep -n \"COIN_VBO\\|COIN_VERTEX_ARRAYS\" deploy/index.html deploy-va/index.html 2>/dev/null | h…
tool result
            std::bind(&Gui::Application::slotActiveDocument, this, sp::_1));
        App::GetApplication().signalRelabelDocument.connect(
            std::bind(&Gui::Application::slotRelabelDocument, this, sp::_1));
        App::GetApplication().signalShowHidden.connect(
            std::bind(&Gui::Application::slotShowHidden, this, sp::_1));
        // NOLINTEND
        // install the last active language
        ParameterGrp::handle hPGrp = App::GetApplication().GetUserParameter().GetGroup("Ba  …[truncated]
Fable
thinking
I found where the deploy HTML is forcing client arrays by setting the Emscripten Module environment variable to disable VBOs. There's a comment in the code explaining the reasoning behind this choice, so let me check what it says to understand if there's an underlying bug preventing VBOs from working properly.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port; sed -n '85,105p' deploy/index.html", "description": "Read COIN_VBO rationale in deploy html"}
tool result
      const E = M.ENV;
      E.FREECAD_HOME = '/freecad/';
      E.PYTHONPATH = '/python/python3.14.zip:/python';
      E.HOME = '/tmp';
      E.QT_LOGGING_RULES = '*=false';
      E.COIN_AUTO_CACHING = '0';
      // Vertex-array rendering path (fast: whole-array glDrawElements instead of a
      // wasm->JS call per vertex). DEFAULT ON. Set window.__FC_VA='0' to force the
      // old immediate-mode path. COIN_VBO=0 keeps it on client arrays because
      // WebGL2 has no glMapBuffer for Coin's  …[truncated]
Fable
thinking
I'm questioning the reasoning behind the COIN_VBO setting. The comment claims WebGL2 lacks glMapBuffer support, but looking at the actual code, SoVBO uses glBufferData for uploads, not glMapBuffer. I need to dig deeper into setBufferData and allocBufferData to see if there's actually a glMapBuffer call somewhere that would justify this constraint.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -n \"glMapBuffer\\|MapBuffer\\|glBufferData\\|glBufferSubData\\|setBufferData\\|allocBufferData\" src/rendering/SoVBO.cpp", "description": "Check SoVBO for…
tool result
202:  \sa setBufferData()
205:SoVBO::allocBufferData(intptr_t size, SbUniqueId dataid)
243:SoVBO::setBufferData(const GLvoid * data, intptr_t size, SbUniqueId dataid)
275:  \sa setBufferData()
313:    cc_glglue_glBufferData(glue, this->target,
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I'm realizing the stated reason for disabling COIN_VBO is incorrect—SoVBO never actually uses glMapBuffer in its upload path, only glBufferData, so the WebGL2 incompatibility claim doesn't hold up.

Now I need to trace through the full vertex data pipeline to confirm VBOs are properly created and bound during rendering. I'm checking whether SoGLVBOElement correctly binds vertex and normal VBOs when the shape renders, and verifying that glDrawElements with the VBO offset actually works end-to-end.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -rn \"SoVBO\\|vbo\\|bindBuffer\\|VertexPointer\\|renderasvbo\\|shouldCreateVBO\" src/shapenodes/soshape.cpp 2>/dev/null | head -30; echo \"=== glue glBuffe…
tool result
=== glue glBufferData wiring ===
1349:    w->glBufferData = (COIN_PFNGLBUFFERDATAPROC) PROC(w, glBufferData);
1352:    w->glMapBuffer = (COIN_PNFGLMAPBUFFERPROC) PROC(w, glMapBuffer);
1365:    w->glBufferData = (COIN_PFNGLBUFFERDATAPROC) PROC(w, glBufferDataARB);
1368:    w->glMapBuffer = (COIN_PNFGLMAPBUFFERPROC) PROC(w, glMapBufferARB);
1430:        !w->glBufferData ||
1433:        !w->glMapBuffer ||
3569:cc_glglue_glBufferData(const cc_glglue * glue,
3575:  assert(glue->glBufferData);
3576:    …[truncated]
Fable
thinking
The VBO feature detection hinges on both glBufferData and glMapBuffer being available—if either is missing, VBO support gets disabled. I'm looking at the critical section around line 1430-1433 to understand how this check works, particularly since WebGL2 through emscripten doesn't expose the classic glMapBuffer function, though it does provide glMapBufferRange as an alternative.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; sed -n '1420,1445p' src/glue/gl.cpp", "description": "VBO feature detection requiring glMapBuffer"}
tool result
      w->glBindBuffer = NULL;
    }
  }

#endif /* GL_ARB_vertex_buffer_object */

  if (w->glBindBuffer) {
    if (!w->glDeleteBuffers ||
        !w->glGenBuffers ||
        !w->glIsBuffer ||
        !w->glBufferData ||
        !w->glBufferSubData ||
        !w->glGetBufferSubData ||
        !w->glMapBuffer ||
        !w->glUnmapBuffer ||
        !w->glGetBufferParameteriv ||
        !w->glGetBufferPointerv) {
      w->glBindBuffer = NULL; /* so that cc_glglue_has_vertex_buffer_object() will re  …[truncated]
Fable
thinking
The core issue is that VBO support depends on functions like glMapBuffer and glGetBufferPointerv that don't exist in WebGL2/GLES2 — emscripten only provides glMapBufferRange in ES3. When glMapBuffer is unavailable, the entire vertex buffer object chain fails, causing the capability check to return false and disabling VBO support.

The solution is to relax the feature-detection gate in gl.cpp. SoVBO only actually uses glGenBuffers, glBindBuffer, glBufferData, glBufferSubData, and glDeleteBuffers — it never calls glMapBuffer, glUnmapBuffer, glGetBufferSubData, or glGetBufferPointerv. So I should remove those unnecessary requirements from the capability check and let VBOs work with just the fun  …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin; grep -rn \"glMapBuffer\\|glUnmapBuffer\\|glGetBufferSubData\\|glGetBufferParameteriv\\|glGetBufferPointerv\" src/rendering/ src/elements/GL/ src/shapenodes/ 2>/…
tool result
src/shapenodes/SoIndexedFaceSet.cpp:557:    // cc_glglue marks VA available) + COIN_VBO=0 (WebGL2 has no glMapBuffer, so
=== confirm which VBO funcs Coin actually uses ===
217 -> cc_glglue_glBindBuffer(glue, GL_ARRAY_BUFFER, 0);
279 -> cc_glglue_glBindBuffer(glue, GL_ELEMENT_ARRAY_BUFFER, 0);
311 -> cc_glglue_glGenBuffers(glue, 1, &buffer);
312 -> cc_glglue_glBindBuffer(glue, this->target, buffer);
313 -> cc_glglue_glBufferData(glue, this->target,
321 -> cc_glglue_glBindBuffer(glue, this->target  …[truncated]
Fable
thinking
Looking at Coin's actual VBO rendering implementation, it only uses the basic buffer operations like glGenBuffers, glBindBuffer, glBufferData, and glDeleteBuffers—plus glBufferSubData for updates. The stricter gate at gl.cpp:1426-1444 that checks for additional buffer operations is overly restrictive since those functions never get called in the render hot path.

There's also a comment in SoIndexedFaceSet.cpp:557 that reflects the team's mistaken assumption about this. Let me check whether emscripten actually provides a glMapBuffer stub—if it does, then the gate should pass and something else must be disabling VBO instead.
Fable
⚙ Bash {"command": "cd /opt/toolchains/emsdk/upstream/emscripten/src/lib; grep -rn \"glMapBuffer\\b\\|glUnmapBuffer\\|glGetBufferPointerv\\|glGetBufferSubData\\|glMapBufferRange\" libwebgl.js libwebgl2.js libglemu.js 2>/dev/null | head; echo \"===…
tool result
libwebgl2.js:109:  glGetBufferSubData: (target, offset, size, data) => {
libwebgl2.js:115:      err('GL_INVALID_VALUE in glGetBufferSubData(target=' + target + ', offset=' + offset + ', size=' + size + ', data=0): Function called with null out data pointer!');
libwebgl.js:4173:  glMapBufferRange__deps: ['$emscriptenWebGLGetBufferBinding', '$emscriptenWebGLValidateMapBufferTarget', 'malloc'],
libwebgl.js:4174:  glMapBufferRange: (target, offset, length, access) => {
libwebgl.js:4176:      err("gl  …[truncated]
Fable
thinking
So Emscripten does provide MapBufferRange and related functions in WebGL2, but it emulates them through getBufferSubData and bufferSubData operations rather than true memory mapping. The key issue is that classic glMapBuffer isn't exported at all—Emscripten only supports the newer MapBufferRange variant, which means when Coin tries to use the traditional mapping approach...

Actually, let me verify this by checking what symbols are actually available. A grep through libwebgl.js confirms glMapBufferRange and glGetBufferPointerv are there, but plain glMapBuffer is completely absent. That's the smoking gun—the gate fails because the function pointer is NULL, which disables the entire VBO subsys  …
Fable
I have a complete, evidence-backed picture. Let me write up the findings.
Fable
⚙ StructuredOutput {"lane": "Redundant GL state queries + buffer churn (getParameter 390ms, getError 42ms, bufferData 465ms, ffSyncContext 86ms)", "root_cause": "The frame draws geometry as CLIENT vertex arrays (COIN_VBO forced to 0), so emscripten's client-a…
tool result
Structured output provided successfully