feat(3d): gl1 shim M0-M2 — FFP-on-WebGL2 substrate + lighting; 12 scenarios truly green
wasm/gl1: the GL1.x fixed-function emulation layer replacing the
gl_ffp_stub.c no-ops in the 3d-regression harness link. This batch:
- symbol split: 52 FFP-only entry points implemented; 10 Emscripten-owned
names intercepted via wasm-ld --wrap (sources.txt/wrapped_symbols.txt are
the shared manifests for both link sites; production hookup lands in M7)
- matrix stacks (MODELVIEW/PROJECTION, glGetFloatv readback), immediate
mode with all 8 GL1 primitive conversions, GL1-default state mirror
- full GL 1.5 Gouraud lighting uber-shader (eye-space light capture at
glLightfv time, color-material, two-side, COMBINE evaluator + alpha test
wired but inert until M3/M5)
- draw routing: FFP traffic identified by GL_VERTEX_ARRAY client state;
blit/2D-GAL draws pass through untouched
- display lists: correct glGenLists/glIsList existing-empty semantics;
recorder itself is M3 (recorded commands drop with a one-time warning)
Parity: 20/47 under the 0.02 floor, of which 12 verified genuinely
rendering (bg-gradient x2, bounding-box, half-open-cylinder, segment x2,
material x3, light x3 — eyeballed against baselines); the other 8 are
small-geometry scenarios whose missing GLU/display-list content sits under
the floor (become real in M3/M4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 11:37:51 +02:00
|
|
|
# wasm/gl1 — GL 1.x fixed-function pipeline on WebGL2
|
|
|
|
|
|
|
|
|
|
Emulation layer that lets KiCad's 3D viewer renderer (`RENDER_3D_OPENGL`,
|
|
|
|
|
pure GL 1.x fixed-function, compiled **unmodified**) render in the browser.
|
|
|
|
|
Supersedes the no-op link stubs of `wasm/stubs/gl_ffp_stub.c` (git history
|
|
|
|
|
has the old file): same public surface, real implementations.
|
|
|
|
|
|
|
|
|
|
TDD harness: `tests/3d-regression/` — 47 native-golden scenarios; parity via
|
|
|
|
|
`npm run 3d:check:parity` (see that README).
|
|
|
|
|
|
|
|
|
|
## How the symbols resolve
|
|
|
|
|
|
|
|
|
|
In the WASM build, `gl*` are plain C functions split between two providers:
|
|
|
|
|
|
|
|
|
|
- **Emscripten's WebGL library** (`-sMAX_WEBGL_VERSION=2`) owns every
|
|
|
|
|
modern/WebGL2 name (`glClear`, `glDrawArrays`, `glTexImage2D`, buffers,
|
|
|
|
|
shaders, stencil...).
|
|
|
|
|
- **This shim** owns the FFP-only names WebGL2 lacks (`glBegin`, display
|
|
|
|
|
lists, matrix stack, `glLight*`/`glMaterial*`, client-array pointers,
|
|
|
|
|
`glTexEnv*`, `glAlphaFunc`, GLU quadrics) — see `src/gl1_entry_ffp.cpp`.
|
|
|
|
|
|
|
|
|
|
The emulator must also *observe* a few Emscripten-owned calls (FFP `glEnable`
|
|
|
|
|
caps, draws over client arrays, matrix readback, state recorded inside
|
|
|
|
|
display lists). Those are intercepted with **wasm-ld `--wrap`**: every name in
|
|
|
|
|
`wrapped_symbols.txt` gets a `-Wl,--wrap=<sym>` flag at both link sites, the
|
|
|
|
|
interceptors live in `src/gl1_entry_wrapped.cpp`, and `__real_*` forwards to
|
|
|
|
|
the WebGL library. No kicad/wxwidgets sources are touched.
|
|
|
|
|
|
|
|
|
|
Link sites (both read `sources.txt` + `wrapped_symbols.txt`):
|
|
|
|
|
|
|
|
|
|
- `tests/3d-regression/wasm/Makefile` (the TDD harness)
|
|
|
|
|
- `scripts/kicad/build-kicad-target.sh` (`GL3D_LINK_FLAGS`, production
|
|
|
|
|
`kicad_editor`)
|
|
|
|
|
|
|
|
|
|
## Draw routing
|
|
|
|
|
|
fix(3d): blank viewer after raytracing round-trip — owner-context FFP routing + VAO isolation (gl1)
Switching OpenGL → raytracing → OpenGL could leave the viewer showing only the
background gradient, with mid-session "[gl1] WebGL context changed" thrash and
INVALID_OPERATION storms on BOTH WebGL contexts. Traced mechanism: the shim's
FFP draw routing keyed on one process-global client-array flag; an interrupted
fixed-function window (MODEL_3D::BeginDrawMulti loops, or the switch-back
reload re-recording display lists with the GL context lock released across
JSPI suspensions) left it set, after which the raytracer blit's and the 2D
GAL's glDrawArrays were routed through the FFP pipeline — and one misrouted
draw permanently repointed the VICTIM's own VAO attributes at shim buffers
(the blit's attribute 0 collides with ATTR_POSITION), so both stayed
broken/blank even after the flag cleared.
Shim fixes (wasm/gl1):
- Owner-context routing gate: __wrap_glDrawArrays/Elements route into the FFP
pipeline only under the shim's owner context (adopted at the first FFP
client-state mutation or programSync in a context); foreign-context draws
always pass through — the 2D GAL can never be misrouted and the context
guard can never thrash.
- VAO isolation (ScopedDefaultVAO): draw executors do their attribute setup on
VAO 0 and restore the caller's binding — a misrouted draw can no longer
corrupt the caller.
- contextSync() resets the whole client-array mirror on a context change
(enables/pointers/VBO names all described the dead context).
kicad pointer bump (d6e3dc1a87a): blit preamble disables the four client
arrays (same-context firewall) + DoRePaint hidden-parent early return now
clears m_is_currently_painting like its six siblings (a standalone
sufficient cause of a permanently blank viewer).
TDD (each observed red before its fix, green after; harness = authoritative):
- T1 VAO corruption, T2 foreign-context routing + guard thrash, T3 stale
client-state surviving context recreation — tests/e2e/3d-webgl.spec.ts over
new harness choreography (appQuad/ffpMakeStale/createSecondContext/
quadDrawFresh). Parity stays 47/47 with zero drift.
- tests/kicad/3d-viewer-engine-toggle.spec.ts (new, CI-skipped like the
deadlock spec): real round-trip happy-path gate — board re-renders, zero
[gl1] lines, zero INVALID_OPERATION. (The raytraced image itself never
displays on the wasm build — the pre-existing inert-toggle KNOWN ISSUE in
3d-viewer-deadlock.spec.ts, out of scope here; the engine switch and the
poisoning reload path run regardless.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 11:24:26 +02:00
|
|
|
A `glDrawArrays`/`glDrawElements` call is FFP traffic iff BOTH hold:
|
|
|
|
|
|
|
|
|
|
- the CURRENT WebGL context is the shim's **owner context** (`contextIsOwner()`,
|
|
|
|
|
adopted at the first FFP client-state mutation or `programSync()` in a
|
|
|
|
|
context — a draw under any other context is a modern-GL consumer by
|
|
|
|
|
definition and always passes through, even mid-display-list-recording);
|
|
|
|
|
- `GL_VERTEX_ARRAY` client state is enabled (only GL1 code calls
|
|
|
|
|
`glEnableClientState`; the raytracer blit and the 2D WebGL GAL drive their
|
|
|
|
|
own GLSL programs).
|
|
|
|
|
|
|
|
|
|
The owner gate exists because the client-state flag is process-global and an
|
|
|
|
|
interrupted FFP window (e.g. `MODEL_3D::BeginDrawMulti` loops crossing a JSPI
|
|
|
|
|
suspension) can leave it set — without the gate, the 2D GAL's draws got routed
|
|
|
|
|
through the FFP pipeline (the engine-toggle blank-viewer bug). Two further
|
|
|
|
|
hardenings from the same bug:
|
|
|
|
|
|
|
|
|
|
- **VAO isolation** (`ScopedDefaultVAO`, gl1_draw.cpp): draw executors bind
|
|
|
|
|
VAO 0 for their attribute setup and restore the caller's binding — a routed
|
|
|
|
|
draw arriving with the caller's VAO bound (the blit's attribute 0 collides
|
|
|
|
|
with `ATTR_POSITION`) must never scribble it.
|
|
|
|
|
- `contextSync()` resets the client-array mirror entirely on a context change:
|
|
|
|
|
enables, pointers and captured VBO names all described the dead context.
|
feat(3d): gl1 shim M0-M2 — FFP-on-WebGL2 substrate + lighting; 12 scenarios truly green
wasm/gl1: the GL1.x fixed-function emulation layer replacing the
gl_ffp_stub.c no-ops in the 3d-regression harness link. This batch:
- symbol split: 52 FFP-only entry points implemented; 10 Emscripten-owned
names intercepted via wasm-ld --wrap (sources.txt/wrapped_symbols.txt are
the shared manifests for both link sites; production hookup lands in M7)
- matrix stacks (MODELVIEW/PROJECTION, glGetFloatv readback), immediate
mode with all 8 GL1 primitive conversions, GL1-default state mirror
- full GL 1.5 Gouraud lighting uber-shader (eye-space light capture at
glLightfv time, color-material, two-side, COMBINE evaluator + alpha test
wired but inert until M3/M5)
- draw routing: FFP traffic identified by GL_VERTEX_ARRAY client state;
blit/2D-GAL draws pass through untouched
- display lists: correct glGenLists/glIsList existing-empty semantics;
recorder itself is M3 (recorded commands drop with a one-time warning)
Parity: 20/47 under the 0.02 floor, of which 12 verified genuinely
rendering (bg-gradient x2, bounding-box, half-open-cylinder, segment x2,
material x3, light x3 — eyeballed against baselines); the other 8 are
small-geometry scenarios whose missing GLU/display-list content sits under
the floor (become real in M3/M4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 11:37:51 +02:00
|
|
|
|
|
|
|
|
## Invariants that are easy to break (learned from the native goldens)
|
|
|
|
|
|
|
|
|
|
- **Display lists snapshot client arrays at record time.** The renderer sets
|
|
|
|
|
`gl*Pointer`, records `glDrawArrays` inside `glNewList`, then `delete[]`s
|
|
|
|
|
the arrays right after `glEndList` (`layer_triangles.cpp`) — a recorder
|
|
|
|
|
that stores pointers reads freed memory. Copy eagerly at record.
|
|
|
|
|
- **Lights live in eye space, captured at `glLightfv` time.** `init_lights()`
|
|
|
|
|
runs under an identity modelview → the directional lights are anchored to
|
|
|
|
|
the camera. Re-deriving light directions at draw time breaks every lit
|
|
|
|
|
scenario.
|
|
|
|
|
- **Do not "fix" mismatched normal counts.** `generate_middle_triangles`
|
|
|
|
|
rejecting countersink walls (normals ≠ vertices) is upstream KiCad behavior
|
|
|
|
|
the goldens document; tolerating it would *diverge* from native.
|
|
|
|
|
- **Lighting is per-vertex (Gouraud)** to match fixed-function output;
|
|
|
|
|
per-fragment lighting visibly mismatches speculars on coarse meshes.
|
fix(3d): blank render + lost position after 3D viewer close/reopen (gl1 context guard)
Closing the viewer destroys its wxGLCanvas's WebGL context; reopening mints a
new one. The gl1 shim cached GL names (FFP program, stream/scratch VBOs) in
never-reset statics behind `if (!handle)` guards — in the new context every
draw died with INVALID_OPERATION and the viewer showed only the clear color
("Reload time 0.031 s" is benign: warm model caches make the rebuild fast).
contextSync() (gl1_state.cpp) now detects the context change in programSync()
— the one choke point every shim draw crosses, and a path the 2D GAL never
reaches (a first attempt checking in the glBindTexture wrap saw the GAL's
context and thrash-rebuilt the program 23x per run) — and drops the cached
names for lazy rebuild in the new context. Context identity is a monotonic id
stamped on Emscripten's per-context record: the numeric
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE is recycled, so a destroy-then-create can
return the same number and a handle comparison detects nothing.
The lost-position half is a wxwidgets wasm fix (pointer bump: GetFromWindow
reports display 0; saved geometry used to carry display=(unsigned)-1, which
LoadWindowState treats as "display not found" and re-centres the frame).
TDD (red observed before each fix, green after):
- tests/kicad/3d-viewer-reopen.spec.ts (new, own worker like the deadlock
spec): load board, open viewer, render-gate, drag by the titlebar, close
via the x, reopen; asserts the board re-renders (was: 1 distinct colour for
90 s) and the window position is restored (was: re-centred to 0,0 after
closing at 40,90). Green run logs exactly one [gl1] context-change line.
- 3d-regression harness: recreateContext() destroys the context AND swaps in
a fresh canvas element (a browser canvas keeps its context for life, so
same-element recreation hands back the live old context and hides the bug);
the new 3d-webgl spec test renders redraw-mini-board-navigator before and
after recreation and requires pixel-identical output. Parity: 47/47, zero
drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 15:55:31 +02:00
|
|
|
- **GL object caches are per-context, and the context is mortal.** Closing the
|
|
|
|
|
3D viewer destroys its wxGLCanvas's WebGL context; reopening mints a new one
|
|
|
|
|
in which the cached names (FFP program, stream/scratch VBOs) are invalid —
|
|
|
|
|
every draw then dies with `INVALID_OPERATION` and the viewer is blank.
|
|
|
|
|
`contextSync()` (gl1_state.cpp) detects the change in `programSync()` — the
|
|
|
|
|
one choke point every shim draw crosses and a path the 2D GAL never reaches
|
|
|
|
|
(a check in any `__wrap_*` would see the GAL's context and ping-pong the
|
|
|
|
|
owner on 2D↔3D paint alternation) — and drops the caches so they rebuild
|
|
|
|
|
lazily. Identity comes from a monotonic id stamped on Emscripten's
|
|
|
|
|
per-context record — NOT the `EMSCRIPTEN_WEBGL_CONTEXT_HANDLE`, which
|
|
|
|
|
Emscripten recycles (a destroy-then-create can return the same number).
|
|
|
|
|
Display-list/immediate state is deliberately untouched: it is CPU-only, and
|
|
|
|
|
the change can be detected mid-scene-rebuild (even inside `glNewList`).
|
|
|
|
|
Known limit (pre-existing): two *simultaneously live* FFP contexts would
|
|
|
|
|
thrash the caches on every alternation — the shim still assumes one live
|
|
|
|
|
3D-viewer context at a time.
|
feat(3d): gl1 shim M0-M2 — FFP-on-WebGL2 substrate + lighting; 12 scenarios truly green
wasm/gl1: the GL1.x fixed-function emulation layer replacing the
gl_ffp_stub.c no-ops in the 3d-regression harness link. This batch:
- symbol split: 52 FFP-only entry points implemented; 10 Emscripten-owned
names intercepted via wasm-ld --wrap (sources.txt/wrapped_symbols.txt are
the shared manifests for both link sites; production hookup lands in M7)
- matrix stacks (MODELVIEW/PROJECTION, glGetFloatv readback), immediate
mode with all 8 GL1 primitive conversions, GL1-default state mirror
- full GL 1.5 Gouraud lighting uber-shader (eye-space light capture at
glLightfv time, color-material, two-side, COMBINE evaluator + alpha test
wired but inert until M3/M5)
- draw routing: FFP traffic identified by GL_VERTEX_ARRAY client state;
blit/2D-GAL draws pass through untouched
- display lists: correct glGenLists/glIsList existing-empty semantics;
recorder itself is M3 (recorded commands drop with a one-time warning)
Parity: 20/47 under the 0.02 floor, of which 12 verified genuinely
rendering (bg-gradient x2, bounding-box, half-open-cylinder, segment x2,
material x3, light x3 — eyeballed against baselines); the other 8 are
small-geometry scenarios whose missing GLU/display-list content sits under
the floor (become real in M3/M4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 11:37:51 +02:00
|
|
|
|
|
|
|
|
## Layout
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
sources.txt / wrapped_symbols.txt single source of truth for both link sites
|
|
|
|
|
include/gl1_shim.h internal API + the state mirror
|
|
|
|
|
src/gl1_entry_ffp.cpp the 52 FFP-only public entry points
|
|
|
|
|
src/gl1_entry_wrapped.cpp __wrap_* interceptors (mechanism-aware TU)
|
|
|
|
|
src/gl1_state.cpp state singleton, capability routing
|
|
|
|
|
src/gl1_matrix.cpp MODELVIEW/PROJECTION stacks (+readback)
|
|
|
|
|
src/gl1_immediate.cpp glBegin/glEnd + primitive conversion
|
|
|
|
|
src/gl1_dlist.cpp display-list recorder/replayer
|
|
|
|
|
src/gl1_draw.cpp draw execution (stream VBO, attrib setup)
|
|
|
|
|
src/gl1_shaders.cpp the FFP uber-program (ES 3.00) + uniforms
|
|
|
|
|
src/gl1_glu.cpp GLU quadrics (SGI tessellation) + gluPerspective
|
|
|
|
|
```
|