9 KiB
10 — Re-enable the 3D viewer
Verdict: port — and there's a cheap first step. KiCad ships two 3D renderers: an OpenGL one (pure fixed-function GL 1.x — far from WebGL2) and a pure-CPU raytracer that needs zero OpenGL. The fork currently nulls the raytracer out. Turning 3D on via the raytracer (Route C) is low-risk and brings the render-to-PNG job and VRML export back nearly free; a WebGL2 port of the interactive renderer (Route B) is the follow-up.
Why it's off (timeline, with primary sources)
| Date | Commit (kicad) | What |
|---|---|---|
| 2025-12-21 | 46bb16e86b |
"Enable 3D viewer build for WASM with OpenGL stubs" — compiled only so headers resolve. Message: "OpenGL rendering won't work at runtime … avoids complex stubbing." |
| 2026-01-03 | a61d2ea406 |
OpenGL GAL on -sLEGACY_GL_EMULATION; added the +190-line kiglew.h __EMSCRIPTEN__ branch (display lists + lighting as no-ops). 3D "built" but couldn't draw. |
| 2026-01-10 | d8a9ac4304 |
"Disable 3D viewer functionality for WASM builds" — same day WEBGL_GAL landed and the build swapped emulation → -sFULL_ES3. Message: 3D "require[s] native OpenGL rendering (not WebGL)". Built with KICAD_BUILD_3D_VIEWER_WASM=OFF. |
| 2026-03-20 | 4ddb9b47f5 |
"Migrate to pure WebGL 2.0 without FULL_ES3 emulation" — 2D GAL goes pure WebGL2; the only GL link flag today is -sMAX_WEBGL_VERSION=2. |
Recorded rationale: features/gl-article/README.md:274-276,421-422 — the 3D viewer is "a
separate legacy-GL renderer, not GAL" needing "GLU + fixed-function", declared out of scope
when the strategy became "progressively remove the Emscripten crutch." So it was a deliberate
scoping decision when the emulation layer it depended on was removed — not a crash.
What the OpenGL renderer actually uses
3d-viewer/3d_rendering/opengl/ = 6 files, 4,555 lines. It's pure GL 1.x fixed-function:
- Immediate mode — 153 tokens (
glBegin/glEnd×28,glVertex*/glNormal3*/glTexCoord2*). - Display lists — 36 refs in 2 files, and they're the primary board-geometry path:
layer_triangles.cpp:582-714bakes every layer's triangles intoglGenLists/glNewListwrapping aglDrawArrays;render_3d_opengl.cpp:1303-1315for the grid. - Fixed-function matrix/lighting — ~117 tokens (
glMaterialfv,glLightfv,glMatrixMode/glPush/PopMatrix,GL_LIGHTING, …). - Client-state arrays — 49 tokens. GLU — 36 (
gluNewQuadric/gluCylinder/gluSpherefor vias/pads). - Modern — zero shaders, zero VAOs; only
3d_model.cppuses VBOs (GL 1.5 style).
Structurally friendlier than the raw counts suggest: geometry already lives in CPU triangle
containers, display lists are thin glDrawArrays wrappers, 3d_model.cpp is already VBO,
camera/transforms are already glm.
Route A — LEGACY_GL_EMULATION for the 3D viewer: REJECTED
- The flag is module-global at link:
GLEmulation.init()replaces the module-wide JS bindings (glDrawArrays,glEnable,glBindBuffer,glGetString, …) for all contexts, including the flagship 2DWEBGL_GAL's. Concretely it corrupts the GAL's ES3 shaders — emscripten prepends#extension GL_OES_standard_derivativesto any fragment shader usingdFdx(kicad_frag.glsl:138does), and prepending anything before#version 300 esis a guaranteed ES3 compile error. Separate canvases/contexts do not isolate this (hooks are per-module). - Even ignoring that: emscripten never implemented display lists (issue #688 — the main board
path draws nothing), GLU quadrics are absent, lighting emulation has
throw 'TODO'holes. You'd rewrite the display-list and quadric paths anyway (the hard half of Route B) while regressing 2D and re-adding ~200 KB of the fragility the project already escaped. Reject.
Route C — CPU raytracer + blit (do this first, low risk)
RENDER_3D_RAYTRACE_RAM (render_3d_raytrace_ram.cpp, 159 lines) is 100% GL-free: it
renders into a plain uint8_t RGBA buffer (initPbo() is just new uint8_t[w*h*4]) and
exposes GetBuffer(). It's progressive by design — each Redraw() traces blocks up to a
400/750 ms budget then yields, so a serial browser loop still animates. Both existing
consumers already convert buffer → wxImage with no GL (eda_3d_viewer_frame.cpp:846-866
screenshot; pcbnew_jobs_handler.cpp:817-885 render job).
Recipe:
KICAD_BUILD_3D_VIEWER_WASM=ON, and genex-out the GL renderer TUs (render_3d_opengl.cpp,layer_triangles.cpp, …) the same wayrender_3d_raytrace_gl.cppis already excluded (3d-viewer/CMakeLists.txt:49) — this sidesteps the legacy-GL link errors entirely. The raytracing subtree (12,232 lines) has no GL.- Have
EDA_3D_CANVASinstantiateRENDER_3D_RAYTRACE_RAMunder__EMSCRIPTEN__(it's renderer-agnostic viaRENDER_3D_BASE), and blit the buffer — either a plain wxWindow painting thewxImagethrough the wx-wasm 2D canvas DC (zero GL), or a ~100-line WebGL2 textured quad (infra exists incommon/gal/webgl/fullscreen_quad.cpp). - Delete the fork gates — they're keyed on
__EMSCRIPTEN__, not the CMake option, so flipping the flag alone isn't enough:eda_3d_canvas.cpp(raytracer nulled, raytracing request early-returned),pcbnew/pcbnew.cpp(3D settings panels — see thePANEL_TOOLBAR_CUSTOMIZATIONbug in 07),pcbnew_jobs_handler.cpp/.h(render job), andexporter_vrml.cpp(in-file stub). Delete the three big stub TUs too (wasm/stubs/3d_viewer_stub.cpp,3d_canvas_stub.cpp,3d_scenegraph_stub.cpp, ~1,200 lines).
Caveats: main tracing runs serial under the bs_thread_pool inline patch (still usable,
because progressive); the preview / SSAO / DLAA passes use raw std::thread + a sleep_for
spin-wait — pthreads are enabled in the build (-pthread -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=…), so they run, but the spin-wait blocks the browser main thread per
pass and is worth reviewing for jank.
Route B — WebGL2 port of the interactive renderer (follow-up)
Scope is ~4,555 lines / 6 files — roughly 1/5 of the 2D GAL port and far less novel (no
compositor, no multi-FBO juggling). The work: display-list → VBO (mechanical, the lists wrap
glDrawArrays of CPU triangle containers), client-state arrays → generic vertex attribs,
fixed-function transforms → a small matrix-stack helper (already glm), ~2 shader pairs
(Phong-lit per-material + flat), GLU quadrics → a triangle helper (small; vias/pads/gizmo).
Reuse common/gal/webgl/'s shader infra + convert_glsl_es3.py.
Divergence: mirror the 2D precedent — new files (3d_rendering/webgl/), swapped via a CMake
genex like the existing render_3d_raytrace_gl.cpp exclusion. Near-zero upstream-file diff
beyond the swap + un-gating. Gives the interactive GPU-speed orbit/pan view. Risk: medium
(GPU correctness; the e2e screenshot harness pattern exists). Prereq: model loading (below).
Model loading (needed for both B and C to show components)
Upstream loads plugins/3d/{vrml,oce,idf} as runtime DSOs —
S3D_PLUGIN_MANAGER::loadPlugins() scans a directory and wxDynamicLibrary-loads each. There
is no static-registration path upstream, and wasm/stubs/3d_scenegraph_stub.cpp (440
lines) currently stubs the whole S3D_CACHE + scenegraph + S3D::WriteVRML.
Static linking entails: compiling the plugin sources into the binary with per-plugin symbol
prefixes (they export identical C ABI names — Load, GetFileFilter, … — so they collide;
wrap each TU with -Dname=vrml_name or a wrapper), plus a small __EMSCRIPTEN__ path (or a
subclassed KICAD_PLUGIN_LDR_3D) with a 3-entry static registry — cleanest as one new
wasm-layer file. Sizes: vrml (VRML1/2 + X3D, fully in-tree, no external deps) = 17,835
lines; oce (STEP/IGES — needs OCC, which is already built for wasm: OCC 7.8 via
--with-occ, verified libTK*.a in the sysroot) = 1,407; idf = 931. kicad_3dsg must
become STATIC for wasm (it's add_library(SHARED) today). Plus: ship 3D model assets to the
FS (models are referenced, not embedded — see
../libraries/0001-library-management.md).
Start with the VRML plugin (no deps) for .wrl models; add OCE for STEP once VRML works.
Render job & VRML export — free with either route
Both only need the 3D model cache / scenegraph / raytracer, not the GL renderer:
JobExportRender(pcbnew_jobs_handler.cpp:641-898) isRENDER_3D_RAYTRACE_RAM+BOARD_ADAPTER+ 3d_cache → board-render-to-PNG/JPEG works as soon as 3d-viewer links.exporter_vrml.cpp's real implementation needsS3D_CACHE+ the SGNODE/IFSG API — un-stub by deleting the fork's#elseblock (a divergence reduction). Board-only export works immediately; embedding component models additionally needs the static VRML plugin above.
Sequencing
C first (working 3D view + render job + VRML export, low risk, mostly deletes gates) → B second (interactive upgrade) → A never.