13 KiB
06 — Part 2: implementation record & current state
Status: Part 2 IMPLEMENTED (2026-07-02). As-built companion to the
04plan: the pcbnew and eeschema kifaces are now statically linked into one WASM executable,kicad_editor, and all four editors (PCB / Footprint / Schematic / Symbol) are runtime--framechoices of that single bundle. One editor per page load (unchanged UX); multi-frame/cross-probing remains a follow-up this merge lays the groundwork for.
One-paragraph summary
kicad_editor.wasm (178 MB at -O1 debug vs 147 + 82 for the two engines separately —
shared wx/common/boost linked once) serves every editor tool. The frontend maps all four
editor tools onto the one bundle (TOOL_BUNDLE) and passes --frame=pcb|fpedit|sch| symedit. The merge's real work was symbol-collision surgery: each engine's Kiface()
accessor and KIFACE_1 getter got per-engine names via CMake compile definitions (zero
call-site edits), 21 additional ODR collisions found by a symbol audit (upstream
reuses class names like DIALOG_TEXT_PROPERTIES for different classes per module) were
renamed on the PCB side the same way, six shared common/ call sites now resolve their
owning frame's kiface exactly, and the duplicated embind layer became per-editor entries
behind a single dispatching registration.
What changed
C++ — kicad submodule (commit a542e264; ALL gated on KICAD_WASM_MERGED_EDITOR, OFF by default — option-OFF trees and native builds are byte-identical)
| File | Change |
|---|---|
include/kiway.h |
KIFACE_GETTER macro #ifndef-guarded so each kiface can carry a distinct getter symbol. |
CMakeLists.txt |
KICAD_WASM_MERGED_EDITOR option; KICAD_WASM_PCB_SIDE_RENAMES (the Kiface=PcbKiface static binding + the 21 audited ODR renames, see below); gated add_subdirectory(${KICAD_WASM_LAYER}/editor) after both engines. |
pcbnew/CMakeLists.txt, eeschema/CMakeLists.txt, common/CMakeLists.txt |
Apply the renames to pcbnew_kiface_objects + pcbcommon / Kiface=SchKiface to eeschema_kiface_objects; per-source KIFACE_GETTER=<engine>_kiface_getter on the hoisted pcbnew.cpp/eeschema.cpp; both *_KIFACE_LIBRARIES lists exported CACHE INTERNAL for the out-of-fork merged target. |
common/single_top.cpp |
KICAD_MERGED_KIFACES branch registers both faces (set_kiface(FACE_PCB/FACE_SCH, …), OnKifaceStart stays lazy — the native project manager's model); latent Part 1 bug fixed: PreloadLibraries now uses the runtime-resolved frame, not compile-time TOP_FRAME (a --frame=sch boot on the PCB-default image preloaded the wrong face). |
common/eda_base_frame.cpp, common/dialogs/dialog_color_picker.cpp, common/design_block_tree_model_adapter.cpp |
The six real Kiface() call sites in shared code (config()/sys_search()/help_name() + color picker ×2 + design-block tree) resolve their owning frame's kiface exactly (m_ident → KifaceType → KiFACE), falling back to the global accessor. __EMSCRIPTEN__-only; standalone-tree behavior identical. |
Why static per-engine binding (not one "active kiface" pointer)
PCB code runs while a schematic frame is active — e.g. the symbol chooser's footprint
preview reaches KiFACE(FACE_PCB) via common/project.cpp, after which pcbcommon
painter/IO code (pcb_painter.cpp, pad.cpp, …) calls Kiface() with no frame context
and must get the PCB kiface. A focus-based global would hand it the schematic one.
Hence: module code binds statically (Kiface=PcbKiface/SchKiface); only the merged
image's fallback Kiface() (in wasm/editor/merged_kiface_dispatch.cpp) does a
focus/top-window walk, and the six common/ sites resolve per-frame.
The ODR audit (Stage 0 gate — the research doc's list was incomplete)
llvm-nm strong-symbol intersection of the two kiface object sets + weak-symbol
intersection minus shared-lib symbols found, beyond Kiface()/KIFACE_1:
- Same-name-different-class pairs (vtables/typeinfo/inline members would silently
cross-bind under
--allow-multiple-definition→ memory corruption, not link errors):DIALOG_TEXT_PROPERTIES,DIALOG_SHAPE_PROPERTIES,DIALOG_TABLE_PROPERTIES,DIALOG_TABLECELL_PROPERTIES,DIALOG_GLOBAL_EDIT_TEXT_AND_GRAPHICS,PANEL_SETUP_FORMATTING(each +_BASE),TEXT_SEARCH_HANDLER,GROUP_SEARCH_HANDLER, and (weak-only, invisible to a strong-symbol scan)TEXTBOX_POINT_EDIT_BEHAVIOR,RECTANGLE_POINT_EDIT_BEHAVIOR,FILEDLG_HOOK_SAVE_PROJECT,FOOTPRINT_INFO_GENERATOR. - Colliding free fn/data:
checkOverwriteDb(),allowedActions[],g_excludedLayers. - Benign (no symbol intersection): PADS importer structs,
CONNECTION_GRAPH(namespaced), all same-name classes defined once ininclude//common/.
All renamed one-side (PCB_ prefix) via the CMake define list. Re-run
scripts/kicad/audit-merged-symbols.sh (in the build container) on every kicad
submodule bump — a new upstream same-name class ships as silent corruption otherwise.
WASM layer & build — root repo (commit 6bf0b76)
wasm/editor/CMakeLists.txt— the merged executable:single_top.cpp(TOP_FRAME=FRAME_PCB_EDITOR;KICAD_MERGED_KIFACES, noPGM_DATA_FILE_EXT— Part 1's runtime extension map covers all four frames) +merged_kiface_dispatch.cpp; links both exported library lists (deduped) with--allow-multiple-definition+--whole-archive pcbcommon; binary dirkicad_editor/makes docker/build.sh's${subdir}/${app}.jscopy work unmodified.wasm/bindings/— both per-editor TUs' six shared collab entries renamedpcbCollab*/schCollab*(JS-facing names unchanged); their duplicatekicadOpenFile/extern "C" kicadCollabOnSavedefinitions and shared-nameEMSCRIPTEN_BINDINGSregistrations compiled out underKICAD_MERGED_EMBIND; newkicad_editor_embind.cppregisters each shared JS name once and dispatches onpcbEditorActive()/schEditorActive()(the same top-windowdynamic_castprobe the impls always started with). Standalone pcbnew/eeschema builds compile without the define and register the same JS names as before. (War story:pcbCollab*/schCollab*in a block comment — the*/terminates the comment. Cost one build cycle.)scripts/kicad/build-kicad-target.sh—kicad_editorcase arms;STUB_APP=pcbnew(eeschema's frame stub arrives via CMaketarget_sources);-DKICAD_WASM_MERGED_ EDITOR=ONfor this app only; Step 7.1 refactored intocompile_embind_tuand compiles three embind objects for the merged app (both per-editor TUs with-DKICAD_MERGED_EMBIND+ the dispatcher), all with the ABI-criticalKICAD_TU_ABI_FLAGS; multi-object force-relink guard.docker/build.sh—all=kicad_editor calculator pl_editor gerbview(kicad_editor first: largest bundle = the wasm-opt critical path).pcbnew/eeschemaremain valid explicit apps (standalone debug aids in their own option-OFF trees; not deployed).scripts/kicad/build-kicad_editor.shwrapper added.scripts/kicad/audit-merged-symbols.sh— the repeatable ODR audit..github/workflows/wasm-opt-bench.yml— left buildingeeschemadeliberately (still a valid explicit app; cheaper fixture, representative for pass sweeps).
Frontend & deploy
web/standalone/src/wasm/constants.ts— newBundleunion type (a bundle is a delivery artifact, NOT aTool;kicad_editoris deliberately not in theTOOLSenum).TOOL_BUNDLE: Record<Tool, Bundle>maps all four editors →kicad_editor;TOOL_FRAMEgains explicitpcb/schfor pcbnew/eeschema (the merged bundle can only default to one frame — PCB).TOOL_ARGV0/config-seed/lib-kind stay per-tool.boot.ts(pthreadWorkerScriptparam →Bundle; dead?? toolfallbacks dropped) andwasm-assets.ts— already bundle-aware from Part 1; typecheck green.scripts/deploy/publish-wasm.mjs—TOOLS = [kicad_editor, pl_editor, gerbview, calculator]. NOTE: run one full publish before any--from-registrysnapshot (the registry needs a kicad_editor entry); old CDN folders are immutable so deployed frontends keep working.
Tests & CI
- All five editor harness HTMLs load
kicad_editor.js; pcbnew/pcbnew-collab/eeschema gained explicit--frame=pcb/--frame=schinModule.arguments. tests/scripts/setup-kicad-wasm.shcopieskicad_editorinstead of pcbnew+eeschema.tests/playwright-kicad.config.ts—PCBNEW_FAMILY_SPECS→BIG_MODULE_SPECS- the 8 eeschema-family globs (eeschema.spec, -collab, -crosshair, -load, -subschema,
-ui, -url-regex, symbol_editor.spec): every spec booting the ~190 MB merged module
routes to
chromium-cion CI (SpiderMonkey x86 code-budget OOM). Firefox keeps the small bundles. Verified via--list: CI-firefox 0 merged-module tests, chromium-ci 43.
- the 8 eeschema-family globs (eeschema.spec, -collab, -crosshair, -load, -subschema,
-ui, -url-regex, symbol_editor.spec): every spec booting the ~190 MB merged module
routes to
tests/kicad/frame-runtime.spec.ts— extended to all four frames; also asserts no duplicate embind registration ("Cannot register public name").
Validation (2026-07-02, local ARM Mac, Firefox, -O1 debug bundle)
- frame-runtime 4/4 — each harness lands on its own title from the one bundle
(
PCB Editor,[no footprint loaded] — Footprint Editor,untitled [Unsaved] — Schematic Editor,[no symbol loaded] — Symbol Editor), no aborts, no duplicate embind registration. 17 s total. - Merged-module regression subset (16 spec files): 24 passed, 1 flaky-passed (3d-viewer open/render under 4-worker load), 1 failed — see known-failure below. 3D raytracer renders the pic_programmer board correctly (screenshot checked). No tracked-baseline screenshot diffs.
- Collab bridge: 10 passed, 2 pre-existing skips — eeschema + pcbnew collab (snapshot/apply/two-tab BroadcastChannel propagation/native-EH double apply) all through the merged embind dispatcher, in both frame types.
- Symbol/link audit of the built tree —
PcbKiface/pcbnew_kiface_getterandSchKiface/eeschema_kiface_getterin their own objects; plainKiface()only in the dispatch TU; embind JS names present exactly once in the wasm. - Full suite (all bundles): 67 passed, 3 pre-existing skips, 1 failed — the single failure is the known pre-existing 3d-viewer drag deadlock below (its follow-on resize test "did not run"). calculator/pl_editor/gerbview rebuilt and green alongside the merged bundle.
- Cross-face probe (
xface-probe.spec.ts, kept as a permanent guard): PASSED — in a--frame=schsession, opening Preferences lazy-starts the PCB kiface and its pages appear in the tree ("Footprint Editor", "PCB Editor", "3D Viewer" — absent on the old single-kiface bundle, whereKiFACE(FACE_PCB)returned nullptr). This is the same path the symbol chooser's footprint preview uses, and a user-visible improvement: the other engine's preference pages now populate.
Pre-existing failure, RESOLVED by the rebase (was NOT a merge regression)
3d-viewer.spec.ts "draggable, X-closable DOM title bar" failed during initial
validation: dragging the 3D viewer re-renders via the raytracer, which needs fresh
pthread workers from a drained pool — the on-demand worker boot deadlocks the main
thread. The fix pre-dated on origin/main as 7630c7e ("raytrace deadlock — pre-warm
2N+8 pthread Workers", a PTHREAD_POOL_SIZE link-flag change the branch base
predated). The branch has since been rebased onto main (root + kicad; conflicts
were the kicad gitlink, the boot.ts/WasmTool.tsx additive hunks vs main's lazy-3D-model
work, and the BIG_MODULE_SPECS rename vs main's added 3D specs/reformat) and
kicad_editor relinked — 3d-viewer + frame-runtime rerun: all green (7 passed +
1 flaky-passed under parallel load, 0 failures).
Build & size facts
output/kicad_editor.wasm178 MB (-O1debug; sidecar.wasm.debug.wasm2.3 GB) vs 147 + 82 MB for the separate engines — the dedup of wx/common/boost is real. Deployed set is now 4 bundles instead of 5.- Host postprocess (dyncall shims → finalize → asyncify →
wasm-opt -O1) completed on a local Mac without drama — the-O2cost/RSS concerns in04apply only to release-level wasm-opt. - Build:
./docker/build.sh kicad_editor(own treekicad-kicad_editor, configured with-DKICAD_WASM_MERGED_EDITOR=ON; standalone pcbnew/eeschema trees untouched).
What's next
- Run
tests/web/tools-open.spec.tsagainst the real web app once ports 3048/3060 are free (at validation time another worktree's dev stack held them; the frontend path itself —resolveWasmBase→TOOL_BUNDLE→boot→--frame— is unchanged from Part 1's validated flow, only the map values differ, typecheck green). First deploy needs one fullpublish-wasm.mjsrun (registry entry) before any snapshot deploy. - Follow-ups (out of scope): multi-frame/cross-probing in one instance (both faces are now registered under one KIWAY — the groundwork exists); folding gerbview/pl_editor/calculator into the image; size levers (wasm-EH, JSPI).