Miss 08A: the binding stores wire-carried (lib_symbols …) definitions in
kdoc_libsymbols and prefixes them on apply wires — a joiner that never saw a
symbol adopts it WITH its definition (new e2e ysync-libsymbols.spec.ts).
Miss 08B: registerSaveHook gains onSavedText; WasmTool routes saved-file text
to syncLayoutToY (per sheet room via the manager's syncLayoutFromSave, or the
single-room doc) so title block / paper / setup edits converge instead of
drifting. Opt 12 (TS half): zod off the observer hot path (yToItemUnchecked),
children index built once per conversion. Opt 13: seed()'s adopt diffs the
editor snapshot against the doc view and applies only the doc-authoritative
difference — clean rebinds apply nothing, the adopt undo entry shrinks to the
real changed set. Opt 14 deliberately deferred (doc 18). All TS-side; no wasm
rebuild (the C++ blob/findLib sides already carried definitions).
Verified: shared 107, standalone 79 (+2 known pre-existing wasm-assets),
ysync e2e 21/21 chromium, collab regression 21/3-skip firefox.
Bumps: web/pcbjam-shared (lib_symbols channel + syncLayoutToY + opts).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
The 2026-07-02 sync review (docs/features/ysync-review, ysync-review branch)
found 7 bugs and that the two-tab e2e only exercised the DEAD legacy scalar
wire. This lands plan doc 15 in full; results + empirical findings in doc 16.
- tests/collab/browser-entry-v2.ts (+build.mjs): the PRODUCTION v2 stack
bundled for e2e (connectKicadDoc + attachKicadCollab, kdoc_* keys), with
in-page renderActiveDoc/singleSeedRender/driftReport helpers and yjs forced
to ONE copy (the two web pnpm workspaces otherwise bundle two
instanceof-incompatible instances).
- tests/kicad/ysync-two-tab.spec.ts: pl_editor green baseline (A↔B edits,
ITEM-level drift silence) + divergent-uuid adopt; bug-01 pcb/ee fresh-room
repros (Chromium-only: two kicad_editor tabs exceed Firefox's per-process
wasm budget); bug-06 concurrent-seed race; bug-03 Y-half.
- tests/kicad/ysync-repros-{pcbnew,eeschema}.spec.ts: bugs 02/03/05 + the
bug-04 matrix (anchor-centred fp rotation, pad resize, endpoint drag,
symbol rotation, Value-field edit), each with green landed-preconditions;
the "local move emits" controls double as headless-emit probes — GREEN on
both tools, so every emit-dependent repro is a live test.fail.
- wasm/bindings: 7 local-edit test hooks via real commits
(CallAfter+COROUTINE) — TestRemoveItem/TestRotateItem (both tools,
dispatched in the merged image), TestSetPadSize/TestMoveEndpoint (pcbnew),
TestSetFieldText (eeschema).
- web/standalone ysync-repros.test.ts: bug-01 units (C++-faithful fake gating
emit on ensureBridge) + bug-07a/b (stale DOWN hook, real sheet-manager gap).
- web/pcbjam-shared bump: bug-03/06 unit repros.
Convention: every repro asserts the CORRECT behavior and is expected-fail
(test.fail/it.fails) naming its bug doc; a fix flips it to "unexpected pass",
forcing marker removal — the repro becomes the regression test. Every
expected failure verified (JSON reporter) to fail at its documented assert.
Suite state: 39 passed / 0 failed / 0 flaky / 5 skipped (2 firefox guards,
2 pre-existing legacy two-tab skips, 1 pre-existing roundtrip fixme).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
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>
The headed Mesa-llvmpipe switch (ce44636) was built on a misdiagnosis. With
--use-gl=angle --use-angle=gl under Xvfb, Chromium gets NO WebGL AT ALL on the
GPU-less CI VM: runs 28652367347 + 28664038296 both log
`glcanvas count before opening 3D viewer: 0` (even the pcbnew board's own GAL
canvas fails to create) and every failure is the FIRST viewer-open wait timing
out. "Timeouts instead of crashes" meant no GL context existed to crash, not
that llvmpipe was stable-but-slow. The Mesa/GLX path works for the Firefox
project, but not for Chromium on this image.
Headless SwiftShader is the environment where GL provably works — the
serialized run 28649537489 opened the viewer and rendered/sampled 56 colours
(viewer:26/130/209 all passed single-tab). Its remaining failures split into
two families, each addressed on its own evidence:
1) SwiftShader GPU-process kills (deadlock spec's lost canvas/black frame):
- chromium-ci-3d back to headless + --enable-unsafe-swiftshader, plus
--disable-gpu-watchdog and --disable-gpu-process-crash-limit: heavy churn
stalls the software-GL GPU process until the watchdog shoots it
(CONTEXT_LOST -> removed GL canvas -> "Target crashed"); with the watchdog
off a slow op completes, and without the crash limit a lost GPU process
keeps restarting for later tests. Same args locally (no-ops on a healthy
real GPU).
- Storm-proof every canvas sampler (3d-viewer, models, deadlock specs): one
full-frame getImageData on a willReadFrequently (CPU-backed) 2D canvas +
JS grid sampling, replacing 256 per-pixel getImageData GPU round-trips per
sample — the "GPU stall due to ReadPixels" trigger, worst in the deadlock
spec's 1.5s-interval settle polling. Sampled pixels and sig order are
unchanged.
- edge-resize spec: keep the open + 5-resize-handles assertions on CI, then
test.skip the drag — each resize step re-raytraces synchronously on the
wasm main thread and the 12-step drag blocked mouse.move past the 240s
budget even single-tab. Real-GPU-paced; still runs locally (deadlock
spec's CI-skip from d9900fe unchanged, same rationale).
2) KNOWN PRODUCT BUG uncovered in the models spec (NOT SwiftShader, NOT CI):
raytracing a scene WITH component models kills the Chromium renderer
process outright ~6s after scene build — silent process death with a FLAT
wasm heap (531-637 MB watched at 2s intervals; not OOM), no console error,
no wasm abort, no crash report. Deterministic on a real GPU (5/5),
fixture-independent (700 KB USB-C and 61 KB USON-8 STEP crash identically);
the same death hits CI SwiftShader at ~66s. Board-only raytraces complete
and stay alive, so the bug is specific to the model path. The spec's one
historical green ended at raytrace-age ~6s — inside the death window by
luck; every CI run of it ever died. Fix here: front-load the bridge
assertions (ensure requests fire during scene BUILD, before the raytrace),
so protocol regressions still fail the suite everywhere, and test.skip the
render tail unconditionally with the bug documented at the skip site. The
raytrace->blit pipeline stays gated on CI by 3d-viewer.spec's board-only
render. Bug tracked for a dedicated fix session.
Correct the now-wrong llvmpipe comments (config, open-wait rationale, deadlock
skip reason). Expected CI matrix: viewer open/render/stacking/titlebar +
models-bridge on SwiftShader serial; edge-resize drag + camera-move deadlock
on real GPUs locally; the Worker-boot deadlock mechanism stays covered on CI
by the standalone wx harnesses.
Locally validated on a real GPU (--project=chromium): 5 passed + 1 skipped
(the documented models render tail).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the headed-llvmpipe switch. That run (28652367347) stopped CRASHING —
every failure became a plain timeout, confirming llvmpipe gives a stable WebGL context
(no more CONTEXT_LOST / "Target crashed") but software raytracing is just slow:
- The first board raytrace takes ~60s on llvmpipe, right at openThreeDViewer's 60s
wait → raise it to 180s (shared helper + the models spec's inline copy). Real GPU
returns in ~2s, so it is only CI headroom.
- The deadlock spec asserts the wasm main thread stays responsive within ~15s DURING a
raytrace — a premise only true on a fast (real) GPU. Under slow software rendering a
legitimately-slow raytrace is indistinguishable from a deadlock, so skip it on CI
(test.skip on process.env.CI). The Worker-boot deadlock MECHANISM stays covered on CI
by the standalone coroutine-pthread-ondemand / raytrace-threads wx harnesses; the spec
still runs locally on a real GPU.
- Gate the headed + llvmpipe launch flags on CI so chromium-ci-3d behaves like a normal
bundled-Chromium project locally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The three heavy 3D-viewer specs (3d-viewer, 3d-viewer-deadlock, 3d-viewer-models)
crashed on CI (run 28604015154): "Target crashed", "browser has been closed", and a
black frozen canvas. Each boots the 3D-enabled pcbnew build, which pre-warms
~hardwareConcurrency*2+8 (~68 on the 30-core VM) Web Workers AND runs a multi-threaded
CPU raytracer over SwiftShader software-WebGL. Under fullyParallel the three spec FILES
run in separate browser processes concurrently, and the simultaneous raytrace tabs
exhaust the shared GPU process's ~16 live-WebGL-context limit / renderer memory. The
pool pre-warm (the deadlock fix, 7630c7e) is correct and present in the tested binary;
per-file isolation prevented in-process Worker accumulation but NOT this cross-process
concurrency — the gap these specs' first CI exposure hit.
Fix (test-orchestration only; no wasm/submodule changes):
- Route the three specs to a dedicated `chromium-ci-3d` Playwright project (own
outputDir=pw-artifacts/kicad-3d so its failure traces survive the later perf run's
wipe; the existing tests/pw-artifacts/** upload already captures it).
- test:kicad:ci runs it as a SECOND `playwright test` pass with --workers=1, so at most
one raytrace tab is alive at a time (the environment in which a single 3D load is
reliably green — docs/features/3d-raytracer/README.md §7). An exit-code-aggregating
sequencer runs both phases and fails if either fails (no silent skip of the 3D pass).
- Cap phase-1 CI workers to 12 as headroom under the WebGL-context cap.
- Log a resource-diagnostic snapshot (hardwareConcurrency, pthread pool, GL-canvas
count, wasm/JS heap) before each 3D interaction so any recurrence is attributable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move OpenCASCADE out of the merged editor image into occ_service: a separate
emscripten module (-sASYNCIFY=0, MODULARIZE, in-container -Oz finalize, 2N+8
pre-warmed pthread pool) booted lazily in a dedicated Web Worker on the first
STEP export or STEP/IGES model parse. kicad_editor.wasm ~190 MB -> 130 MB;
sessions that never touch OCC never fetch its 57 MB. STEP export works in the
browser for the first time: the unchanged desktop dialog runs EXPORTER_STEP,
whose wasm shadow suspends into globalThis.occService and the export bytes go
straight to a browser download (never entering the editor heap). STEP/IGES 3D
models parse in the worker via the oce shadow (S3D WriteCache/ReadCache wire).
- wasm/occ-service/: service CMake target (hooked from the kicad fork's
top-level CMakeLists, wasm/editor pattern), embind entry
(occExport/occLoadModel), wxConfig pre-js.
- wasm/stubs/{exporter_step,oce_plugin}_stub.cpp: EM_ASYNC_JS worker bridges
(callee-shadowing; no caller #ifdefs).
- web/standalone: provider installed whenever the kicad_editor bundle boots
(cross-face safe); ONE shared worker-boot source occ-worker.js (vite ?raw;
the e2e stub reads the same file) — blob worker with locateFile absolutized
against the glue URL; export download-name guard.
- deps: OCC builds with RapidJSON so its glTF/GLB writer exists — pinned to
the vcpkg master snapshot 2025-02-26 (24b5e7a8b27f), the same code official
KiCad consumes via vcpkg.json's opencascade[rapidjson]; rapidjson's latest
tag (v1.1.0, 2016) is ill-formed under modern clang.
- tests: occ-export dialog e2e (lazy-fetch boundary + STEP download bytes),
occ-probe incl. a 9-format matrix (step/stpz/brep/xao/ply/stl/glb/u3d/pdf),
3d-viewer-models hard-asserts the worker parse; occ provider stub installed
ambiently by the kicad fixtures.
Validated against desktop kicad-cli 10.0.4: geometric exact equality (bbox
delta 0 um, volume delta 0.0000%) for STEP/GLB/STL/BREP/STPZ across three
boards and option sweeps — with desktop OCC 7.9 vs wasm OCC 7.8; PLY/XAO/PDF
structurally equal; U3D same-size (quantizer float LSBs differ). Full kicad
e2e green on Firefox and Chromium; standalone verified end to end (lazy fetch
only on the Export click; export.step 60,628 B ISO-10303-21; loadModel 700 KB
STEP -> 569 KB scenegraph cache).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All four editors (PCB / Footprint / Schematic / Symbol) are now runtime --frame
choices of a single kicad_editor.wasm (178 MB at -O1 vs 147+82 separate; shared
wx/common/boost linked once). One editor per page load, as before; frames pcb /
fpedit / sch / symedit.
- wasm/editor/: the merged executable target (single_top + both kiface library sets,
whole-archive pcbcommon) + the safety-net focus-walk Kiface() dispatch TU. Gated by
KICAD_WASM_MERGED_EDITOR (kicad submodule bump carries the fork side: per-engine
Kiface/getter binding + ODR renames + dual-kiface launcher).
- wasm/bindings/: per-editor collab entries renamed pcbCollab*/schCollab* (JS names
unchanged); duplicate kicadOpenFile/kicadCollabOnSave + shared-name registrations
guarded behind KICAD_MERGED_EMBIND; new kicad_editor_embind.cpp registers each
shared JS name once, dispatching on the live frame.
- Build: kicad_editor app (build wrapper, target case arms, 3-object embind compile
with the ABI-critical flags, STUB_APP=pcbnew); docker/build.sh "all" =
kicad_editor calculator pl_editor gerbview (pcbnew/eeschema stay as explicit debug
apps); scripts/kicad/audit-merged-symbols.sh = repeatable ODR-collision audit (run
on kicad bumps).
- Frontend: Bundle type (bundle ≠ tool); TOOL_BUNDLE maps all four editors to
kicad_editor; explicit --frame tokens for pcbnew (pcb) and eeschema (sch); publish
list = the 4 real bundles.
- Tests/CI: five harnesses load kicad_editor.js with explicit frame tokens;
PCBNEW_FAMILY_SPECS renamed BIG_MODULE_SPECS + the 8 eeschema-family specs (they
now boot the merged module — SpiderMonkey x86 CI OOM routing); frame-runtime spec
covers all four frames from the one bundle.
Validated so far: frame-runtime 4/4 (each frame boots with the right title, no
aborts, no duplicate embind registration); 24-spec merged-module regression green;
3D raytracer renders. Known pre-existing failure: 3d-viewer title-bar drag deadlock,
fixed on main by 7630c7e (2N+8 pthread pre-warm) — picked up by the follow-up rebase.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The footprint and symbol editors are no longer separate WASM bundles: the frontend loads the parent pcbnew/eeschema bundle and passes --frame=fpedit / --frame=symedit (TOOL_BUNDLE + TOOL_FRAME -> Module.arguments in boot). Drops the two duplicate build+deploy targets and their wrapper scripts + vestigial embind; adds low-level harnesses (footprint_editor.html, symbol_editor.html) and a runtime-frame spec. Bumps the kicad submodule to the runtime --frame launcher.
The frame-runtime spec is listed in PCBNEW_FAMILY_SPECS so CI routes it to the chromium-ci (V8) project — its footprint case boots the pcbnew module, which OOMs SpiderMonkey on x86 CI. Includes the editor-unification dossier (research docs 01-04 + the as-built implementation record 05).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New tooling in tests/tools/screenshots/ (TypeScript via tsx):
- compare.ts: one pixelmatch engine (AA-excluded), connected-component
"where to look" boxes, old|new+boxes|heatmap triptych, per-engine floors.
- promote.ts: churn-free updater — overwrite a baseline only when decoded
pixels differ beyond the floor, copying CI bytes verbatim (no re-encode
churn); pulls a CI run via `gh run download` or a local --from dir.
- post-discord.ts: always-on CI-on-main report (SHA + e2e status + the
track-only runtime-perf table), then screenshot triptychs, batched +
size-capped + flood-collapsed + 429-aware.
- perf-report.ts: perf table with Δ vs the previous main run (via gh).
- changelog.ts: no-build git-history baseline differ (Discord trigger B).
- noise.ts / gen-manifest.ts: calibration + manifest generation.
CI wiring:
- wasm-build.yml: post-test step runs the gate + report on the already-
produced test-results (no extra build); report-only (continue-on-error),
posts only on push to main, inert without DISCORD_WEBHOOK_URL.
- ci-ubicloud.yml: secrets: inherit (pass the webhook through).
- screenshot-changelog.yml: ~30s no-build changelog on baseline changes.
screenshot-manifest.json: canonical 354-name set + best-effort engine tags
(313 chromium-swiftshader / 41 firefox-llvmpipe).
Normalize scale:'device'->'css' across 18 spec files (no-op at CI DSF=1)
so committed baselines are uniformly css-scaled.
Design: CI's Linux render is the single source of truth; no pinned
container (accept rare env drift -> re-promote); dev commits via promote.
Replaces the byte-cmp compare-screenshots.sh + file-size-proxy
update-baseline-screenshots.sh (kept for now until the first re-baseline).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Measures the current build's cold load, open+render, and pan/zoom FPS
(1x/4x/6x CPU-throttle sweep) for both editors, writing
tests/test-results/perf-*.json. Track-only: asserts only that the app booted
and the document opened — no perf thresholds, so it never gates CI.
Runs on a new Chromium `perf` Playwright project (CDP throttling; pcbnew needs
V8 anyway) via `npm run test:perf`, and is wired into wasm-build.yml as a
continue-on-error step so the numbers are captured/uploaded without flaking the
gate. Reuses the existing fixtures, ready-signal, fs-inject, and board-ready
helpers; FPS counter is cancel-able so a throttle sweep on one page doesn't
accumulate rAF loops.
.gitignore: ignore /benchmark-builds/ (disposable prebuilt-WASM + standalone
harness bundle used to reproduce the native-vs-JS-EH comparison locally).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the legacy Emscripten JS-exceptions model with native wasm-EH (legacy
encoding) across the whole build, keeping Asyncify coroutines working via a
from-source Binaryen --hoist-cpp-catches pre-pass. Net result: native-EH is the
only build mode, the 3D viewer is on by default, and pcbnew shrinks substantially.
Highlights:
- Binaryen submodule everywhere + --hoist-cpp-catches integration in apply-asyncify;
post-link Asyncify covers every app wasm (not just standalone test wasm).
- Build deps (incl. OpenCASCADE without OCC_CONVERT_SIGNALS) and all KiCad apps
with -fwasm-exceptions; emscripten_sleep added to the post-link asyncify-imports.
- libcontext fiber entry wired under native exceptions; while-loop main loop +
currData shim injected into all wx apps.
- Native-EH collab apply fixed: DEBUG-define the embind TU + match all out-of-CMake
C++ TUs' ABI flags to the core, fixing the vtable-layout skew / mis-dispatch.
- 3D viewer enabled by default (real raytracer linked, not the stub).
- Retire the EH-spike scaffolding; flip the asyncify-races ablation pins to
shim-redundancy pins (native-EH stays clean with the legacy shims ablated).
- Fix the asyncify-races quiescence check to not require Asyncify.currData==0:
under the native-EH per-frame-yield top loop the main stack is asyncify-suspended
every frame, so currData legitimately churns (a freed-but-not-yet-nulled buffer,
not a leak). Refresh the pcbnew toolbar screenshot baseline for the new kicad.
- CI: drop the obsolete binaryen_version input/env (the build uses the binaryen
submodule fork's wasm-opt, not a version download); key the wasm-output cache on
the binaryen submodule SHA instead.
Bumps the wxwidgets + binaryen submodules to their squashed feature commits.
Validated green: all 7 apps native-EH (real 3D in pcbnew); KiCad e2e 63/63
Firefox + Chromium (3D viewer renders); wx 336; coroutine 34/34 both engines;
asyncify 7/7 both engines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The crosshair toolbar group-cycle (ACTION_TOOLBAR::onToolEvent) was
expected-to-fail because the kicad submodule was a pre-10.0 (9.99.0)
snapshot that lacked it. The KiCad 10.0.4 rebase on this branch restores
the behavior, so the test now passes and Playwright flagged it as
"Expected to fail, but passed". Remove the test.fail annotation as the
test's own comment instructed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds KiCad's 3D viewer for the browser (opt in with BUILD_3D_VIEWER=ON) and
wires up the legacy-GL emulation it needs.
- build-kicad-target.sh / docker/build.sh: BUILD_3D_VIEWER →
-DKICAD_BUILD_3D_VIEWER_WASM=ON + -sLEGACY_GL_EMULATION + the GL js-library.
- wasm/shims/gl_immediate_shim.js: display-list emulation (record/replay board
layers), fixed-function + GLU stubs, throw-guards for unsupported pnames, and
per-context GLImmediate init for the viewer's SECOND WebGL context.
- tests/kicad/3d-viewer.spec.ts + utils/pcbnew-ready.ts: open View → 3D Viewer.
- bumps the kicad submodule (EMSCRIPTEN-guarded GL changes).
WIP: the viewer window + UI render and geometry draws (glError=0x0), but the
board is not yet visibly rendered — GLImmediate's FFP shader program isn't
linked on the viewer's 2nd WebGL context. Full status + next steps in
features/feat/add-3d-view/notes.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Standalone wx test apps (contextmenu, scrollbar) + e2e specs proving the
wxScrollBar control, the wxScrolledWindow gutter, and DoPopupMenu.
- KiCad in-app specs: pl_editor + pcbnew canvas right-click context menus and
scrolled-panel scrollbar gutters, with baseline screenshots.
- build-wx-wasm.sh: serial -j1 fallback when the parallel build trips the
intermittent clean-build race (a generated header read mid-rewrite by
concurrent compiles; never happens at -j1). Recovery resumes from built
objects; a genuine error still fails the serial pass.
- Bump wxwidgets submodule (context menus + draggable scrollbars).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The canvas (wxUniversal) mode is gone (wxwidgets submodule); remove every
piece of side-by-side plumbing so there is exactly one build and one test
flow:
- scripts/build-wxuniversal-wasm.sh -> scripts/build-wx-wasm.sh; no
--dom/--enable-universal; builds into build-wasm/wxwidgets
- build-wasm-test.sh: no DOM_BUILD / apps-dom rsync mirror / PORT=dom;
apps build straight into tests/apps (Makefile.wasm PORT conditionals
collapsed; wx.js + wx-dom.js always pre-js)
- docker/build.sh, build-kicad-target.sh, env.sh: WX_PORT / -dom /
-universal suffixes removed; kicad builds to kicad-<app>, outputs to
output/; wx.js/wx-dom.js copied from the real source path
(/workspace/wxwidgets/build/wasm — the old build-wasm path never
existed and silently failed)
- setup-kicad-wasm.sh: single target dir; the perl wx-dom.js injection is
gone — the 7 checked-in kicad pages now reference wx-dom.js directly
- playwright configs serve apps/; fixtures drop the test-results/dom and
logs/wxwidgets/dom namespacing; boot.spec asserts wxDomPort
unconditionally; pcbnew.spec uses one reference image;
appearance.spec assertions unconditional
- compare/update-baseline-screenshots.sh: --port removed
- tests/gal-regression/wasm/Makefile: links build-wasm/wxwidgets and
carries wx-dom.js as a second pre-js — the gal-webgl suite (30 specs)
now actually builds and runs here (it needed host-side boost+glm via
scripts/deps; the bundle had been missing, timing the whole spec out)
- tests: clickCanvas() dispatches via page.mouse (DOM widgets
legitimately cover the canvas; locator actionability refused the
click); the comprehensive spec drives wxChoice through its native
<select> (browser-owned popup cannot be coordinate-clicked)
- docs: README/CLAUDE.md/build.md script names and dirs;
features/wx-dom-port README reframed (DOM is THE port), visual-notes
bugs 26-28; FindwxWidgets.cmake config label drops 'wasmuniv'
- wxwidgets submodule -> 9dbacc9448 (DOM-only port, fork diff shrunk)
Gate: full wx e2e suite 292 passed / 1 skipped / 0 failed — first run
ever with the gal-webgl specs green (28 scenarios + load + sequential).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New tests/kicad/appearance.spec.ts: tab cycle through Layers/Objects/
Nets and back (with blank-rows regression assertion), wheel scrolling
in Layers (movement + exact scroll-back restoration) and Objects, with
screenshots per state. All 3 pass against the DOM pcbnew.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The asyncify single-slot work, executed red-green (full ledger:
docs/features/asyncify-arbiter/redgreen.md; decisions record:
docs/features/async/07-decisions-and-outcome.md):
- tests/apps/standalone/asyncify-races/ + tests/asyncify/ + dedicated
playwright config: 8 scenarios reproducing the KiCad asyncify failure
family with the kicad-faithful startup topology (pre-park fiber swap →
park throw through the live trampoline). Built in 3 variants; the
SHIM_DISABLE_TRAMPOLINE_HEAL / SHIM_DISABLE_HANDLESLEEP ablation builds
keep the historical hang and index-out-of-bounds crash reproducible
forever (mutation-style pins for the existing shims).
- scripts/common/shims/handlesleep.js: catch the "unwind" park sentinel
in the wakeUp path — when main's last pre-park suspension was a sleep,
the main-loop park throw escaped through that sleep's promise reaction
as an uncaught rejection (the calculator/gerbview console errors).
- scripts/common/inject-dyncall-shims.sh: SHIM_DISABLE_* ablation knobs.
- Spec tightening (the acceptance bar): 'uncaught exception: unwind'
tolerance DELETED from pcbnew/eeschema specs; load-pcb gained a hard
clean-console gate over 5 asyncify corruption signatures.
- wxwidgets pointer bump: modal LIFO resolvers, pump resolve-on-error,
sync clipboard IsSupported (014f67e6c1).
Final state: asyncify suite 7/7, wx e2e 291/292 (1 skip), KiCad e2e 40
passed / 2 skipped with ZERO corruption signatures in any log across all
six apps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- WasmTool: hard cutover to startKicadCollab (gates on
kicadCollabSnapshotItems; scalar reconciler no longer runs in the app)
- file-seed (ysync 0005 tie-in): empty rooms are seeded from the opened
file via fileToDoc/docToY (meta+layout+items — file recoverable from the
Y.Doc alone); populated rooms still adopt (seed-once unchanged)
- binding: gate UP applies until seed() — the provider's initial state
sync otherwise streams the full doc into an editor that already holds
the file (trapped eeschema's paste path in the real app)
- pcbnew blobForItem: footprints Format a uuid-corrected copy directly —
SaveSelection's copy regenerated mandatory-field uuids (FOOTPRINT copy
ctor assigns into fresh fields), breaking wire identity (rebuild)
- roundtrip.spec on the v2 items wire; new passing pcbnew
footprint-containment round trip (the 0004 containment win); full
fixture stays fixme on the tracked envelope-parse limit
- real-app two-tab emit verified for eeschema + pcbnew (see 0008 doc)
Full kicad suite: 45 passed, 3 skipped (known), 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add kicadCollabSnapshotItems / kicadCollabApplyItems / window.kicadCollab.onItems
to pl_editor, eeschema, pcbnew — per-item native-blob payloads
({added/changed:[{sexpr,parent}], removed:[uuid]}) alongside the untouched scalar
wire (legacy collab specs stay green).
- pl_editor: itemBlob per item; apply = SetPageLayout(append) + replace-by-uuid
(pointer-snapshot safe); bare payloads get the kicad_wks envelope. Snapshot +
apply + emit verified headless.
- eeschema: clipboard Format per item (symbols carry their lib_symbols); apply
mirrors the native paste — LoadContent into a throwaway sheet → detach →
replace-by-uuid → symbol lib relink (blob's lib_symbols first, live screen's
second) → SCH_COMMIT, in the CallAfter+COROUTINE context. Snapshot + apply
verified headless (a "lost" lone junction turned out to be correct connection
cleanup — test uses text).
- pcbnew: blobForItem per ROOT item with child→footprint lifting in flushDiff;
apply = makeFromBlob + commit replace-by-uuid on the fiber; bare non-footprint
payloads get wrapInBoardEnvelope (live board layer table). Snapshot + footprint
replace/add WITH children (the 0004 containment gap, closed) + removal verified
headless. Track/via/zone/text blob-apply hits the documented asyncify-fragile
envelope parse (reconfirmed empirically — a verbatim SaveSelection segment
envelope dies silently in the commit) and stays on the legacy scalar apply;
tracked in ysync 0008 status.
- eeschema/pcbnew scheduleFlush now runs flushDiff inside a COROUTINE: the
per-item Format in the v2 emit needs the fiber stack (0007 lesson). Their emit
remains unverifiable headless (both legacy two-tab tests are test.skip:
"open=false → SCH_COMMIT no-ops" / "harness can't PAINT") — verify in the real
app at Stage D; pl_editor's emit IS verified.
- tests/kicad/items-bridge.spec.ts: per-tool suite (snapshot uuids → local-edit
emit (where drivable) → apply changed/added/removed via save-readback → no
apply echo). 3/3 pass; roundtrip + collab suites unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- standalone wasm/collab/types.ts: the CollabItem/CollabDelta data shapes now
come from MIT @pcbjam/shared (collab-wire zod schemas) via re-export; the
RUNTIME adapter interface (CollabBridge) stays here. emptyDelta/isEmptyDelta
alias the shared helpers — reconciler untouched.
- tests/collab/browser-entry.ts: fix the import path stale since the repo
restructure (web/apps/frontend → web/standalone) — the collab bundle had been
silently unbuildable; its opts type now tracks startCollab's real signature.
- tests/kicad/*-collab.spec.ts: update KicadCollab.start calls from the
pre-ysync-0004 { channel, settleMs } API to { provider: { kind:
"broadcastchannel", settleMs }, room } — the drift the broken bundle hid.
Verified: pl_editor 2/2, eeschema 2 passed, pcbnew 7 passed (skips pre-existing);
shared 47 unit tests; both typechecks clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Block A save embind exports + the round-trip integration harness.
Save exports (wasm/bindings, GPL):
- pcbnew: kicadSaveBoard(path) via PCB_IO_KICAD_SEXPR::SaveBoard(GetBoard()).
- eeschema: kicadSaveSchematic(path) via SCH_IO_KICAD_SEXPR::SaveSchematicFile.
Saves GetCurrentSheet().Last() (NOT Schematic().Root()): the wasm open-flow
nests the opened doc under an auto-created project root, so Root()'s screen
holds only a child-sheet symbol, not the loaded items.
(pl_editor already had kicadSaveDrawingSheet.)
Round-trip harness (tests/kicad/roundtrip.spec.ts):
- load fixture → save (ORIG) → kicadCollabSnapshot → reload (fresh wasm) →
open empty → kicadCollabApply → save (REGEN) → assert sexprDiff(ORIG,REGEN).equal.
- Two separate pages (extract closed before rebuild opens) so the process-global
wasm heap frees between boots (pcbnew ~190MB). Open both sides from the same
filename (eeschema embeds it as the root Sheetfile property). pcbnew boots the
seeded pcbnew-collab.html (plain pcbnew.html's first-run wizard blocks boot).
- pl_editor + eeschema round trips PASS (lossless). pcbnew is test.fixme with
tracked apply-coverage findings (footprints/zones not reconstructed; segment
width + via size lost; fp_text→gr_text) — bridge gaps for follow-up, not test bugs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI runs (Hetzner 27329612719, Ubicloud 27330989479) failed 22/41 identically,
in two buckets, both firefox-environment issues (the suite is firefox-only:
test:kicad -> --project=firefox):
1. No WebGL: headless Firefox can't create ANY GL context on a GPU-less VM
(blocklist bypass still dies with FEATURE_FAILURE_WEBGL_EXHAUSTED_DRIVERS),
so the GAL canvas failed and an error dialog blocked every UI flow. Fix:
run headed under xvfb (GLX + Mesa llvmpipe software GL) with
webgl.force-enabled — workflows now wrap the e2e step in xvfb-run.
2. pcbnew wasm OOM: the ~190M module OOMs Firefox's optimizing wasm JIT at
compile ("InternalError: out of memory") and the app never boots. Fix:
javascript.options.wasm_optimizingjit=false (baseline-only compile).
Both prefs are CI-gated in playwright-kicad.config.ts; local runs keep stock
behavior. Boot got slower (baseline JIT), so the wizard helpers now wait for
the registry to have actual UI ENTRIES (the registry OBJECT exists pre-boot)
instead of clicking into a fixed 10x500ms window.
Validated in a local docker repro of the CI env (playwright:v1.57.0 image,
no GPU): pl_editor canvas, gerbview wizard+canvas, and pcbnew wizard (on a
properly asyncified 187M module) all pass; before the fix they reproduced the
exact CI signatures.
Known-fail left on CI: zoom-cursor — the zoom-in anchors at ~ -P in headed
Firefox (screen-vs-client coordinate mix-up in the wheel->GAL path, headed
mode only); marked test.fixme(CI) pending a wx-wasm-layer investigation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a layer-preservation check to the footprint/via/zone/text round-trip add tests,
documenting + verifying that an item's own serialized layer is applied by makeItem and
the peer editor's active layer is never consulted. 4 add tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A board-level graphic text (Place→Text) lands in Drawings() and would otherwise hit the
same asyncify-fragile (kicad_pcb …) envelope-blob wall as via/zone on add. So PCB_TEXT
reconstructs NATIVELY: itemToJson emits size/thickness/angle plus horizontal & vertical
justification, mirror, and bold/italic (the text string was already emitted for any
EDA_TEXT); makeItem builds a fresh PCB_TEXT and restores all of them. flushDiff skips the
blob for PCB_TEXT_T. Footprint child text is unaffected — it syncs by move, and its add
is carried by the footprint blob.
Justification matters: it anchors the glyphs relative to the text POSITION, so without it
a left-justified text reconstructed centered on the peer and rendered visibly offset even
though GetPosition() matched (which is why the anchor-only headless check missed it).
Verified two-tab in the real app: a left/bottom-justified bold text added in tab A
reconstructs in tab B at the exact position with matching justification + string. Headless:
a board-text round-trip add test (sample text is left/bottom-justified) asserts the
position AND that hjust/vjust/text round-trip. 7 passed, 1 skipped, 0 aborts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Footprint add reconstructs from the bare `(footprint …)` s-expr clipboard blob via
CLIPBOARD_IO (SetWriter/SetReader redirect it to a string for headless/wasm). The
non-footprint `(kicad_pcb …)` envelope parse (parseBOARD) is asyncify-fragile in wasm
— commit.Add of an envelope-parsed item silently stages nothing and subsequent virtual
dispatch traps ("index out of bounds"), the same wall that deferred the eeschema symbol
blob — so vias and zones reconstruct NATIVELY instead: itemToJson emits drill/layer-pair
for a PCB_VIA and the outline polygon for a ZONE, and makeItem builds a fresh PCB_VIA /
ZONE. flushDiff attaches the blob only to types that need it (footprints/board graphics);
tracks/vias/zones skip it. Includes the dangling-parent fix for the envelope path
(SetParent before delete) which footprints don't hit.
Build: kicad_clipboard.h pulls in the generated pcb_lexer.h (emitted into the common
build subdir by make_lexer) — added -I${KICAD_BUILD}/common to the embind include path.
Tests: sample board gains a via + zone; snapshot asserts their native fields are emitted,
and three round-trip add tests (footprint via blob, via/zone native) delete then re-add
each by uuid and confirm it returns at the same position. 6 passed, 1 skipped, 0 aborts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bug 1 — footprint child text now syncs. forEachTopItem also visits each footprint's
TEXT children by their own uuid (GetFields() PCB_FIELDs + GraphicalItems() PCB_TEXT),
so moving a silkscreen reference/value/user text — which leaves the footprint origin
unchanged — produces a diff. Apply needed only a guard: the removed loop skips items
with a parent footprint (a footprint delete cascades to its children, so don't
double-remove). ResolveItem already resolves child uuids and BOARD_COMMIT::undoLevelItem
rolls a child Modify up to the footprint, so Modify+SetPosition (in the existing
COROUTINE) just works. itemToJson now carries text for EDA_TEXT items.
Bug 2 — kicadCollabTestMoveFirst no longer no-ops. Its CallAfter now runs the move
inside a COROUTINE fiber (like doApply) so the virtual BOARD_ITEM::Move dispatches
instead of asyncify-no-opping.
Tests: sample board gains a footprint with a Reference field + user fp_text; snapshot
test asserts the footprint and both text children are emitted by uuid; a new test
moves each child by uuid and asserts the footprint origin stays put. 3 pass, 1 skipped.
Verified two-tab in the real app: C1's reference text move syncs A->B, no footprint moves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pcbnew's half of the unified Yjs collab bridge (4th tool; yjs-bridge commit 4),
a near-verbatim port of the eeschema design. Root-repo only — kicad/wxwidgets
submodules untouched.
- wasm/bindings/pcbnew_embind.cpp: BOARD_LISTENER trigger + post-settle snapshot
diff emit; BOARD_COMMIT apply inside a CallAfter + COROUTINE fiber (so a new
item's GAL view->Add dispatches correctly). Move/delete sync for any top-level
item by uuid; native PCB_TRACK add. Footprint/via/zone add deferred.
- WasmTool.tsx: add pcbnew to COLLAB_TOOLS.
- tests/apps/kicad/pcbnew-collab.html: seeded (wizard-free) harness, leaving
pcbnew.html untouched for its wizard test.
- tests/kicad/pcbnew-collab.spec.ts: snapshot + apply(move/remove/add) — 2 pass,
two-tab skipped headless.
Verified two-tab in the real web app: footprint move applies + syncs A->B.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thrust A — dialogs render top-left with OK clipped in the WEB app (not the
test harness): root cause was the React shell missing the .window /
.window-canvas CSS that wx.js relies on (it positions each dialog div via
inline left/top, which need position:absolute). Added the rules to
web/apps/frontend/src/index.css. Native draw-text now works end-to-end;
symbol/power choosers render (placing still blocked by absent libraries).
Thrust B — collab apply of a newly-added SCH_SHAPE trapped in KiCad core
(SCH_COMMIT::Push CHT_ADD -> GAL view->Add, an asyncify invoke_* mis-dispatch)
because doApply ran off a fiber stack. doApply now runs inside a COROUTINE so
it executes on a libcontext fiber, the same context native draws use; the add
dispatches correctly. Re-enabled the SCH_SHAPE converter (rect/circle). Added
thirdparty/libcontext to the embind include path (tool/coroutine.h needs it).
Verified two-tab: rectangle + circle drawn in tab A sync + render in tab B.
Extended eeschema-collab.spec.ts apply test with a SCH_SHAPE add.
All changes root-repo only; kicad and wxwidgets forks untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump kicad + wxwidgets submodules with two wasm eeschema UI fixes, and add the
regression test tests/kicad/eeschema-ui.spec.ts (Delete + Backspace delete; the
text-tool properties dialog opens and closes without freezing).
- wxwidgets c27fe8b: nested (quasi-modal) event loops pump via Asyncify instead of
re-entering emscripten_set_main_loop (which threw an un-resumable 'unwind').
- kicad 4132395: bind Backspace to delete under emscripten + apply default alt hotkeys.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>