| Filename | Latest commit message | Latest commit date |
|---|---|---|
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>
|
||
| .. | ||
| baseline | ||
| baseline-webgl | ||
| native | ||
| scenarios | ||
| wasm | ||
| floors.json | ||
| manifest.json | ||
| README.md | ||
3D Renderer Regression Suite (OpenGL → WebGL port)
Screenshot-baseline TDD harness for porting KiCad's 3D viewer OpenGL renderer
(RENDER_3D_OPENGL, pure GL 1.x fixed-function) to WebGL2 — the same approach
used for the 2D GAL port (tests/gal-regression/), but compared with the CI
pixelmatch engine instead of ImageMagick.
Shared C++ scenarios call real KiCad 3D-viewer code (draw helpers, display lists, materials, camera, stencil hole-subtraction, VBO models…) and are compiled two ways:
- native/ — macOS app on real desktop OpenGL (2.1 compatibility context via
the vendored glad loader). Renders each scenario into a fixed 800×600
offscreen FBO (
GL_RGBA8+GL_DEPTH24_STENCIL8, theEDA_3D_CANVAS::RenderToFrameBufferrecipe) → golden baselines. - wasm/ — the same scenario TUs compiled with em++, linked against the
wasm/gl1GL1→WebGL2 emulation layer (the port itself — seewasm/gl1/README.md). Since the port landed, all 47 scenarios render under the parity floor with zero per-scenario overrides.
Directory layout
scenarios/ shared scenario sources (native + wasm) — the registry in
scene3d_test_scenarios.cpp is the single source of truth
native/ golden generator (CMake; homebrew wxWidgets + OpenGL.framework)
wasm/ WebGL harness (planned)
baseline/ committed native goldens: 3d-<name>.png
baseline-webgl/ committed browser renders (port era, CI-promoted)
output/ run output + diffs (gitignored)
manifest.json {width,height,scenarios[]} written by the native harness;
committed — the anti-drift anchor for specs and orchestrator
floors.json pixelmatch verdict floors per comparison level
Running
./scripts/test-3d-regression.sh # build + render + gate (native; webgl when present)
./scripts/test-3d-regression.sh native # native phase only
./scripts/test-3d-regression.sh compare # comparisons only
./scripts/test-3d-regression.sh promote # promote output/native -> baseline/ (byte-diff guarded)
Logs land in logs/test-3d-regression/ (build logs in tests/logs/).
Comparison levels
Engine: tests/tools/screenshots/compare-dirs.ts (pixelmatch
{threshold: 0.1, includeAA: false} — AA edge pixels ignored), floors from
floors.json. Diff triptychs/heatmaps + report.json land in
output/diff/<level>/.
| Level | Pair | Floor (changedRatio) | Role |
|---|---|---|---|
native-self |
baseline/ vs output/native/ |
0.001 (measured noise: exactly 0 on Apple M5) | gating regression check on the dev Mac |
webgl-vs-native |
baseline/ vs output/webgl/ |
0.02, report-only | the TDD port-progress meter (npm run 3d:check:parity) |
webgl-self |
baseline-webgl/ vs output/webgl/ |
0.005 | browser regression anchor (once the port renders) |
npm scripts (from tests/): 3d:check, 3d:check:webgl, 3d:check:parity,
3d:compare (generic dir pair), 3d:test:webgl.
Updating baselines
Baselines are generated on the dev Mac (CI has no native GL — same model as the GAL suite). After an intentional render change:
./scripts/test-3d-regression.sh native(fails with triptychs inoutput/diff/native-self/— eyeball them),./scripts/test-3d-regression.sh promote(byte-diff-guarded copy, zero churn) and commitbaseline/+manifest.json.
The orchestrator cmps the freshly-written manifest against the committed one
every run, so a scenario registry change can't silently drift past the
baselines. Scenario names are append-only — never rename or renumber (they are
the PNG names and the WebGL test IDs).
Port status (was: TDD red state)
The suite drove the port TDD-style: the harness started against no-op FFP
stubs (~100% changed everywhere), and the wasm/gl1 emulation layer ground
the parity report down milestone by milestone (substrate → lighting →
display lists/arrays → GLU quadrics). Current state: 47/47 under the 0.02
parity floor, no floors.json overrides needed.
Two caveats the parity floor cannot see (verified by eyeballing renders):
- Small-geometry scenarios (the GLU arrows, the spheres gizmo, sparse grids) fit under the floor even when absent — a regression there needs the triptych/eyeball check, not just the report.
glLineWidth > 1is clamped to 1 by browsers (gizmo axes, native width 2–4 lines render thinner); the affected coverage is small enough to stay under the floor.
Scenario tiers (47 scenarios)
- Tier 1 (30) — standalone TUs:
opengl_utils(arrows/segments/bbox/half-cylinder),ogl_utils(background gradient, materials, textures),TRIANGLE_DISPLAY_LIST/OPENGL_RENDER_LIST(display lists, seg-ends alpha-test texture,DrawCulledstencil subtraction, z-transform, transparency),MODEL_3D(VBO/IBO, material modes, bboxes),SPHERES_GIZMO, camera (perspective/ortho/preset views, isolated lights). - Tier 2 (14) —
RENDER_3D_OPENGLprivate generators (generateCylinder/Disk/Dimple/InvCone, all fiveaddObjectTrianglesoverloads,appendPostMachiningGeometry, via composite, the four grid densities,setupMaterials/setLayerMaterial/setArrowMaterial,createBoard) via the rob-template accessor (native/render3d_test_accessor.*) over the syntheticBOARD_ADAPTER(native/board_adapter_test_impl.cpp— itsInitSettingsis the test-data seam). - Tier 3 (3) — full
reload()+Redraw()composites over the synthetic mini-board:redraw-empty,redraw-mini-board(copper/silk/mask/stencil holes),redraw-mini-board-navigator(grid + gizmo — the port-complete gate).
Known upstream bug (documented by 3d-post-machining.png)
appendPostMachiningGeometry's COUNTERSINK path adds middle-contour quads
with AddQuad but never calls AddNormal, so the normals array ends up half
the vertex count and OPENGL_RENDER_LIST::generate_middle_triangles rejects
the whole middle list — a countersunk hole silently erases the walls of any
geometry batched into the same TRIANGLE_DISPLAY_LIST (the real viewer has
the same defect). The scenario keeps counterbore and countersink in separate
lists so the counterbore renders correctly while the countersink half records
the buggy (empty) upstream output.
Lighting semantics worth knowing: init_lights() runs once at context init
under the identity modelview, so the two directional lights are anchored in
eye space (they follow the camera) — the harness replicates that
(SCENE3D_CTX::InitOnce), and only the headlight is repositioned per frame
like Redraw() does. Also avoid toggling GL_LIGHTx between draws inside one
frame: the Apple GL driver drops the first draw after a mid-frame toggle (the
real renderer never does this; the isolated-light scenarios use one light per
frame instead).