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: SoAsciiText 3D text (~355ms) + immediate-mode glue (ffVertex 268ms + ffNormal3 + emitImm + glVertex3f).
Investigate the Coin render path: SoAsciiText::GLRender + setUpGlyphs render glyph outlines via IMMEDIATE MODE
(glBegin/glVertex3f), which emscripten emulates in JS (ffVertex/emitImm) — extremely slow in wasm (per-vertex wasm->JS).
- Confirm SoAsciiText uses glBegin/glVertex per glyph per frame with no caching (Coin src/nodes/SoAsciiText.cpp +
the font glyph render). Is there a glyph display-list / VBO cache that's disabled on wasm (no display lists in GLES2)?
- WHERE does the text come from in a typical FreeCAD scene (dimension labels, FEM result legend, datum text, the
origin axis labels)? Is a lot of text drawn every frame? Can off-screen/backfacing text be culled?
- The immediate-mode path (ffVertex) is also hit by sogl_render_sphere and other Coin primitives that DON'T go through
the VA fast path. Which Coin shapes still use immediate mode on wasm, and can they be moved to vertex arrays / a cached
VBO? (COIN_VBO=0 today — would enabling a glyph/primitive VBO cache help despite the no-glMapBuffer limitation? bufferData
works in WebGL2 even though glMapBuffer doesn't.)
Propose: cache glyph geometry into a VBO/vertex-array (build once, redraw via drawArrays) OR reduce text; and move the
remaining immediate-mode primitives to VA. Estimate ms saved.
Fable
thinking
I need to dig into how SoAsciiText renders and how the immediate-mode API connects to it, so I'm looking for the Coin3D source code.
Fable
⚙ Bash {"command": "find / -name SoAsciiText.cpp 2>/dev/null; echo \"---GLUE---\"; find / -name \"gl.cpp\" -path \"*glue*\" 2>/dev/null; echo \"---SoText2/3---\"; find / -name \"SoText3.cpp\" 2>/dev/null; find / -name \"SoText2.cpp\" 2>/dev/null",…
Fable
⚙ Bash {"command": "find / -path \"*fonts*\" -name \"*.cpp\" 2>/dev/null | grep -i -E \"coin|font\" | head -40; echo \"---glyph---\"; find / -name \"glyph*.c*\" 2>/dev/null | head; find / -name \"font*.c*\" 2>/dev/null | grep -i coin | head", "des…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin/src/shapenodes/SoAsciiText.cpp"}
Fable
thinking
I'm tracing through the SoAsciiText rendering path and seeing how it uses immediate-mode OpenGL calls with glBegin and glVertex3f for each glyph every frame. I need to find where emscripten emulates the immediate-mode API and identify which other shapes still rely on it. I'm also noticing that FreeCAD typically uses SoText2 for dimension and axis labels, but the profile is specifically showing SoAsciiText as the bottleneck.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; grep -rln \"SoAsciiText\" src/ --include=*.cpp --include=*.h | grep -v 3rdParty | head -40", "description": "Find SoAsciiText usage in FreeCAD"}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; echo \"===SoText2===\"; grep -rln \"SoText2\\b\" src/ --include=*.cpp --include=*.h | grep -v 3rdParty | head; echo \"===origin/axis labels===\"; grep -rln \"SoAsciiText\\|SoText…
Fable
thinking
I'm running into a zsh globbing issue, so I'll switch to bash to handle this properly.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; grep -rln --include='*.cpp' --include='*.h' \"SoAsciiText\" src/ | grep -v 3rdParty", "description": "Find SoAsciiText usage"}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; echo \"===SoText2 users===\"; grep -rln --include='*.cpp' --include='*.h' 'SoText2' src/ | grep -v 3rdParty; echo \"===origin labels===\"; grep -rln --include='*.cpp' --include='…
Fable
thinking
I'm seeing that SoAsciiText is the culprit in the performance profile—it's rendering 3D polygon geometry through immediate mode OpenGL calls, which is much slower than SoText2's bitmap texture approach. SoText2 is used throughout the codebase for legends, dimensions, and sketcher labels, but SoAsciiText in ViewProviderAnnotation and ViewProviderPlane is what's eating up those 314+ milliseconds during rendering.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; grep -n \"SoAsciiText\\|SoText2\\|SoFont\\|->string\" src/Gui/ViewProviderPlane.cpp src/Gui/ViewProviderAnnotation.cpp | head -40", "description": "Inspect plane/annotation text …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; echo \"===glyph3d cache / VBO===\"; grep -n \"displaylist\\|display_list\\|glGenLists\\|VBO\\|glBindBuffer\\|cache\" src/3rdParty/coin/src/fonts/glyph3d.cpp | head; echo \"===cc_…
Fable
thinking
SoAsciiText is what renders the datum plane labels in ViewProviderPlane—those XY, XZ, and YZ text labels that appear when the origin planes are displayed. The glyph face indices for these text elements are cached in the glyph3d structure, which stores both the coordinates and face indices.
The problem is that even though the geometry is cached in CPU memory, the GLRender method re-emits it every frame using immediate mode calls (glBegin/glVertex3f). This gets routed through emscripten's fixed-function GLES2 emulation, which builds client arrays in JavaScript and makes per-vertex JS calls—that's where the 268ms ffVertex cost is coming from. The fix would be to build the triangles from the ca …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; echo \"===sogl_render_sphere===\"; grep -rn \"sogl_render_sphere\\|glBegin\\|glVertex3f\\|GL_QUAD_STRIP\\|GL_TRIANGLE\" src/3rdParty/coin/src/rendering/SoGL.cpp | head -40", "des…
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; echo \"===where does glBegin route? Coin system/gl.h + glue===\"; grep -rn \"glBegin\\|cc_glglue.*begin\\|GLImmediate\\|immediate\" src/3rdParty/coin/include/Inventor/system/gl.h…
Fable
thinking
I need to locate the emscripten glemu source code, specifically looking for the ff and emitImm functions.
Fable
⚙ Bash {"command": "grep -rln \"emitImm\\|ffVertex\\|GLImmediate\\|glVertex3f\" /opt/toolchains/emsdk/upstream/emscripten/src/ 2>/dev/null | head; echo \"---find glemu---\"; find /opt/toolchains/emsdk/upstream/emscripten/src -iname '*glemu*' -o -i…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin/src/rendering/SoGL.cpp", "offset": 487, "limit": 70}
Fable
thinking
I need to verify how emscripten handles the glemu emitImm and ffVertex mechanisms, so I'm checking the libglemu.js source to understand the implementation details.
Fable
⚙ Bash {"command": "grep -n \"ffVertex\\|ffNormal\\|emitImm\\|glVertex3f\\|glBegin\\|clientBuffers\\|prepareClientVertices\\|syncFuncs\\|ffSyncContext\" /opt/toolchains/emsdk/upstream/emscripten/src/lib/libglemu.js | head -40", "description": "Con…
Fable
thinking
So glVertex3f is implemented as a per-call JavaScript function in emscripten's GL emulation layer, which means every vertex call from WebAssembly crosses the boundary into JavaScript, gets added to a temporary buffer, and then when glEnd is called, it constructs a client-side array and performs the actual draw. That boundary crossing overhead for each individual vertex is what's causing the performance bottleneck.
Fable
⚙ Bash {"command": "grep -n \"glEnd\\b\\|glEnd:\\|bufferData\\|bufferSubData\\|flush\\|GL_TRIANGLES\\|drawArrays\" /opt/toolchains/emsdk/upstream/emscripten/src/lib/libglemu.js | sed -n '1,30p'", "description": "Confirm glEnd bufferData upload pat…
Fable
thinking
I'm tracing through the rendering pipeline: SoAsciiText's GLRender function issues individual vertex calls for each glyph triangle, which cross the wasm-to-JavaScript boundary through emscripten's GL emulation layer, accumulating data in temporary buffers before flushing it all at once with a single buffer upload and draw call.
The same immediate-mode pattern appears in other primitive renderers like sogl_render_sphere and the cone/cylinder/cube fallbacks that bypass the optimized SoGLVertexArray path. The glyph geometry itself is already cached on the CPU side in the glyph cache, computed once and reused—only the GL submission repeats every frame.
The solution is to assemble the triangle …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; echo \"===display list glue===\"; grep -rn \"glNewList\\|glCallList\\|glGenLists\\|can_do_displaylists\\|glglue_has_display\" src/3rdParty/coin/src/glue/gl.cpp | head; echo \"===…
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; grep -n \"glNewList\\|glGenLists\\|glCallList\\|glEndList\\|can_do_display_lists\\|GLES\\|__EMSCRIPTEN__\\|display list\" src/3rdParty/coin/src/rendering/SoGLDisplayList.cpp | he…
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; f=$(find src/3rdParty/coin -name SoGLDisplayList.cpp); echo \"$f\"; grep -n \"glNewList\\|glGenLists\\|glCallList\\|glEndList\\|GLES\\|__EMSCRIPTEN__\\|glbegin\\|feature\" \"$f\"…
Fable
thinking
Found the root cause — Coin's render caching relies on display lists (glGenLists, glNewList, glCallList) for optimization, but these don't exist in WebGL2/GLES2 through emscripten, where glGenLists just returns 0 and becomes a no-op. This means Coin's normal desktop rendering optimization completely breaks down in the web environment.
Fable
⚙ Bash {"command": "grep -n \"glGenLists\\|glNewList\\|glCallList\" /opt/toolchains/emsdk/upstream/emscripten/src/lib/libglemu.js | head", "description": "Check emscripten display-list stubs"}
Fable
thinking
The display list functions aren't actually implemented in emscripten's WebGL layer, so they're just stubs that don't work—this means the SoSeparator's render cache can't capture the text geometry, forcing SoAsciiText to regenerate vertices every frame. Now I'm ready to structure the solution.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; grep -n \"getLabelText\\|Plane\\|return\" src/Gui/ViewProviderPlane.cpp | grep -i \"plane\\|label\" | head; sed -n '95,140p' src/Gui/ViewProviderPlane.cpp 2>/dev/null | grep -n \…
Fable
thinking
The plane labels use bold sans-serif at size 10, with each character tessellated into filled triangles—roughly 20-60 triangles per glyph depending on complexity. An 8-10 character label ends up being around 1000-1800 vertices total, and with three visible planes during the origin display, that's hitting 3000-5000 vertex calls per frame, each crossing from WebAssembly to JavaScript. This matches up with the 268ms I'm seeing in the profiler for vertex operations.
Now I can put together a concrete fix. I'll add a cached flat vertex buffer to SoAsciiText that assembles all the triangle positions upfront whenever the glyph cache is created or invalidated. Instead of looping through thousands of …
Fable
I have the complete mechanism. Let me record the findings.
Fable
⚙ StructuredOutput {"lane": "SoAsciiText 3D text + immediate-mode glue (ffVertex/ffNormal3/emitImm)", "root_cause": "SoAsciiText::GLRender (src/3rdParty/coin/src/shapenodes/SoAsciiText.cpp:372-460) emits every glyph triangle every frame through fixed-function…
Fable
⚙ StructuredOutput {"lane": "SoAsciiText 3D text + immediate-mode glue (ffVertex/ffNormal3/emitImm)", "root_cause": "SoAsciiText::GLRender (src/3rdParty/coin/src/shapenodes/SoAsciiText.cpp:372-460) emits every glyph triangle every frame through fixed-function…