subagent transcript

session 18e25685 · workflow wf_23df5b1c
delegated task
Deeply review the fixed-function-GL-over-WebGL2 emulator at /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/WasmGLFixedFunc.cpp (535 lines). It e
▸ Magik
Deeply review the fixed-function-GL-over-WebGL2 emulator at /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/WasmGLFixedFunc.cpp (535 lines). It emulates OpenGL 1.x fixed-function (matrix stacks, materials/lighting, glBegin/glEnd immediate mode, client vertex arrays, indexed draws) over WebGL2 for Coin3D. Assess: correctness of the matrix math and shader, completeness vs what Coin's SoGLRenderAction actually needs (faces via SoBrepFaceSet, edges, SoText2, markers, transparency), the glDrawArrays/glDrawElements FF-mode gating (must pass Qt's own draws through untouched), the fcWasmResolveGL resolver table and fcWasmSyncGLContext context binding. Identify what is missing or wrong for a box to actually render with orbit/pan/zoom. Report concrete findings + a gap list for the viewport.
Fable
thinking
Looking at the file now.
Fable
I'll review the emulator file thoroughly.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/WasmGLFixedFunc.cpp"}
tool result
1	/* SPDX-License-Identifier: LGPL-2.1-or-later
2	 *
3	 * WebAssembly build only. A compact fixed-function OpenGL 1.x emulation over
4	 * WebGL2/GLES3, so Coin3D (which renders FreeCAD geometry with legacy GL) can
5	 * draw into the WebGL2 context Qt hands us. Emscripten's LEGACY_GL_EMULATION
6	 * is WebGL1-only and conflicts with Qt's forced WebGL2, so we implement the
7	 * subset Coin actually exercises ourselves.
8	 *
9	 * Design: the legacy entry points are C functions here; the state and dr  …[truncated]
Fable
thinking
I'm working through several key concerns about this implementation: the matrix math conventions, shader correctness, whether it covers everything Coin's SoGLRenderAction needs, the gating of glDrawArrays and glDrawElements for fixed-function mode, the fcWasmResolveGL resolver table, and how the context binding works in fcWasmSyncGLContext. Starting with the matrix math—I need to verify that the column-major convention is being applied correctly in the multiplication operations.

Checking the matrix operations more carefully. The mul function correctly implements column-major matrix multiplication where the result computes a*b properly. The MVP is correctly computed as projection times model-  …
Fable
Let me check how the resolver and context sync are wired, and whether the quad/missing-symbol concerns are real.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui 2>/dev/null && grep -rn \"fcWasmResolveGL\\|fcWasmSyncGLContext\\|WasmGLFixedFunc\" . 2>/dev/null | head -50; echo \"---GREP ROOT---\"; grep -rln \"fcWasmResolveGL\\|fcWas…
tool result
CMakeLists.txt:1574:    target_sources(FreeCADGui PRIVATE FcWasmProcess.cpp WasmGLFixedFunc.cpp)
WasmGLFixedFunc.cpp:224:void fcWasmSyncGLContext(void){ ffSyncContext(); }
WasmGLFixedFunc.cpp:508:void* fcWasmResolveGL(const char* name){
Quarter/QuarterWidget.cpp:71:extern "C" void fcWasmSyncGLContext(void);
Quarter/QuarterWidget.cpp:1005:  // a valid WebGL2 context. See Gui/WasmGLFixedFunc.cpp.
Quarter/QuarterWidget.cpp:1006:  fcWasmSyncGLContext();
---GREP ROOT---
/home/magik6k/lcad-wasm/freeca  …[truncated]
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD 2>/dev/null; echo \"=== glClearDepth / glDepthRange desktop-double refs in Coin/src ===\"; grep -rn \"glClearDepth\\b\\|glDepthRange\\b\" --include=*.cpp --include=*.c --include=*…
tool result
=== glClearDepth / glDepthRange desktop-double refs in Coin/src ===
src/3rdParty/coin/src/elements/GL/SoGLDepthBufferElement.cpp:152:  glDepthRange(this->range[0], this->range[1]);
src/Gui/CoinRiftWidget.cpp:446:    glClearDepth(1.0);
src/Gui/View3DInventorViewer.cpp:3150:// The calls of glDepthRange inside renderScene() causes problems with transparent objects
src/Gui/View3DInventorViewer.cpp:3177:    glDepthRange(0.1, 1.0);
src/Gui/View3DInventorViewer.cpp:3239:    glDepthRange(0.0, 0.1);
src/  …[truncated]
Fable
thinking
I'm checking how SoVertexArrayIndexer handles GL_QUADS specifically around line 146-147.
Fable
Let me verify the SoVertexArrayIndexer quad handling and how the resolver is wired into Coin's glue.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD 2>/dev/null; echo \"=== SoVertexArrayIndexer quad->triangle conversion ===\"; sed -n '140,175p;225,275p' src/3rdParty/coin/src/rendering/SoVertexArrayIndexer.cpp; echo \"=== how f…
tool result
=== SoVertexArrayIndexer quad->triangle conversion ===
void
SoVertexArrayIndexer::addQuad(const int32_t v0,
                              const int32_t v1,
                              const int32_t v2,
                              const int32_t v3)
{
  if (this->target == 0) this->target = GL_QUADS;
  if (this->target == GL_QUADS) {
    this->addIndex(v0);
    this->addIndex(v1);
    this->addIndex(v2);
    this->addIndex(v3);
  }
  else {
    this->getNext()->addQuad(v0,v1,v2,v3);
  }
}

/*!  …[truncated]
Fable
thinking
I'm seeing that SoVertexArrayIndexer passes GL_QUADS directly to glDrawElements, which triggers an INVALID_ENUM error in WebGL2 since quads aren't supported there — this prevents anything from rendering for quad-based face sets. I'm checking whether FreeCAD's geometry classes like SoBrepFaceSet handle this by triangulating internally, or if this is a broader compatibility issue.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD 2>/dev/null; sed -n '600,660p' src/3rdParty/coin/src/glue/gl.cpp"}
tool result
{
  if (!glue->dl_handle) {
    const_cast <cc_glglue *> (glue)->dl_handle = cc_dl_handle_with_gl_symbols();
  }
  return glue->dl_handle;
}

/* doc in header file */
void *
cc_glglue_getprocaddress(const cc_glglue * glue, const char * symname)
{
  void * ptr = NULL;

#ifdef __EMSCRIPTEN__
  // On WebAssembly there is no WGL/EGL/GLX/dlsym; resolve GL entry points
  // (both emscripten's real WebGL2 functions and our fixed-function emulation
  // in Gui/WasmGLFixedFunc.cpp) through emscripten's o  …[truncated]
Fable
thinking
The resolver order matters here—emscripten_GetProcAddress handles the standard WebGL2 and core GL functions that emscripten knows about, while fcWasmResolveGL picks up the legacy ones. But there's a problem: when emscripten_GetProcAddress resolves something like glDrawArrays, it returns emscripten's own implementation, not our overridden version, so Coin would be calling the wrong function.

This is the critical issue—cc_glglue_glDrawElements uses a function pointer that gets resolved through emscripten_GetProcAddress, which returns emscripten's real WebGL2 glDrawElements without any fixed-function emulation. So when Coin's VBO rendering path calls glDrawElements through that pointer, it's h  …
Fable
Critical wiring question: does Coin's VBO render path call `glDrawElements` directly (our override) or through an `emscripten_GetProcAddress`-resolved pointer (real WebGL, bypassing emulation)?
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD 2>/dev/null; echo \"=== cc_glglue_glDrawElements definition ===\"; grep -n \"cc_glglue_glDrawElements\\|glDrawElements\\b\" src/3rdParty/coin/src/glue/gl.cpp | head; echo; echo \"…
tool result
=== cc_glglue_glDrawElements definition ===
1287:    w->glDrawElements = (COIN_PFNGLDRAWELEMENTSPROC) PROC(w, glDrawElements);
1299:        !w->glDrawElements ||
3439:cc_glglue_glDrawElements(const cc_glglue * glue,
3443:  assert(glue->glDrawElements);
3444:  glue->glDrawElements(mode, count, type, indices);

=== does it resolve a pointer or call directly? ===
1275:  w->glVertexPointer = NULL; /* for cc_glglue_has_vertex_array() */
1278:    w->glVertexPointer = (COIN_PFNGLVERTEXPOINTERPROC) PROC  …[truncated]
Fable
thinking
I've found the critical issue—the PROC macro is resolving to Emscripten's actual WebGL2 functions through cc_glglue_getprocaddress, which means glDrawArrays and glDrawElements are being bound to the real WebGL implementations rather than the mocked versions.

Since glVertexPointer and glEnableClientState don't exist in WebGL2, Emscripten's lookup fails and falls back to our overrides, but glDrawArrays and glDrawElements succeed in Emscripten's table, so they bypass our fixed-function emulation entirely. This means the vertex array rendering path skips ff_setup_and_draw completely—it calls the real WebGL functions with no valid shader program bound, which is why nothing renders. The real fix   …
Fable
I have enough to assess this thoroughly. Let me verify one more thing about the quad-index path and confirm the depth-symbol situation compiles.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD 2>/dev/null; echo \"=== is glClearDepth/glDepthRange (double) defined or stubbed anywhere in-tree? ===\"; grep -rn \"void glClearDepth\\b\\|void glDepthRange\\b\\|glClearDepth(GLc…
tool result
=== is glClearDepth/glDepthRange (double) defined or stubbed anywhere in-tree? ===
(empty = not defined in-tree; relies on emscripten which only has the f-suffixed GLES variants)

=== SoBrepFaceSet draw primitive (triangles vs quads) ===
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD 2>/dev/null; find src -iname \"SoBrepFaceSet.cpp\" 2>/dev/null; echo \"---\"; grep -rln \"SoBrepFaceSet\" src/Mod 2>/dev/null | head; echo \"=== primitive used by SoBrepFaceSet ==…
tool result
src/Mod/Part/Gui/SoBrepFaceSet.cpp
---
src/Mod/BIM/ArchSectionPlane.py
src/Mod/BIM/nativeifc/ifc_generator.py
src/Mod/Part/Gui/CMakeLists.txt
src/Mod/Part/Gui/AppPartGui.cpp
src/Mod/Part/Gui/SoFCShapeObject.cpp
src/Mod/Part/Gui/DlgFilletEdges.cpp
src/Mod/Part/Gui/ViewProviderExt.cpp
src/Mod/Part/Gui/TaskFaceAppearances.cpp
src/Mod/Part/Gui/SoBrepFaceSet.cpp
src/Mod/Part/Gui/SoFCShapeObject.h
=== primitive used by SoBrepFaceSet ===
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD 2>/dev/null; f=src/Mod/Part/Gui/SoBrepFaceSet.cpp; grep -n \"glDrawElements\\|glDrawArrays\\|glBegin\\|GL_TRIANGLES\\|GL_QUADS\\|glVertexPointer\\|glEnableClientState\\|glNormalPo…
tool result
(Bash completed with no output)
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD 2>/dev/null; f=src/Mod/Part/Gui/SoBrepFaceSet.cpp; grep -n \"glDraw\\|glBegin\\|GL_TRI\\|GL_QUAD\\|Pointer\\|ClientState\\|renderShape\\|glEnable\\|glVertex\" \"$f\" | head -40; e…
tool result
470:    const SbColor* diffuse = element->getDiffusePointer();
493:    const float* trans = element->getTransparencyPointer();
652:        materialIndex.setValuesPointer(matIndex.size(), matIndex.data());
=== lines count ===
888 src/Mod/Part/Gui/SoBrepFaceSet.cpp
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD 2>/dev/null; f=src/Mod/Part/Gui/SoBrepFaceSet.cpp; grep -n \"gl[A-Z]\\|GL_\" \"$f\" | head -40"}
tool result
745:        GLint oldDepthFunc = GL_LEQUAL;
746:        glGetIntegerv(GL_DEPTH_FUNC, &oldDepthFunc);
747:        if (oldDepthFunc != GL_LEQUAL) {
748:            glDepthFunc(GL_LEQUAL);
775:        if (oldDepthFunc != GL_LEQUAL) {
776:            glDepthFunc(oldDepthFunc);
Fable
thinking
I'm checking how SoBrepFaceSet renders its geometry, likely through render helpers that call glDrawElements internally, so I need to look at the relevant code section around lines 745-775.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD 2>/dev/null; f=src/Mod/Part/Gui/SoBrepFaceSet.cpp; sed -n '700,780p' \"$f\""}
tool result
                viewProvider->setFaceHighlightActive(true);
            }
            const SoPath* currentPath = action->getCurPath();
            Gui::SoDelayedAnnotationsElement::addDelayedPath(state, currentPath->copy(), 100);
            return;
        }
        inherited::GLRender(action);
        renderHighlight(action, ctx);
        return;
    }

    SoMaterialBundle mb(action);
    mb.sendFirst();
    const bool pushed = overrideMaterialBinding(action, ctx, ctx2);
    if (!this->shoul  …[truncated]
Fable
thinking
Looking at how SoBrepFaceSet renders through its inherited GLRender method, it triangulates faces and sends them to the GPU via either the VBO path through cc_glglue_glDrawElements or immediate mode rendering, both using GL_TRIANGLES which avoids any quad handling issues for FreeCAD Part faces. Now I need to determine which rendering path is actually being used.

The choice between VBO and immediate mode depends on whether SoGLVBOElement enables vertex buffer objects — on initial frames or smaller meshes, Coin typically falls back to immediate-mode sogl_render with direct glBegin/glVertex calls where our emulation works, but the VBO path bypasses our override entirely. Either way, the critic  …
Fable
I now have a complete, evidence-backed picture. Compiling findings.
Fable
⚙ StructuredOutput {"findings": [{"severity": "critical", "category": "render-path-bypass", "summary": "Coin's primary vertex-array/VBO draw path resolves glDrawArrays/glDrawElements to emscripten's real WebGL2 functions, bypassing the fixed-function emulatio…
tool result
Output does not match required schema: /findings/0: must NOT have additional properties, /findings/1: must NOT have additional properties, /findings/2: must NOT have additional properties, /findings/3: must NOT have additional properties, /findings/4: must NOT have additional properties, /findings/5: must NOT have additional properties, /findings/6: must NOT have additional properties, /findings/7: must NOT have additional properties, /findings/8: must NOT have additional properties, /findings/9  …[truncated]
Fable
thinking
I need to remove the recommendation_line field I added since it's not part of the allowed schema, and merge that information into the recommendation and detail fields instead.
Fable
⚙ StructuredOutput {"findings": [{"severity": "critical", "category": "render-path-bypass", "summary": "Coin's primary vertex-array/VBO draw path resolves glDrawArrays/glDrawElements to emscripten's real WebGL2 functions, bypassing the fixed-function emulatio…
tool result
Structured output provided successfully