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.
You are the rendering-performance lead. Below are 6 investigation reports into why FreeCAD-wasm renders
at 3-4 fps, each tracing a profile hotspot to its root cause with proposed code-level fixes.
REPORTS (JSON):
[
{
"lane": "Qt software compositing — the RGBA64 raster path (storeRGBA8888FromRGBA64PM / fetchRGBA8888ToRGBA64PM / comp_func_SourceOver_rgb64 / fetchTransformed64 / qt_rectfill)",
"root_cause": "The Qt-wasm backingstore image is Format_RGBA8888 (straight, non-premultiplied) at /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmbackingstore.cpp:108 (`QImage::Format format = QImage::Format_RGBA8888;`). Qt's raster fast-path tables in /opt/toolchains/qtsrc/qtbase/src/gui/painting/qblendfunctions.cpp register optimized 32-bit routines ONLY for premultiplied/opaque destinations: qBlendFunctions/qTransformFunctions/qScaleFunctions have entries for Format_RGBA8888_Premultiplied and Format_RGBX8888 (lines ~28-43) but NONE for straight Format_RGBA8888 as destination (`grep qBlendFunctions[QImage::Format_RGBA8888]` -> empty). With a null fast func, QRasterPaintEngine::drawImage (qpaintengine_raster.cpp:2121/2304/2318/2409) and the span fillers fall through to blend_src_generic/blend_color_generic (qdrawhelper.cpp:4327/3993), which expand the RGBA8888 dest to 64-bit (fetchRGBA8888ToRGBA64PM via destFetch64, table at qdrawhelper.cpp:441), composite in comp_func_SourceOver_rgb64, and write back with storeRGBA8888FromRGBA64PM (qStoreFromRGBA64PM, qdrawhelper.cpp:721). Every widget paint AND the 3D FBO blit into the window therefore run the slow 64-bit generic pipeline. The FBO blit is drawn DPR-scaled, so it takes fetchTransformed64<BlendTransformed=2> (enum at qdrawhelper.cpp:846-852; table at 3339), also generic-64 because qTransformFunctions[Format_RGBA8888][*] is null. The web upload (qwasmdom.cpp:305-343) is a raw scanline byte-copy into a canvas ImageData, which is why straight (non-premult) RGBA byte order was chosen in the first place — that requirement is what forced the fast-path-less format.",
"optimizations": [
{
"fix": "Change /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmbackingstore.cpp:108 from `QImage::Format format = QImage::Format_RGBA8888;` to `QImage::Format_RGBA8888_Premultiplied`. This makes qBlendFunctions[dst][src]/qTransformFunctions/q …
Fable
thinking
I'm looking at this as a synthesis task where I need to create a ranked optimization plan from multiple reports. I have the comprehensive report data available, so I can work directly from what's been provided rather than needing to verify sources. The key challenge is reconciling some disagreements between the reports—particularly between Report 1 and the others—to arrive at a coherent prioritized plan.
Fable
I have comprehensive report data. Let me verify the most load-bearing claims before synthesizing, since this plan will be implemented directly.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Quarter 2>/dev/null && grep -n \"drawImage\\|CompositionMode\\|readback\\|paintEvent\\|actualRedraw\" QuarterWidget.cpp | head -40", "description": "Check QuarterWidget pa…
Fable
⚙ Bash {"command": "grep -n \"Format_RGBA8888\\|QImage::Format format\" /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmbackingstore.cpp 2>/dev/null | head", "description": "Check Qt wasm backingstore format line"}
Fable
⚙ Bash {"command": "grep -n \"COIN_VBO\\|COIN_VERTEX_ARRAYS\" /home/magik6k/lcad-wasm/freecad-port/deploy/index.html /home/magik6k/lcad-wasm/freecad-port/deploy-va/index.html 2>/dev/null", "description": "Check COIN_VBO env flag in deploy html"}
Fable
thinking
I need to check the specific line ranges in QuarterWidget, verify the VBO gate implementation in gl.cpp, and review the SensorManager code.
Fable
Key claims confirmed. Let me verify the QuarterWidget blit lines, the VBO gate, and SensorManager timer arming.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Quarter && sed -n '918,937p' QuarterWidget.cpp 2>/dev/null; echo \"---WASMGL---\"; grep -rn \"readbackImage\\|toImage\\|glReadPixels\\|WA_OpaquePaint\" /home/magik6k/lcad-…
Fable
⚙ Bash {"command": "sed -n '1420,1450p' /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin/src/glue/gl.cpp 2>/dev/null", "description": "Read VBO feature gate in coin gl.cpp"}
Fable
⚙ Bash {"command": "grep -n \"idletimer->start\\|delaytimer->start\\|isActive\\|sensorQueueChanged\\|start(0)\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Quarter/SensorManager.cpp 2>/dev/null | head -20", "description": "Check SensorMan…
Fable
thinking
I've verified the key findings across the codebase: the QuarterWidget is using the scaled rect overload for image drawing with WA_OpaquePaintEvent enabled, and the framebuffer object is being converted to an image in WasmGLWidget. Both fixes check out.
Now I'm reconciling the core tension between the two approaches: Report 1 suggests changing the backingstore format to Premultiplied (which requires a Qt rebuild), while Report 3 targets the specific drawImage call in QuarterWidget with CompositionMode_Source and the point overload to avoid compositing overhead. The critical advantage of Report 3's approach is that it addresses the largest per-frame image blit—the 3D FBO—without needing to re …
Fable
All load-bearing claims verified against source. Key confirmations: `QuarterWidget.cpp:933` uses the scaling-rect `drawImage` overload with default SourceOver; `WA_OpaquePaintEvent` is set (`WasmGLWidget.cpp:22`); readback is `fbo_->toImage()` (`WasmGLWidget.cpp:113`); the Coin VBO gate (`gl.cpp:1426-1444`) requires 5 unused functions (glMapBuffer/glUnmapBuffer/glGetBufferSubData/glGetBufferParameteriv/glGetBufferPointerv); backingstore format line 108 = straight `Format_RGBA8888`; `COIN_VBO='0'` forced at deploy html:98; `SensorManager.cpp:129` arms `idletimer->start(0)` unconditionally while delaytimer:131 is guarded by `isActive()`.
Here is the plan.
## Frame budget breakdown (~285 ms/frame @ 3.5 fps, from the 10 s / ~35-frame capture)
| Bucket | Capture self-time | ~ms/frame | What it is |
|---|---|---|---|
| Qt software compositing (RGBA64 path) | ~1.2 s | ~34 | store/fetchRGBA8888↔RGBA64PM, comp_func_SourceOver_rgb64, fetchTransformed64, qt_rectfill, blend_color_generic |
| QtSvg chrome re-raster | ~1.09 s | ~31 | QSvgNode::draw / QSvgG / QStroker / gray_raster — QSS widget chrome re-rendered every 3D frame |
| 3D Coin scene render | ~1.3 s (GLRenderBelowPath total) | ~38 | SoAsciiText 354, ffVertex 268, bufferData 465, getParameter 390, sphere 130 (these overlap in the traverse total) |
| FBO readback + flip + copy | ~0.21 s | ~6 | readPixels 113, do_mirror 33, QImage::copy 66 |
| Timer churn + pick | ~0.52 s + pick | ~15+ | setTimeout 382 + clearTimeout 137; plus SoRayPickAction under pointerenter |
These buckets partly overlap (SVG feeds gray_raster which is counted in compositing; the 3D numbers are inside the traverse total). Reconciled: **two CPU costs dominate and both are FreeCAD-side, no Qt rebuild needed** — (a) the 3D FBO→raster blit runs through Qt's slowest generic 64-bit SourceOver pipeline because the blit uses the scaling overload + default SourceOver into a format-mismatched image, and (b) every hover redraw force-re-rasterizes the translucent overlay panels' QSS-SVG chrome via the overlay QGraphicsEffect.
**Single biggest lever:** the 3D blit. One FreeCAD-only two-line change (CompositionMode_Source + point overload, fed by a format-matched non-premultiplied readback) removes the destination read-back entirely for the largest image drawn each frame — killing fetchRGBA8888ToRGBA64PM (250), comp_func_SourceOver_rgb64 (104), fetchTransformed64 (224) and most of store (363): ~800–940 ms/capture, no Qt rebuild.
## Ranked optimizations (highest ROI first)
| # | Fix (file:line → change) | ms saved / capture | fps effect | Effort | Rebuild | Risk |
|---|---|---|---|---|---|---|
| 1 | **Source-mode blit.** `QuarterWidget.cpp:933`: replace `painter.drawImage(viewport()->rect(), frame)` with `painter.setCompositionMode(QPainter::CompositionMode_Source); painter.drawImage(QPoint(0,0), frame);` | ~600–940 | large | trivial | FreeCAD | low (widget is WA_OpaquePaintEvent; overlays still drawn after) |
| 2 | **Format-matched readback.** `WasmGLWidget.cpp:113`: return a persistent member `QImage(Format_RGBA8888)` filled by direct `glReadPixels(GL_RGBA,GL_UNSIGNED_BYTE)` with an inline bottom-up flip, reallocated only on resize. Enables #1's memcpy + kills per-frame full-frame alloc | ~99 + alloc/GC churn | small | small | FreeCAD | low-med (reuse only while size stable) |
| 3 | **Disable overlay QGraphicsEffect on wasm.** `OverlayWidgets.cpp` `OverlayTabWidget::effectEnabled()` (~:773): `#ifdef __EMSCRIPTEN__ return false;` Stops `OverlayGraphicsEffect::draw()/sourcePixmap()` re-rendering the whole panel subtree (and its QSS SVG) every 3D frame | ~1092 (SVG lane) | large | trivial | FreeCAD | med (loses drop-shadow; only if overlays active) |
| 4 | **Relax Coin VBO gate + enable VBOs.** `gl.cpp:1428-1438`: under `__EMSCRIPTEN__` drop the glMapBuffer/glUnmapBuffer/glGetBufferSubData/glGetBufferParameteriv/glGetBufferPointerv requirements (keep Gen/Bind/Buffer(Sub)Data/Delete/IsBuffer). Then remove `E.COIN_VBO='0'` at deploy/index.html:98 + deploy-va/index.html:98 | ~600–700 (bufferData 465 + getParameter share) | med | small | Coin + runtime flag | med (relink+verify _ownBufs binding lifecycle) |
| 5 | **Guard the idle timer.** `SensorManager.cpp:129`: wrap in `if (!this->idletimer->isActive())` (mirror delaytimer:131) | ~100–200 (setTimeout/clearTimeout) | small | trivial | FreeCAD | very low |
| 6 | **SoAsciiText glyph VBO.** `SoAsciiText.cpp`: assemble glyph triangles into a cached `std::vector<float>`+VBO in `setUpGlyphs()`, replace the per-vertex `glVertex3f` loop (435/440/445) with one `glBufferData` (dirty) + one `glDrawArrays` per label; keep `glBegin` path under `#ifndef __EMSCRIPTEN__` | ~450–600 | med | medium | Coin | med (VBO recreate on context loss / cache invalidation) |
| 7 | **rAF-coalesce the redraw drain.** `SensorManager.cpp` (:128 delay path): on `__EMSCRIPTEN__` drain via a single pending `emscripten_request_animation_frame` flag instead of `idletimer->start(0)`; collapses multiple scheduleRedraw in one input burst into one render | ~300 + one avoided full render/burst | med | medium | FreeCAD | med (must stop rAF when queue empty; test under JSPI) |
| 8 | **GL desktop-enum query short-circuits.** `SoGLLazyElement.cpp:428` (GL_RGBA_MODE→true), `SoGLClipPlaneElement.cpp:119` (MAX_CLIP_PLANES→6), `SoGLLightIdElement.cpp:182` (MAX_LIGHTS→8): hard-code on wasm | ~40–80 (getParameter/getError) | small | small | Coin | low |
| 9 | **Backingstore → premultiplied (all non-3D chrome).** `qwasmbackingstore.cpp:108` → `Format_RGBA8888_Premultiplied`; pair with unpremultiply-on-dirty-rect in `qwasmdom.cpp:305-343` | ~800 (chrome only; 3D already fixed by #1) | med | trivial patch | **Qt rebuild** | med (translucent popup edges need the unpremultiply) |
| 10 | **sogl_render_sphere/cone/cylinder → VA.** `SoGL.cpp:552-728`: fill scratch coords/normals, one glDrawArrays/strip | ~130–200 | small | medium | Coin | med (shared macros across shapes) |
| 11 | **Preselection pick throttle.** `SoFCUnifiedSelection.cpp:1051-1070`: skip getPickedList unless pointer moved > N px / once per frame | ~200–600 | med | small | FreeCAD | med (too coarse hurts highlight responsiveness) |
| 12 | **STRUCTURAL: dedicated WebGL canvas** for the 3D region (Coin renders to a real DOM `<canvas>` default FBO; punch a transparent hole in the Qt backing store). Eliminates readPixels+toImage+drawImage+putImageData for 3D pixels | ~1150+ (whole lane) | very large | large | FreeCAD (+Qt backing-store punch-through) | high (event/z-order/DPR/resize sync; overlays must move) |
**Quick wins (no Qt rebuild): #1, #2, #3, #5** (FreeCAD-only) and **#4, #6, #8** (Coin relink). **Structural: #9 (Qt rebuild), #7 (rAF), #11, #12.**
## Recommended first batch (best fps/effort)
Do these four together — one FreeCAD relink + one Coin relink, no Qt rebuild — for the bulk of the win:
1. **#1 Source-mode blit** — `QuarterWidget.cpp:933`:
```cpp
QPainter painter(this->viewport());
painter.setCompositionMode(QPainter::CompositionMode_Source);
painter.drawImage(QPoint(0,0), frame);
```
2. **#2 Format-matched persistent readback** — `WasmGLWidget.cpp:108-116`: `glReadPixels(0,0,w,h,GL_RGBA,GL_UNSIGNED_BYTE, buf)` into a reused `QImage(w,h,QImage::Format_RGBA8888)` member (realloc on resize), flip inline bottom-up. Required for #1 to collapse to a copy and to kill the per-frame FBO-image alloc. (#1+#2 together: ~700–1000 ms/capture, the single largest lever.)
3. **#3 Disable overlay QGraphicsEffect on wasm** — `OverlayWidgets.cpp` `effectEnabled()` returns false under `__EMSCRIPTEN__` (~1092 ms SVG lane). If profiling shows overlays were *not* active in the session, this yields less — but it is a trivial, zero-cost-when-inactive guard, so land it regardless.
4. **#4 Relax the VBO gate + set COIN_VBO=1** — `gl.cpp:1428-1438` (drop the 5 unused function checks under `__EMSCRIPTEN__`) and delete `E.COIN_VBO='0'` in both deploy HTMLs (~600–700 ms). This is a Coin relink; bundle **#8** (GL enum short-circuits, same rebuild) with it for another ~40–80 ms.
Add the trivial **#5** timer guard to the FreeCAD relink (one line, ~100–200 ms, zero risk).
Expected first-batch total: roughly **1.6–2.2 s off the 10 s capture** → frame time ~285 ms → ~150–190 ms, i.e. **3.5 fps → ~5.5–7 fps**, with no Qt rebuild.
**On the Qt rebuild (#9):** it is NOT needed for the first batch. Report 1 proposes changing the backingstore to premultiplied to fix the RGBA64 pipeline; report 3 achieves the same result for the 3D blit — the dominant single image — purely in FreeCAD via CompositionMode_Source (Source mode skips the destination read entirely, which is what fetchRGBA8888ToRGBA64PM + comp_func_SourceOver_rgb64 cost). The Qt change only additionally helps the *remaining* widget chrome (toolbars/tree/menus), and its benefit shrinks further once #3 stops the per-frame chrome re-raster. Defer #9 until a post-batch profile shows chrome compositing (not the 3D blit) still hot; then it is worth the ~800 ms and the rebuild.
## Verification
Re-capture the same ~10 s hover/pointer profile in the existing harness after each relink and watch specific lines:
- **#1+#2:** `fetchRGBA8888ToRGBA64PM` (250) and `comp_func_SourceOver_rgb64` (104) should drop to near-zero; `fetchTransformed64` (224) gone (no transform, point overload); `do_mirror` (33) + `QImage::copy` (66) gone (inline flip, no toImage); heap-alloc/GC churn drops. `readPixels` (113) stays.
- **#3:** `QSvgNode::draw` / `QSvgG::drawCommand` (1092) collapses when overlays are present. To confirm the mechanism before rebuild, instrument `OverlayGraphicsEffect::sourcePixmap` (OverlayWidgets.cpp:2819) hit-count per frame — expect it to fall to 0 on 3D-only repaints.
- **#4:** `bufferData` (465) drops to a one-time upload (watch it appear once at load, not per frame); `getParameter` (390) partially drops (client-array temp-buffer state queries gone). Confirm no visual geometry regression and no `_ownBufs` stray-binding artifacts.
- **#5/#7:** `setTimeout` (382) + `clearTimeout` (137) shrink; with #7, `Timer fired` (472) drops and multiple GLRenderBelowPath per burst collapse to one.
- **#6:** `SoAsciiText::GLRender` self (354) + text share of `ffVertex` (268) drop; verify datum/annotation labels still render.
- **fps check:** use the existing deploy/deploy-va harness FPS counter during a steady hover-over-edge interaction; record before (3–4 fps) and after each batch. Toggle `?va=1`/`va=0` to isolate the VA/VBO path.
## Risks / unknowns
- **#1/#2 format contract:** the Source-mode memcpy is only correct if the readback QImage format exactly matches the backing store (both straight `Format_RGBA8888`, same byte order as `putImageData`). Do #2 before/with #1. The FBO is opaque (WA_OpaquePaintEvent, GL_RGBA8 background) so Source (overwrite, no alpha blend) is safe; verify the nav-cube/axis overlays still composite via `inherited::paintEvent` (runs after, unchanged).
- **#4 VBO invalidation:** SoVBO re-uploads on `dataid` change, so static CAD geometry is cached but any workbench mutating geometry per frame (Draft/Sketcher live preview) re-uploads and partially negates the win for that subtree — not a correctness risk. The real hazard is the known `_ownBufs` scratch-VBO leftover binding being mistaken for a Coin VBO; the VA fix handled this for VA mode but VBO mode changes the binding lifecycle — relink + visual verify required (medium confidence).
- **#3 correctness/coverage:** purely visual (loses overlay drop-shadow/translucency). If the profiled session had overlays *off*, the SVG cost shifts to toolbar/QGraphicsView chrome overlapping the dirty region — same QSS-SVG root cause but #3 won't touch it; the fallback is pre-baking the 86 QSS `.svg` refs to PNG or bumping `QPixmapCache::setCacheLimit` at startup (currently unset → default 10 MB, likely thrashing).
- **#7 rAF coalescing:** must stop the rAF loop when no sensors are pending (else a busy render loop), and keep spin/animation responsive; interaction with JSPI/asyncify pumping needs testing — hence medium confidence, second batch.
- **#9 premultiplied backingstore:** translucent popup/tooltip/shadow edges uploaded raw via putImageData are wrong without the companion unpremultiply pass in qwasmdom.cpp; opaque main window is unaffected. Also confirm whether `qt_blend_argb32_on_argb32` compiles to the wasm-SIMD path (big win) vs scalar fallback (win nearer low end).
- **#11 pick throttle:** low confidence — too large a pixel threshold hurts highlight responsiveness and can drop the settled-position highlight; must still fire once the pointer stops.
- **Reconciled disagreement (bufferData 465):** report 5 attributes it to geometry client-array re-upload (fixed by #4), report 4 to SoAsciiText immediate-mode glyph flush (fixed by #6). Both are real and additive on different buffers — do not expect either fix alone to zero it.
Relevant files (all absolute): `/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Quarter/QuarterWidget.cpp`, `/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/WasmGLWidget.cpp`, `/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/OverlayWidgets.cpp`, `/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Quarter/SensorManager.cpp`, `/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin/src/glue/gl.cpp`, `/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/3rdParty/coin/src/shapenodes/SoAsciiText.cpp`, `/home/magik6k/lcad-wasm/freecad-port/deploy/index.html`, `/home/magik6k/lcad-wasm/freecad-port/deploy-va/index.html`, `/opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmbackingstore.cpp`.