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…
Fable
⚙ Bash {"command": "ls /home/magik6k/.claude/projects/-home-magik6k-lcad-wasm/memory/", "description": "List memory dir"}
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"}
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}
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/…
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"}
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"}
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…
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…
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 …
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/WasmGLFixedFunc.cpp", "offset": 83, "limit": 130}
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}
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}
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…
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}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/View3DInventorViewer.cpp", "offset": 1986, "limit": 45}
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…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/WasmGLFixedFunc.cpp", "offset": 700, "limit": 101}
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…
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\" …
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…