2026-07-03 09:26:43 +02:00
|
|
|
|
# 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`, the
|
|
|
|
|
|
`EDA_3D_CANVAS::RenderToFrameBuffer` recipe) → **golden baselines**.
|
feat(3d): gl1 shim M4+M6+M7 — GLU quadrics, production link, stub retired; 47/47 parity
- gl1_glu: gluCylinder/gluDisk/gluSphere per the SGI GLU reference
tessellation (sin/cos phase, strip emission order, pole fans — Apple's
GLU derives from the same source, so vertices match the goldens);
emission goes through the public glBegin/glVertex entry points so
quadrics record into display lists
- glLineWidth>1 one-time warning (browsers clamp to 1; affected coverage
stays under the parity floor — zero floors.json overrides needed)
- production link site: build-kicad-target.sh now compiles wasm/gl1 from
sources.txt and applies wrapped_symbols.txt --wrap flags (same manifests
as the test harness); kicad_editor's RENDER_3D_OPENGL links against the
real emulation layer instead of no-ops
- wasm/stubs/gl_ffp_stub.c deleted (role fully superseded; history in git)
- docs: 3d-regression README port-status section (floor-blind small
geometry + line-width caveats), harness comments de-red-stated
Parity: 47/47 under the 0.02 floor, zero overrides. Native golden path
byte-identical to bb0e238 (whose gate ran green) — scenarios/, native/,
baseline/, manifest, floors, spec all untouched by the port.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 11:56:19 +02:00
|
|
|
|
- **wasm/** — the same scenario TUs compiled with em++, linked against the
|
|
|
|
|
|
**`wasm/gl1`** GL1→WebGL2 emulation layer (the port itself — see
|
|
|
|
|
|
`wasm/gl1/README.md`). Since the port landed, all 47 scenarios render under
|
|
|
|
|
|
the parity floor with zero per-scenario overrides.
|
2026-07-03 09:26:43 +02:00
|
|
|
|
|
|
|
|
|
|
## 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:
|
|
|
|
|
|
|
|
|
|
|
|
1. `./scripts/test-3d-regression.sh native` (fails with triptychs in
|
|
|
|
|
|
`output/diff/native-self/` — eyeball them),
|
|
|
|
|
|
2. `./scripts/test-3d-regression.sh promote` (byte-diff-guarded copy, zero
|
|
|
|
|
|
churn) and commit `baseline/` + `manifest.json`.
|
|
|
|
|
|
|
|
|
|
|
|
The orchestrator `cmp`s 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).
|
|
|
|
|
|
|
feat(3d): gl1 shim M4+M6+M7 — GLU quadrics, production link, stub retired; 47/47 parity
- gl1_glu: gluCylinder/gluDisk/gluSphere per the SGI GLU reference
tessellation (sin/cos phase, strip emission order, pole fans — Apple's
GLU derives from the same source, so vertices match the goldens);
emission goes through the public glBegin/glVertex entry points so
quadrics record into display lists
- glLineWidth>1 one-time warning (browsers clamp to 1; affected coverage
stays under the parity floor — zero floors.json overrides needed)
- production link site: build-kicad-target.sh now compiles wasm/gl1 from
sources.txt and applies wrapped_symbols.txt --wrap flags (same manifests
as the test harness); kicad_editor's RENDER_3D_OPENGL links against the
real emulation layer instead of no-ops
- wasm/stubs/gl_ffp_stub.c deleted (role fully superseded; history in git)
- docs: 3d-regression README port-status section (floor-blind small
geometry + line-width caveats), harness comments de-red-stated
Parity: 47/47 under the 0.02 floor, zero overrides. Native golden path
byte-identical to bb0e238 (whose gate ran green) — scenarios/, native/,
baseline/, manifest, floors, spec all untouched by the port.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 11:56:19 +02:00
|
|
|
|
## Port status (was: TDD red state)
|
2026-07-03 09:26:43 +02:00
|
|
|
|
|
feat(3d): gl1 shim M4+M6+M7 — GLU quadrics, production link, stub retired; 47/47 parity
- gl1_glu: gluCylinder/gluDisk/gluSphere per the SGI GLU reference
tessellation (sin/cos phase, strip emission order, pole fans — Apple's
GLU derives from the same source, so vertices match the goldens);
emission goes through the public glBegin/glVertex entry points so
quadrics record into display lists
- glLineWidth>1 one-time warning (browsers clamp to 1; affected coverage
stays under the parity floor — zero floors.json overrides needed)
- production link site: build-kicad-target.sh now compiles wasm/gl1 from
sources.txt and applies wrapped_symbols.txt --wrap flags (same manifests
as the test harness); kicad_editor's RENDER_3D_OPENGL links against the
real emulation layer instead of no-ops
- wasm/stubs/gl_ffp_stub.c deleted (role fully superseded; history in git)
- docs: 3d-regression README port-status section (floor-blind small
geometry + line-width caveats), harness comments de-red-stated
Parity: 47/47 under the 0.02 floor, zero overrides. Native golden path
byte-identical to bb0e238 (whose gate ran green) — scenarios/, native/,
baseline/, manifest, floors, spec all untouched by the port.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 11:56:19 +02:00
|
|
|
|
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 > 1` is 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.
|
2026-07-03 09:26:43 +02:00
|
|
|
|
|
|
|
|
|
|
## 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, `DrawCulled` stencil 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_OPENGL` private generators
|
|
|
|
|
|
(`generateCylinder/Disk/Dimple/InvCone`, all five `addObjectTriangles`
|
|
|
|
|
|
overloads, `appendPostMachiningGeometry`, via composite, the four grid
|
|
|
|
|
|
densities, `setupMaterials`/`setLayerMaterial`/`setArrowMaterial`,
|
|
|
|
|
|
`createBoard`) via the rob-template accessor
|
|
|
|
|
|
(`native/render3d_test_accessor.*`) over the synthetic `BOARD_ADAPTER`
|
|
|
|
|
|
(`native/board_adapter_test_impl.cpp` — its `InitSettings` is 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).
|