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>
This commit is contained in:
Istvan Matejcsok 2026-08-24 15:55:31 +02:00
commit 3722891d48
13 changed files with 504 additions and 20 deletions

View file

@ -55,6 +55,22 @@ untouched.
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.
- **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.
## Layout