fix(e2e): rescope the 3D fixes for the webgl-era viewer (rebased onto main)

The 3d-webgl merge (kicad eb13ff3bdc: the viewer now defaults to the real
OpenGL renderer via wasm/gl1, and occ-split moves STEP parsing into the
occ_service worker) made the raytracer-era orchestration on this branch moot —
main's chromium-ci phase is green at 15-way parallelism (28666407570 /
28698861536). Drop what no longer earns its complexity, keep the diagnostics,
fix main's live flake, and make the deadlock spec test what it was written for.

- REVERT the chromium-ci-3d serial project, the two-phase test:kicad:ci, the
  SwiftShader GPU-process flags, and the resize-drag/models skips: config and
  package.json are byte-for-byte back to main's shape. The raytracer contention
  they guarded is no longer on the CI path.

- FIX main's live flake: run 28698861536 is green only via retry
  (3d-viewer.spec:26 flaky) and 28666407570's deadlock red sampled an ALL-ZERO
  pixel signature — the viewer's first frame lags the canvas's creation on
  software WebGL under parallel load, and sampling too early reads an all-black
  backbuffer. New waitForThreeDRender() gates render assertions on actual
  pixels (1s-interval full-frame CPU reads) instead of fixed sleeps, used by
  3d-viewer.spec:26 and the models render tail.

- KEEP the storm-proofed samplers (one full-frame getImageData on a
  willReadFrequently canvas replacing 256 per-pixel GPU round-trips per sample
  — the "GPU stall due to ReadPixels" trigger) and the logThreeDDiag
  instrumentation: engine-independent, and they de-risk every remaining
  software-GL pixel read.

- models spec: bridge assertions stay front-loaded (the protocol regression
  signal is independent of the render); the occ_service parse verdict is now
  POLLED — it lands async relative to the bridge ensures, so asserting it
  immediately raced the worker; the render tail runs again everywhere. (The
  pre-webgl raytracer+models renderer-death documented in a17f3be does not
  affect the OpenGL default path — the raytracer-toggle+models combination
  remains untested product surface, tracked outside this branch.)

- deadlock spec: the deadlock it guards is raytracer-specific and the viewer
  now defaults to OpenGL — on the GL engine it either passes vacuously (fast
  renders make every liveness assertion trivial, 28698861536) or fails on the
  black first frame (28666407570). It now flips the engine via the "Use
  raytracing" toolbar toggle (loud assert if the toggle moved) and
  cross-checks engagement by requiring the canvas pixels to CHANGE after the
  flip with no input in between (the raytraced frame is lit differently; a GL
  re-render reproduces identical pixels; heap growth is unusable — mimalloc
  satisfies the raytracer from freed arena pages). That guard immediately
  caught a REAL defect: on the webgl-era wasm build the toggle is INERT (the
  click lands and "Reload time" updates, but the canvas never changes —
  suspects: DoRePaint's silent catch(runtime_error) freezing the canvas after
  a raytracer Redraw throw, or ToggleRaytracing writing m_boardAdapter.m_Cfg
  while RenderEngineChanged() reads GetAppSettings<…>(), possibly different
  instances in the merged bundle). The spec is therefore test.skip-annotated
  as a KNOWN ISSUE with the full engine-force machinery in place — unskipping
  it self-validates the product fix. The CI-skip also stays (raytracer
  liveness needs real-GPU pacing; the Worker-boot deadlock mechanism is
  covered on CI by the standalone wx harnesses).

- 180s viewer-open waits kept as pure CI headroom (never slow a passing run).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Istvan Matejcsok 2026-07-04 11:37:27 +02:00
commit 561d0500a6
6 changed files with 110 additions and 127 deletions

View file

@ -1,12 +1,18 @@
import { test, expect } from './fixtures';
import { clickByTooltip, clickToolbarTool } from '../e2e/utils/element-tracker';
import { waitForPcbnew } from './utils/pcbnew-ready';
import { countGlCanvases, loadBoard, logThreeDDiag, openThreeDViewer } from './utils/threed-viewer';
/**
* Regression for the camera-move-on-canvas raytrace DEADLOCK.
*
* The 3D viewer's EDA_3D_CANVAS is a wxGLCanvas whose paint runs the multi-threaded CPU
* raytracer. Moving the camera makes the raytracer spawn raw std::thread workers; with no
* Since the 3D-webgl port the wasm viewer DEFAULTS to the real OpenGL renderer, which never
* had this deadlock — so this spec explicitly flips the viewer to the raytracer engine (its
* toolbar toggle) before driving it; the raytracer remains user-reachable via that same
* toggle, so the regression it guards is still live product surface.
*
* The 3D viewer's EDA_3D_CANVAS is a wxGLCanvas whose paint (on the raytracer engine) runs
* the multi-threaded CPU raytracer. Moving the camera makes the raytracer spawn raw std::thread workers; with no
* PROXY_TO_PTHREAD its join runs on the browser main thread, and KiCad's own thread pool has
* already drained emscripten's pre-warmed Worker pool — so those threads fall back to on-demand
* `new Worker()`, whose boot handshake needs the main thread back in the JS event loop, which it
@ -44,6 +50,16 @@ test.describe('3D viewer camera-move deadlock', () => {
// on a real GPU.
test.skip(!!process.env.CI, 'raytracer liveness assertions require a real GPU; deadlock '
+ 'mechanism is covered on CI by the standalone coroutine-pthread-ondemand/raytrace-threads harnesses');
// KNOWN ISSUE (2026-07-04, webgl-era wasm build): the "Use raytracing" toolbar toggle is
// INERT — the click lands and a scene reload fires ("Reload time" status updates), but the
// canvas pixels never change (sampled for 20s; this spec's engine-engagement guard below
// caught it). Suspects: EDA_3D_CANVAS::DoRePaint's silent catch(runtime_error) freezing
// the canvas after a raytracer Redraw throw, or the engine toggle writing
// m_boardAdapter.m_Cfg while RenderEngineChanged() reads GetAppSettings<…>() — possibly
// different instances in the merged kicad_editor bundle. Unskip once the toggle works:
// the engagement guard below then validates the engine flip loudly.
test.skip(true, 'KNOWN ISSUE: the raytracer engine toggle is inert on the webgl-era wasm '
+ 'build — the deadlock mechanism cannot be driven until it works (see comment)');
// One 187 MB wasm runtime is already heavy; keep this serial and generous.
test.describe.configure({ mode: 'serial' });
test.setTimeout(240000);
@ -65,7 +81,7 @@ test.describe('3D viewer camera-move deadlock', () => {
}, winsBefore);
expect(winId, 'the 3D viewer should open a new top-level window').toBeTruthy();
// Let the INITIAL raytrace settle through the safe per-frame pump (Workers boot here).
// Let the INITIAL render settle through the safe per-frame pump (Workers boot here).
await page.waitForTimeout(5000);
await logThreeDDiag(page, 'deadlock: after open+settle');
@ -164,6 +180,33 @@ test.describe('3D viewer camera-move deadlock', () => {
`wasm main thread unresponsive after "${step}" → deadlock`).toBe(true);
};
// Since the 3D-webgl port (kicad eb13ff3bdc) the wasm viewer defaults to the REAL
// OpenGL renderer; the deadlock mechanism this spec guards is raytracer-specific, so
// flip the viewer to the raytracer via its toolbar toggle. Loud failure if the
// toggle moved — a silent no-op would leave the fast GL renderer making every
// liveness assertion below vacuously green.
const sigOnGl = (await sampleCanvas()).sig;
const toggled = (await clickToolbarTool(page, 'Use raytracing'))
|| (await clickByTooltip(page, 'Render current view using Raytracing'));
expect(toggled, 'the "Use raytracing" toolbar toggle must exist in the 3D viewer').toBe(true);
// Engine cross-check (guards the same false green): with NO input between the two
// samples, only an engine change repaints the canvas differently — the raytraced
// frame is lit/shadowed differently from the GL frame it replaces, a GL re-render
// reproduces the identical pixels, and a no-op leaves the canvas untouched. (Heap
// growth is NOT a usable signal here: mimalloc satisfies the raytracer's buffers
// from already-freed arena pages, so HEAPU8.length stays flat.)
let raytracerEngaged = false;
for (let i = 0; i < 20 && !raytracerEngaged; i++) {
await page.waitForTimeout(1000);
raytracerEngaged = (await sampleCanvas()).sig !== sigOnGl;
}
expect(raytracerEngaged,
'the canvas did not change after the engine toggle — the raytracer did not engage')
.toBe(true);
// Let the first full raytrace converge before taking the interaction baseline.
await settleRender(25000);
const before = await sampleCanvas();
console.log(`[TEST] 3D render before interaction: ${before.distinctColors} distinct colours`);