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: Qt software compositing — the RGBA64 raster path (~1s: storeRGBA8888FromRGBA64PM/fetchRGBA8888/comp_func_SourceOver_rgb64/fetchTransformed64/qt_rectfill).
Investigate: WHY does the Qt-wasm backingstore composite in 64-bit (QRgba64) instead of 32-bit ARGB32?
- What QImage::Format does the wasm backingstore / the FBO-blit QImage use? (qwasmbackingstore.cpp, qwasmwindow.cpp).
RGBA64 fetch/store implies a source or dest in Format_RGBA64* or a gamma/precision path. Find where it's chosen.
- The 3D FBO blit: what format is the grabbed FBO image (RGBA8888? premultiplied?), and does QPainter::drawImage
into the widget trigger an RGBA64 conversion (fetchRGBA8888ToRGBA64PM -> composite -> storeRGBA8888FromRGBA64PM)?
- Is the whole window surface re-composited each frame, or just the dirty 3D rect? Check the backingstore flush /
the widget repaint region. If a full-window RGBA64 recomposite happens per 3D frame, that's the ~1s.
Propose: force ARGB32_Premultiplied (32-bit) compositing for the window/FBO blit to skip RGBA64 entirely; and/or
limit the repaint to the 3D viewport rect. Estimate ms saved.
Fable
thinking
I'm diving into the Qt-wasm backingstore compositing path to understand how it works.
Fable
⚙ Bash {"command": "ls /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/ 2>/dev/null; echo \"---\"; find /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm -name \"qwasmbackingstore*\" -o -name \"qwasmwindow*\" -o -name \"qwasmscreen*\…
Fable
⚙ Bash {"command": "grep -rn \"grabFramebuffer\\|QOpenGLFramebufferObject\\|drawImage\\|Format_RGB\\|Format_ARGB\\|Format_RGBA\\|convertToFormat\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/View3DInventorViewer.cpp 2>/dev/null | head -60…
Fable
thinking
I'm examining the backing store implementation to understand how QImage formats are handled in the WebAssembly compositor.
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmbackingstore.cpp"}
Fable
⚙ Bash {"command": "grep -n \"Format_\\|QImage\\|composeAndFlush\\|blit\\|drawImage\\|QRgba64\\|Rgba64\\|texSubImage\\|readPixels\\|do_mirror\\|colorspace\\|ColorSpace\" /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmwindow.cpp /opt/…
Fable
thinking
The backing store uses RGBA8888 format, and the 3D FBO blit can produce either RGB32 or RGBA8888 depending on the code path. I'm tracing through how the RGBA64 path works, which involves QPainter compositing an image source into the RGBA8888 backing store.
The key insight is that RGBA8888 isn't a native Qt format—Qt's optimized paths are for ARGB32_Premultiplied and RGB32. When compositing into RGBA8888, Qt's raster engine falls back to a generic 64-bit pipeline: it fetches the RGBA8888 data, converts to RGBA64 for compositing, then stores back. The profile data matches exactly—the slow functions are the fetch and store conversions plus the 64-bit composite operation. So the root cause is t …
Fable
⚙ Bash {"command": "grep -n \"Format_RGBA8888\\|convert_generic\\|Rgba64\\|fetchRGBA8888ToRGBA64PM\\|storeRGBA8888FromRGBA64PM\\|Fetch64\\|Store64\\|fetchTransformed64\\|DestFetchProc64\\|fetch64\\|fetchProc64\\|convert_to_rgb64\\|fetchTransformed…
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmscreen.cpp", "offset": 148, "limit": 12}
Fable
thinking
I need to trace through the blend function selection logic to understand why RGBA8888 compositing uses the 64-bit blend path instead of the 32-bit optimized version. The decision happens in the composition function dispatcher, specifically around whether RGBA8888 has a dedicated 32-bit fast path or falls back to the generic 64-bit handler.
Fable
⚙ Bash {"command": "grep -n \"m_format\\|Format_\" /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmscreen.cpp /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmscreen.h; echo \"=== storeFromRGBA64PM / blend selection ===\";…
Fable
⚙ Bash {"command": "grep -n \"TextureBlendType\\|fetchTransformed64\\|getBlendType\\|BilinearTransform\\|updateSpanData\\|type = \\|blend = \\|bilinear\" /opt/toolchains/qtsrc/qtbase/src/gui/painting/qdrawhelper.cpp | head -40", "description": "Fi…
Fable
thinking
I'm zeroing in on why RGBA8888 as the destination format routes through the 64-bit blend path instead of the optimized 32-bit routines. The blend function selection logic in the raster paint engine determines this based on the destination format, and I need to trace exactly where ARGB32PM versus RGBA8888 causes the engine to choose between the specialized 32-bit blend operations and the generic 64-bit fallback.
The key insight is that Qt only has fast-path blend routines for ARGB32_Premultiplied and RGB32 destinations, so any other format like RGBA8888 forces the engine to use the generic blend path that converts to RGBA64, composites there, and converts back. Additionally, the image being …
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/gui/painting/qdrawhelper.cpp", "offset": 846, "limit": 14}
Fable
⚙ Bash {"command": "grep -n \"qt_blend_argb32_on_argb32\\|convertToFormat\\|Format_ARGB32_Premult\\|Format_RGB32\\|d->rasterBuffer->format\\|useFastImageBlending\\|canUseFastImageBlend\\|drawImage\\|isBlitting\\|d->blend\" /opt/toolchains/qtsrc/qt…
Fable
thinking
So TextureBlendType 2 corresponds to BlendTransformed, which means we're dealing with a scaled or DPI-adjusted framebuffer image being drawn through the 64-bit generic rendering path without bilinear filtering. Now I need to check the qBlendFunctions lookup table to see what blend function entries exist for the destination and source formats.
Fable
⚙ Bash {"command": "grep -n \"qBlendFunctions\\|qTransformFunctions\\|qScaleFunctions\\|SrcOverBlendFunc qBlend\" /opt/toolchains/qtsrc/qtbase/src/gui/painting/qblendfunctions.cpp | head; echo \"=== table def ===\"; sed -n '/SrcOverBlendFunc qBlen…
Fable
thinking
I found the root cause—the fast blend and transform functions are only registered for premultiplied RGBA8888 format, but the backing store uses non-premultiplied RGBA8888, so those optimized paths never get used and fall back to slower implementations.
Fable
⚙ Bash {"command": "grep -n \"qBlendFunctions\\[QImage::Format_RGBA8888\\]\\|qTransformFunctions\\[QImage::Format_RGBA8888\\]\\|qScaleFunctions\\[QImage::Format_RGBA8888\\]\" /opt/toolchains/qtsrc/qtbase/src/gui/painting/qblendfunctions.cpp; echo …
Fable
thinking
Looking at the backing store configuration, the QImage format is set to RGBA8888 with straight alpha, but the blend, transform, and scale function tables for this format are all unregistered and pointing to null.
When these fast paths aren't available, the raster engine falls back to generic functions that convert everything through the RGBA64 pipeline—fetching to 64-bit, compositing, then storing back. This happens for every blend operation, transform, and scale, including widget paints and the FBO scaling with device pixel ratio.
The fix seems straightforward: switch the backing store to RGBA8888_Premultiplied format, which would unlock all the optimized 32-bit fast paths. But there's a …
Fable
I have the root cause confirmed. Let me verify one more detail about the upload path's alpha expectation.
Fable
⚙ Bash {"command": "grep -n \"drawImageToWebImageDataArray\\|putImageData\\|premultipli\\|unpremultipli\\|Format_RGBA8888\\|constBits\\|createImageData\\|Uint8\" /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmdom.cpp | head -30", "de…
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmdom.cpp", "offset": 305, "limit": 40}
Fable
thinking
The core issue is that the image upload does a direct byte copy into canvas ImageData, which requires RGBA byte order with straight alpha for translucent windows to render correctly—that's why Format_RGBA8888 was chosen, but it's the one format without optimized 32-bit blend paths. So there's a real tradeoff between correctness for transparency and rendering performance, and I'm weighing different format options to resolve it. Translucent popups would show minor artifacts at the edges, but that's acceptable or can be handled per-window. I'm recommending Format_RGBA8888_Premultiplied at line 108 to unlock the fast 32-bit argb32-on-argb32 blending paths for all compositing operations, bypassin …
Fable
I have everything I need. The root cause is confirmed end-to-end.
**The mechanism:** The wasm backingstore QImage is created as `QImage::Format_RGBA8888` (straight/non-premultiplied alpha) at `qwasmbackingstore.cpp:108`. In Qt's raster engine, the optimized 32-bit blit/blend/scale/transform function tables (`qBlendFunctions`, `qTransformFunctions`, `qScaleFunctions` in `qblendfunctions.cpp`) have **no entries for `Format_RGBA8888` as destination** — only `Format_RGBA8888_Premultiplied`, `Format_RGBX8888`, `Format_ARGB32_Premultiplied`, and `Format_RGB32` are registered (grep for `qBlendFunctions[QImage::Format_RGBA8888]` returns nothing). When the fast func pointer is null, `QRasterPaintEngine::drawImage` and the span fillers fall through to `blend_src_generic`/`blend_color_generic`, which run the RGBA64 pipeline: `fetchRGBA8888ToRGBA64PM` → composite in `comp_func_SourceOver_rgb64` → `storeRGBA8888FromRGBA64PM`. That is exactly the profiled lane. The FBO blit adds `fetchTransformed64<BlendTransformed=2>` (DPR-scaled drawImage of the grabbed viewport) through the same generic-64 path.
Fable
⚙ StructuredOutput {"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_RGBA8…