subagent transcript

session 17f09f89 · workflow wf_a3d8d75a
delegated task
CONTEXT: FreeCAD 26.3 compiled to WebAssembly (emscripten, single-thread, ASYNCIFY, static Qt 6.11.1 + OCCT + Coin3D + CPython 3.14, NO dlopen/subproc
▸ Magik
CONTEXT: FreeCAD 26.3 compiled to WebAssembly (emscripten, single-thread, ASYNCIFY, static Qt 6.11.1 + OCCT + Coin3D + CPython 3.14, NO dlopen/subprocess/threads). After a big push it now has 17 workbenches, numpy, Draft/TechDraw/Assembly all working. The 3D VIEWPORT RENDERS WELL (Coin3D -> WebGL via an offscreen FBO + QPainter blit). Toolbars/menus/tree are functional. THREE PROBLEM AREAS TO RESEARCH (be concrete, grep the real source + deploy, cite files): A) UNSTYLED UI (from a screenshot): the right "Tasks" panel (Gui::TaskView, shows "Start Part"/"New Body") has NO background — floating text on the 3D view. Context menus (right-click tree) are plain white, no hover highlight. NO hover states anywhere (buttons/menu items don't react on mouse-over). Font looks slightly oversized. This is the classic signature of FreeCAD's Qt STYLESHEET (.qss) + assets not being applied. FreeCAD ships stylesheets in src/Gui/Stylesheets/*.qss ("FreeCAD Dark.qss", "FreeCAD Light.qss", "Behave-*.qss") and the overlay/taskview qss; the active one is set by the preference MainWindow/StyleSheet and applied via qApp->setStyleSheet / Gui::Application. Icons/qss may be in a Qc qrc compiled into the wasm OR need FS packaging. B) The RENDER PERF hot path (from a profiler trace of a single mouse-wheel zoom = 96ms): dominated by Coin's IMMEDIATE-MODE GL emulation — emscripten's ffVertex/ffEnd/ffNormal3/ffColor4 (per-vertex glBegin/glVertex emulation) called from Coin scenegraph traversal (recursive wasm-function[15570]/[48370]), plus _glTexSubImage2D (~4ms, the FBO->texture blit) and _glReadPixels. deploy/index.html sets `COIN_VERTEX_ARRAYS=0` and `COIN_AUTO_CACHING=0` in Module.preRun ENV — these DISABLE Coin vertex arrays (forcing the slow immediate-mode path) and render caching. There are NO wasm debug symbols so wasm-function[N] can't be resolved. C) NEXT PARITY STAGES: (1) browser-reload PERSISTENCE is broken — IDBFS mount+hydrate work, but FreeCAD's user.cfg ParameterManager serializer is created in Application::bootstrapEarly()->LoadParameters() with a stale root path ("/user.cfg") before the real UserConfigPath is known; App.saveParameter() then writes to /user.cfg (or no-ops). An attempted fix re-pointed the serializer in LoadParameters()'s idempotent-guard using getUserConfigPath() but it did NOT work (user.cfg still not at /tmp/.config/FreeCAD/v26-3/). (2) CAM is blocked because pybind11 is absent from /opt/toolchains (CAM's area/tsp_solver need it). (3) BIM/Arch could partially load now that Draft+numpy exist (IFC needs ifcopenshell, but non-IFC Arch modeling may work). PATHS: FreeCAD source /home/magik6k/lcad-wasm/freecad-port/FreeCAD (git branch wasm-parity-push). wasm build tree /opt/toolchains/src/freecad-gui-build. deploy /home/magik6k/lcad-wasm/freecad-port/deploy (index.html + *.data + *.data.js — grep the .data.js manifests to see what's on the FS). Toolchain env: source /home/magik6k/lcad-wasm/env.sh. To check what's packaged: the .data.js files contain a JSON "files" manifest with "filename" paths. The base freecad.data mounts at /freecad (share at /freecad/share, modules at /freecad/Mod). getResourceDir()=/freecad/share/. Qt wasm plugin + qt.conf considerations apply. DIMENSION C — rendering performance. The profile shows immediate-mode GL emulation (ffVertex/ffEnd) dominating a 96ms wheel-zoom redraw. Investigate: 1. WHY are COIN_VERTEX_ARRAYS=0 and COIN_AUTO_CACHING=0 set in deploy/index.html? Search git history / code comments / the memory dir (/home/magik6k/.claude/projects/-home-magik6k-lcad-wasm/memory) / research notes for the rationale. Was vertex-arrays disabled due to a bug, or conservatively? 2. Would COIN_VERTEX_ARRAYS=1 work and help? Coin's SoVertexArrayIndexer / GLSL — under emscripten WebGL2, vertex arrays (glDrawElements/glDrawArrays) are MUCH faster than emulated immediate mode. Assess the risk (does Coin's wasm GL backend support VBO? does the FBO path interfere?). This is likely the single biggest perf win. Also COIN_AUTO_CACHING (render caching via GL display lists — emscripten emulates these; may or may not help). 3. What triggers a FULL redraw on every wheel event (SoGLRenderAction re-traverse + re-emit all geometry)? Is there render caching (SoSeparator renderCaching) or is caching off? The offscreen-FBO + QPainter blit (glTexSubImage2D ~4ms/frame, glReadPixels) — is the readback/blit avoidable (render directly to the default framebuffer / swap)? 4. Coin GL backend: is FreeCAD's Coin using GLES2/WebGL2 shaders or fixed-function emulation? The ff* prefix = emscripten's LEGACY_GL_EMULATION-style immediate-mode shim. Is there a real GLES2 Coin path? Produce fixes RANKED by impact/effort: the quick env-flag experiments (COIN_VERTEX_ARRAYS=1 etc — testable by editing index.html + re-profiling, NO rebuild) first, then deeper work. Give the exact index.html changes to test and how to measure (a playwright probe timing a redraw). Be honest about which need a Coin rebuild.
Fable
thinking
I'm shifting focus to rendering performance and need to investigate the COIN environment flags. I'll start by examining the deploy configuration, git history, memory usage, and the COIN source code to understand what's affecting performance.
Fable
⚙ Bash {"command": "grep -n \"COIN_\\|preRun\\|ENV\\|VERTEX_ARRAYS\\|AUTO_CACHING\\|glReadPixels\\|TexSubImage\\|FBO\\|offscreen\" /home/magik6k/lcad-wasm/freecad-port/deploy/index.html | head -80", "description": "Find COIN env flags in index.htm…
tool result
51:    preRun: [(M) => {
81:      const E = M.ENV;
86:      E.COIN_AUTO_CACHING = '0';
87:      E.COIN_VERTEX_ARRAYS = '0';
91:      // edited without touching this HTML. Fetch it synchronously here in preRun
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "ls /home/magik6k/.claude/projects/-home-magik6k-lcad-wasm/memory/", "description": "List memory dir"}
tool result
freecad-wasm-new-vm-setup.md
freecad-wasm-parity-roadmap.md
freecad-wasm-viewport.md
MEMORY.md
pyside-wasm-port.md
wasm-toolchain-eh-encodings.md
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "ls -la /home/magik6k/lcad-wasm/freecad-port/deploy/ | head; echo \"---\"; ls /home/magik6k/lcad-wasm/", "description": "List deploy and project root"}
tool result
total 419988
drwxr-xr-x 1 magik6k magik6k      1070 Jul  5 22:26 .
drwxr-xr-x 1 magik6k magik6k       742 Jul  5 23:19 ..
-rw-r--r-- 1 magik6k magik6k       941 Jul  5 22:27 boot.py
-rw-r--r-- 1 magik6k magik6k  15229294 Jul  4 20:36 freecad.data
-rw------- 1 magik6k magik6k   8977277 Jul  4 20:36 freecad.data.gz
-rw-r--r-- 1 magik6k magik6k    135559 Jul  4 20:36 freecad.data.js
-rw------- 1 magik6k magik6k     17642 Jul  4 20:36 freecad.data.js.gz
-rw-r--r-- 1 magik  …[truncated]
Fable
thinking
I'm checking the preRun section in index.html and reviewing the memory files.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/deploy/index.html", "offset": 45, "limit": 110}
tool result
45	  const bootEl   = document.getElementById('boot');
46	  const barEl    = document.querySelector('#bootbar > div');
47	  const setStatus = (s) => { statusEl.textContent = s; };
48	  var Module = {
49	    qtContainerElements: [screenEl],
50	    arguments: [],
51	    preRun: [(M) => {
52	      // ---- P0 browser persistence: hydrate IDBFS-backed config/data dirs ----
53	      // FreeCAD derives its user dirs from HOME (=/tmp below) via Qt XDG paths:
54	      //   UserConfigPath = /tmp/.config/F  …[truncated]
Fable
⚙ Bash {"command": "grep -rn \"COIN_VERTEX_ARRAYS\\|COIN_AUTO_CACHING\\|VERTEX_ARRAYS\\|immediate\\|vertex array\\|renderCaching\\|glReadPixels\\|glTexSubImage\\|LEGACY_GL\\|ffVertex\\|blit\" /home/magik6k/.claude/projects/-home-magik6k-lcad-wasm/…
tool result
/home/magik6k/.claude/projects/-home-magik6k-lcad-wasm/memory/wasm-toolchain-eh-encodings.md:14:**How to apply:** compile C++ deps with `-fwasm-exceptions -sWASM_LEGACY_EXCEPTIONS=0`; setjmp-using C code (freetype, Qt bundled libpng/libjpeg) emits legacy sjlj regardless unless given `-mllvm -wasm-use-legacy-eh=false`; a post-link `wasm-opt --emit-exnref --all-features` cleans up whatever remains (wire it as POST_BUILD — emcc doesn't reliably run it itself). Other qt-jspi 6.11 wasm-GUI lessons li  …[truncated]
Fable
thinking
So the real insight here is that WebAssembly's GL implementation doesn't actually use standard WebGL2 — instead it relies on a custom emulator that converts immediate-mode OpenGL calls like glBegin/glEnd/glVertex into shader-based operations. And on WebAssembly, there's a deliberate override forcing `SoIndexedFaceSet` to use the immediate path rather than vertex arrays, which means setting `COIN_VERTEX_ARRAYS=0` wasn't a conservative choice but an intentional design decision.

Now I need to dig into the actual source code to find where `WasmGLFixedFunc.cpp` implements this emulation and locate that hardcoded `dova=FALSE` override.
Fable
⚙ Read {"file_path": "/home/magik6k/.claude/projects/-home-magik6k-lcad-wasm/memory/freecad-wasm-viewport.md"}
tool result
1	---
2	name: freecad-wasm-viewport
3	description: How the FreeCAD-wasm 3D viewport was made to render (offscreen FBO + QPainter blit) and the asyncify-safe way to create a 3D view
4	metadata: 
5	  node_type: memory
6	  type: project
7	  originSessionId: 18e25685-98b5-4959-935f-943b1af20789
8	---
9	
10	FreeCAD WebAssembly port (/home/magik6k/lcad-wasm/freecad-port/FreeCAD, build dir /opt/toolchains/src/freecad-gui-build). The 3D viewport now RENDERS (gradient background, navigation cube, RGB axi  …[truncated]
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD 2>/dev/null; find . -iname \"WasmGLFixedFunc*\" 2>/dev/null; echo \"---git log---\"; git log --oneline -20 2>/dev/null", "description": "Find WasmGLFixedFunc and git log"}
tool result
./src/Gui/WasmGLFixedFunc.cpp
---git log---
e7fb887653 wasm parity push T3 VERIFIED: TechDraw + Assembly (17 workbenches total)
bbf63b8c24 wasm parity push T1 VERIFIED: Mesh/Points/MeshPart/Inspection/Robot + numpy
7c4dbdf527 wasm parity push T0/T1: persistence fix + module registration + SMESH gate
6f009ae01c wasm parity push: swarm source patches (single-thread serialization + module gates)
8f36d383de wasm: inittab RegName=CInitSuffix form + FREECAD_WASM_EXTRA_PY_MODULES
f51203445d wasm: Sketc  …[truncated]
Fable
thinking
So the real mechanism here is that the WASM GL path uses a custom emulator in WasmGLFixedFunc.cpp that mimics the old immediate-mode API (glBegin/glVertex/glEnd) through shader-based rendering, not actual WebGL2 vertex arrays. The COIN_VERTEX_ARRAYS=0 setting wasn't a conservative fallback—it was intentionally disabled, along with a hard-coded dova=FALSE flag, to force this emulation path.

Now I need to dig into the emulator itself to see what it actually supports—whether it can handle glDrawElements, VBOs, or vertex arrays at all—and understand why that hard-coded dova flag exists in SoIndexedFaceSet. I should also check how the immediate-mode emitter works to see if it batches operations   …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; wc -l src/Gui/WasmGLFixedFunc.cpp; echo \"=== grep array/VBO/DrawElements/DrawArrays in emulator ===\"; grep -n \"DrawElements\\|DrawArrays\\|VertexPointer\\|NormalPointer\\|Colo…
tool result
801 src/Gui/WasmGLFixedFunc.cpp
=== grep array/VBO/DrawElements/DrawArrays in emulator ===
13: * then translate glDrawArrays/glDrawElements/glBegin..glEnd into a shader draw
71:    posVBO: null, nrmVBO: null, colVBO: null, idxVBO: null,
72:    // GL objects (program, uniform locations, VBOs) belong to a specific WebGL
83:    emitImm(rec) {
92:      g.bindBuffer(g.ARRAY_BUFFER,this.posVBO); g.bufferData(g.ARRAY_BUFFER,new Float32Array(rec.verts),g.STREAM_DRAW);
93:      g.enableVertexAttribArray(  …[truncated]
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; grep -rn \"dova\" src/3rdParty/salomesmesh 2>/dev/null | head; echo \"=== find SoIndexedFaceSet ===\"; find . -iname \"SoIndexedFaceSet.cpp\" 2>/dev/null; echo \"=== find coin sr…
tool result
=== find SoIndexedFaceSet ===
./src/3rdParty/coin/src/shapenodes/SoIndexedFaceSet.cpp
=== find coin src ===
3Dconnexion
Clipper2
CMakeLists.txt
coin
FastSignals
GSL
json
lazy_loader
libE57Format
libkdtree
lru-cache
OndselSolver
OpenGL
pivy
PyCXX
salomesmesh
zipios++
/home/magik6k/lcad-wasm/freecad-port/  …[truncated]
Fable
thinking
Vendored Coin is already in the FreeCAD tree, and the emulator supports a real vertex-array path with drawElements calls plus Qt UI passthrough, so COIN_VERTEX_ARRAYS could potentially use it. I need to examine the hardcoded dova=FALSE and trace through the emulator's array handling to see if enabling COIN_VERTEX_ARRAYS=1 would actually work, starting with looking at the ff_set implementation.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; grep -n \"dova\\|__EMSCRIPTEN__\\|EMSCRIPTEN\\|COIN_VERTEX\\|vertexarray\\|VertexArray\\|renderInternal\\|GLRender\" src/3rdParty/coin/src/shapenodes/SoIndexedFaceSet.cpp | head …
tool result
192:#include <Inventor/actions/SoGLRenderAction.h>
229:#include "rendering/SoVertexArrayIndexer.h"
253:  SoVertexArrayIndexer * vaindexer;
425:SoIndexedFaceSet::GLRender(SoGLRenderAction * action)
435:    this->vertexProperty.getValue()->GLRender(action);
438:  if (!this->shouldGLRender(action)) {
537:  SbBool dova =
538:    SoVBO::shouldRenderAsVertexArrays(state, contextid, numindices) &&
546:#ifdef __EMSCRIPTEN__
551:  dova = FALSE;
558:  if (dova && (mbind != OVERALL)) {
559:    dova = FALSE  …[truncated]
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/WasmGLFixedFunc.cpp", "offset": 83, "limit": 130}
tool result
83	    emitImm(rec) {
84	      const g=this.gl(); if(!g||!rec||rec.verts.length===0) return; if(!this.program()) return;
85	      g.useProgram(this.prog);
86	      const mvp=this.mul(this.pr[this.pr.length-1], this.mv[this.mv.length-1]);
87	      g.uniformMatrix4fv(this.loc.mvp,false,new Float32Array(mvp));
88	      g.uniformMatrix3fv(this.loc.nm,false,new Float32Array(this.normalMat3(this.mv[this.mv.length-1])));
89	      g.uniform1i(this.loc.lit, rec.lighting?1:0);
90	      g.uniform3fv(this.l  …[truncated]
Fable
thinking
The emulator does support vertex arrays through its real array path in the drawing functions, so setting COIN_VERTEX_ARRAYS=1 should theoretically work. However, there's a hardcoded `dova=FALSE` check specifically for Emscripten at line 551 that forces immediate mode rendering regardless of the environment variable, which means just enabling it in index.html won't actually help.

I need to look at the SoIndexedFaceSet.cpp code around that region to see exactly how this override works and whether the environment variable has any effect at all in this context.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin/src/shapenodes/SoIndexedFaceSet.cpp", "offset": 530, "limit": 120}
tool result
530	  fprintf(stderr,"numindices: %d, convex: %d, ncache: %d, nbind: %d, mbind: %d, tbind: %d, va: %d\n",
531	          numindices, convexcacheused, normalCacheUsed, nbind, mbind, tbind,
532	          cc_glglue_has_vertex_array(sogl_glue_instance(state)));
533	#endif
534	
535	  const uint32_t contextid = action->getCacheContext();
536	  SoGLLazyElement * lelem = NULL;
537	  SbBool dova =
538	    SoVBO::shouldRenderAsVertexArrays(state, contextid, numindices) &&
539	    !convexcacheused && !norma  …[truncated]
Fable
thinking
I'm noticing a discrepancy between what the comment claims about the emulator's capabilities and what the actual code shows—there's clearly a full vertex-array implementation in place with support for glDrawElements, glDrawArrays, and the pointer functions, so the comment seems outdated or inaccurate.

The dova=FALSE workaround was originally added to work around a "renders once then disappears" issue, but that turned out to be caused by a depth-clear bug that's since been fixed, which means the conservative workaround might no longer be necessary. However, there's a complication: Coin's vertex-array path uses SoVertexArrayIndexer with VBOs, and I need to verify whether the emulator's glDraw  …
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/WasmGLFixedFunc.cpp", "offset": 569, "limit": 100}
tool result
569	EM_JS(void, ffBegin, (GLenum mode), { const F=globalThis.__ff; F.imm={mode, verts:[], nrms:[], cols:[]}; })
570	EM_JS(void, ffVertex, (GLfloat x,GLfloat y,GLfloat z), {
571	  const F=globalThis.__ff; if(!F.imm)return;
572	  F.imm.verts.push(x,y,z); F.imm.nrms.push(F.normal[0],F.normal[1],F.normal[2]);
573	  F.imm.cols.push(F.color[0],F.color[1],F.color[2],F.color[3]);
574	})
575	EM_JS(void, ffEnd, (void), {
576	  const F=globalThis.__ff; const g=F.gl(); const im=F.imm; F.imm=null;
577	  if(!  …[truncated]
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; grep -n \"setRenderCache\\|RenderCache\\|COIN_AUTO_CACHING\\|renderCaching\\|autoCaching\\|AutoCaching\" src/Gui/Application.cpp src/Gui/ViewParams.* src/Gui/View3DInventorViewer…
tool result
src/Gui/ViewParams.h:47:    long getRenderCache() const;
src/Gui/ViewParams.h:48:    void setRenderCache(long);
src/Gui/ViewParams.cpp:59:        Base::is_getter<decltype(&ViewParams::getRenderCache), Int::value_type>,
src/Gui/ViewParams.cpp:63:        Base::is_setter<decltype(&ViewParams::setRenderCache), Int::value_type>,
src/Gui/ViewParams.cpp:304:    addParameter("RenderCache", Int {0});
src/Gui/ViewParams.cpp:378:long ViewParams::getRenderCache() const
src/Gui/ViewParams.cpp:380:    return   …[truncated]
Fable
thinking
Looking at the render cache implementation across those files to see whether display-list caching would actually help—specifically checking if the emulator's display-list replay avoids the C++ scenegraph traversal and per-vertex JavaScript calls, or if it just re-emits the same immediate-mode operations anyway.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Application.cpp", "offset": 2730, "limit": 20}
tool result
2730	    // gets called once we start the event loop
2731	    QTimer::singleShot(0, &mw, SLOT(delayedStartup()));
2732	
2733	#ifdef __EMSCRIPTEN__
2734	    // Coin's GL display-list render caches are replayed via glCallList, which the
2735	    // WebGL2 fixed-function emulator (Gui/WasmGLFixedFunc.cpp) cannot fully honor —
2736	    // cached geometry renders on the first frame and then vanishes. FreeCAD drives
2737	    // scene caching through the "RenderCache" preference (read by
2738	    // Vi  …[truncated]
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/View3DInventorViewer.cpp", "offset": 1986, "limit": 45}
tool result
1986	void View3DInventorViewer::setRenderCache(int mode)
1987	{
1988	    static int canAutoCache = -1;
1989	
1990	    if (mode < 0) {
1991	        // Work around coin bug of unmatched call of
1992	        // SoGLLazyElement::begin/endCaching() when on top rendering
1993	        // transparent object with SORTED_OBJECT_SORTED_TRIANGLE_BLEND
1994	        // transparency type.
1995	        //
1996	        // For more details see:
1997	        // https://forum.freecad.org/viewtopic.php?f=18&t=43305&  …[truncated]
Fable
thinking
So the real culprit was that depth-clear stub all along, not the vertex arrays or caching mechanisms. The workarounds that disabled those features were addressing symptoms, not the actual problem. The code comments are outdated and still reference the old theory before the root cause was identified.

Re-enabling vertex arrays would unlock significant performance gains since the emulator already has the full infrastructure to support them with the proper pointer functions. The three settings that are currently killing performance—the environment variables and the hard-coded render cache setting—were all added as band-aids during the debugging process and never cleaned up once the real fix lan  …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; echo \"=== glNewList/CallList/GenLists in emulator ===\"; grep -n \"glNewList\\|glCallList\\|glGenLists\\|glEndList\\|glDeleteLists\\|curList\\|F.lists\\|GENLISTS\" src/Gui/WasmG…
tool result
=== glNewList/CallList/GenLists in emulator ===
79:    lists: {}, curList: null, curListMode: 0, _listCtr: 0, // GL display lists
579:  // (now, or on each glCallList replay) so cached geometry tracks the camera.
582:  if(F.curList!==null){
584:    // for GL_COMPILE (0x1300) do NOT execute now — it will render via glCallList.
585:    if(F.lists[F.curList]) F.lists[F.curList].push(rec);
586:    if(F.curListMode===0x1300) return;
718: * replays it via glCallList on later frames. Real display lists  …[truncated]
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/WasmGLFixedFunc.cpp", "offset": 700, "limit": 101}
tool result
700	void glPopClientAttrib(void){ }
701	void glLineStipple(GLint, unsigned short){ }
702	void glPolygonStipple(const GLubyte*){ }
703	void glRasterPos2f(GLfloat,GLfloat){ }
704	void glRasterPos3f(GLfloat,GLfloat,GLfloat){ }
705	void glRasterPos3fv(const GLfloat*){ }
706	void glBitmap(GLint,GLint,GLfloat,GLfloat,GLfloat,GLfloat,const GLubyte*){ }
707	void glDrawPixels(GLint,GLint,GLenum,GLenum,const void*){ }
708	void glClearIndex(GLfloat){ }
709	void glDrawBuffer(GLenum){ }
710	void glPixelMapfv  …[truncated]
Fable
thinking
The display-list emulation already handles this efficiently—glCallList replays stored commands through emitImm, which skips both scenegraph traversal and the per-vertex JavaScript overhead. Enabling render caching with RenderCache=Auto/On or COIN_AUTO_CACHING=1 is a real optimization that the emulator supports, even though the Application.cpp comments suggest it can't fully honor the behavior.

There's a caveat though: emitImm re-uploads vertices to the STREAM_DRAW VBO on every replay, so geometry still transfers to the GPU each frame. But the big win is avoiding the recursive Coin traversal and the thousands of ffVertex boundary crossings between WebAssembly and JavaScript—that's where most  …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; echo \"=== dova/EMSCRIPTEN overrides across shape nodes ===\"; grep -rln \"dova = FALSE\\|dova=FALSE\" src/3rdParty/coin/src/ src/Mod/*/Gui/ 2>/dev/null; echo \"--- SoBrepFaceSet…
tool result
=== dova/EMSCRIPTEN overrides across shape nodes ===
src/3rdParty/coin/src/shapenodes/SoFaceSet.cpp
src/3rdParty/coin/src/shapenodes/SoIndexedLineSet.cpp
src/3rdParty/coin/src/shapenodes/SoPointSet.cpp
src/3rdParty/coin/src/shapenodes/SoIndexedPointSet.cpp
src/3rdParty/coin/src/shapenodes/SoIndexedFaceSet.cpp
src/3rdParty/coin/src/vrml97/IndexedLineSet.cpp
src/3rdParty/coin/src/vrml97/IndexedFaceSet.cpp
src/3rdParty/coin/src/vrml97/PointSet.cpp
--- SoBrepFaceSet (FreeCAD solids) ---
src/Mod/Part  …[truncated]
Fable
thinking
SoBrepFaceSet doesn't have the dova=FALSE override either, so I need to look at how it actually renders — checking whether it uses immediate mode or vertex arrays by examining its GLRender implementation.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; grep -n \"VBO\\|VertexArray\\|renderShape\\|glBegin\\|glVertex\\|glDrawElements\\|glDrawArrays\\|GLRender\\|COIN_VERTEX\\|EnableClientState\\|glNormal\\|immediate\\|EMSCRIPTEN\" …
tool result
31:#include <Inventor/actions/SoGLRenderAction.h>
117:    SoGLRenderAction* action,
138:        SoShapeStyleElement::setTransparencyType(state, SoGLRenderAction::BLEND);
139:        SoLazyElement::setTransparencyType(state, SoGLRenderAction::BLEND);
151:    faceSet->GLRender(action);
390:void SoBrepFaceSet::renderHighlight(SoGLRenderAction* action, SelContextPtr ctx)
420:void SoBrepFaceSet::renderSelection(SoGLRenderAction* action, SelContextPtr ctx, bool /*push*/)
453:bool SoBrepFaceSet::overri  …[truncated]
Fable
thinking
So the FreeCAD solids are indeed going through the SoIndexedFaceSet rendering path where that hardcoded dova=FALSE override forces immediate-mode rendering for the main geometry, and each shape node applies the same constraint independently. Now I'm putting together what I've found so far.

The highest-impact fix would be removing those dova=FALSE hardcodes on the Emscripten build across the eight Coin shape nodes, especially in SoIndexedFaceSet — this would let Coin batch geometry into single glDrawElements calls per shape instead of thousands of individual glBegin/vertex/end sequences. The VBO path has some sketchy handling in the index buffer code, but the client-memory array path should   …
Fable
I have enough evidence. Let me compile the findings.
Fable
⚙ StructuredOutput {"area": "Dimension C — 3D render performance (immediate-mode GL emulation dominating a 96ms wheel-zoom redraw)", "rootCause": "The immediate-mode GL path is NOT conservative or a real VBO bug — it is a set of stale workarounds left over fr…
tool result
Structured output provided successfully