pcbjam/wasm/stubs/oce_plugin_stub.cpp

264 lines
7.2 KiB
C++
Raw Normal View History

feat(wasm): occ-split — lazy occ_service worker; kicad_editor drops OCC (−31%) 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>
2026-07-03 12:39:58 +02:00
/*
* WASM shadow of the oce 3D plugin's flat-C surface (oce3d_*) the editor
* build links this INSTEAD of s3d_plugin_oce (no OCC in pcbnew.wasm; see
* docs/features/occ-split/README.md). The static plugin registry
* (kicad/3d-viewer/3d_cache/pcbjam_static_3d_plugins.cpp) consumes exactly
* this surface, so the plugin manager, S3D_CACHE, and the scene build compile
* untouched .step/.iges footprint models keep loading, their parse just runs
* in the occ_service worker.
*
* oce3d_Load: the model file is already in MEMFS (materialized by
* PCBJAM_3D::EnsureModelFile before the plugin dispatch). Ship its bytes to
* the worker (EM_ASYNC_JS suspend legal here: EnsureModelFile already
* suspends on this same S3D_CACHE::load path), get back the SCENEGRAPH
* serialized in KiCad's own binary cache format, and rebuild it with
* S3D::ReadCache.
*
* Metadata (extensions/filters/versions) mirrors plugins/3d/oce/oce.cpp and
* include/plugins/3d/3d_plugin.h so the manager's handshake and extension map
* are identical to the real plugin's.
*/
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <emscripten.h>
#include <wx/string.h>
#include <wx/filefn.h>
#include <plugins/3dapi/ifsg_api.h>
class SCENEGRAPH;
namespace
{
// Non-Windows extension/filter lists from plugins/3d/oce/oce.cpp (FILE_DATA).
const std::vector<std::string> k_extensions = {
"stp", "STP", "stpZ", "stpz", "STPZ", "step", "STEP", "stp.gz", "STP.GZ", "step.gz",
"STEP.GZ", "igs", "IGS", "iges", "IGES"
};
const std::vector<std::string> k_filters = {
"STEP (*.stp;*.STP;*.stpZ;*.stpz;*.STPZ;*.step;*.STEP;*.stp.gz;*.STP.GZ;*.step.gz;"
"*.STEP.GZ)|*.stp;*.STP;*.stpZ;*.stpz;*.STPZ;*.step;*.STEP;*.stp.gz;*.STP.GZ;"
"*.step.gz;*.STEP.GZ",
"IGES (*.igs;*.IGS;*.iges;*.IGES)|*.igs;*.IGS;*.iges;*.IGES"
};
bool acceptAnyCacheTag( const char*, void* )
{
// The cache blob just crossed the worker boundary from our own writer —
// there is no plugin-version drift to guard against.
return true;
}
} // namespace
// Phase E shape (docs/features/async/22 §5, K5): waits for the worker
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose The runtime is JSPI-only; this removes everything that still pretended otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove the inventory; every deletion verified by grep closure + full gates. Broken-right-now fixes: - deploy-staging.yml passed the retired opt_level input — the workflow could not even start. Removed. - env.sh carried dead exports with a live -sASYNCIFY=1 inside (WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason. - docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone. Dead weight removed: - binaryen submodule (nothing builds or invokes it), wasm-opt-bench workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess scaffolding (existed to parallelize the deleted wasm-opt phase; the postprocess is a seconds-long node script and now runs inline), build-monitor's dead asyncify rows, sched-context orphan build output, dead .gitignore entries, the .jspi-assets spike dir (the two wf-result research JSONs moved to docs/features/async/migration-evidence/). - bindings: fiber_park.h + its 12 embind registrations (broken-if- called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route, main_stack_runner.h + 5 includes, the always-null context-sleep weak hook in nanosleep_yield.c. - shim: the backend field (installed-flag idempotency instead), noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the WasmTool fallback and string-dump normalize branch). - web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts (gerber-demo keeps it: it loads the deployed CDN release, which predates emscripten 6 — noted inline). Conditionals: all 'backend === jspi' checks reduced to scheduler- presence checks; races_quiescent re-keyed from Asyncify.state (vacuous) to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive, which is the probing activation's own window by definition). Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→ JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS, kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests), collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→ wasmTrapSignatures (lists byte-identical). Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused asserts re-keyed to live JSPI beacons; eeschema-load's failure message no longer sends the developer to a deleted script; wait-beacons' dead families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is unconstructible); the embind test.fail re-gated with the JSPI reason (plain embind invokers cannot suspend — verified still failing); lint-determinism now scans tests/jspi (166 files clean); eeschema-collab local-move gated to chromium (~50% flaky on FF even solo; pcbnew twin covers both engines). Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md describes the single-phase build; docs/features/async/README.md banner-marked historical and repointed at the NEW 23-jspi-runtime.md (current architecture: export census, turnstile, libcontext ownership + refusal contract, embind call shapes, the em-pthread service-wrapper trick, exception policy, known gaps). Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the quiescence-probe fix; the 3 other reds were verified contention flakes solo-green or the documented FF gate), web 76/0, jspi 18/18 both engines, vitest 295/295 + 17/17, all lints green, live-app census clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
// parse+tessellate via a token wait instead of suspending in place. The
// wait result is a malloc'd path string: the scenegraph-cache file written
// into this module's MEMFS ("" on failure). Every resolution defers to at
// least a microtask (the early-resolve contract, doc 22 §10 Phase E retry).
EM_JS( void, js_occLoadModelStart, ( int aToken, const char* aModelPath ),
feat(wasm): occ-split — lazy occ_service worker; kicad_editor drops OCC (−31%) 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>
2026-07-03 12:39:58 +02:00
{
const modelPath = UTF8ToString( aModelPath );
findings(E-5,E-8,E-9): module-identity ngspice events + runWaitCompletion admission gate E-8 (re-implemented for JSPI — the codex gate is entangled with the dropped execution owner; under JSPI a fresh non-suspending JS→wasm entry while another activation is suspended is structurally safe on its own stack, so the admission boundary for worker completions is liveness + trap state, not execution ownership): - jspi-scheduler.js grows `terminal` (trapped instance; distinct from `dead`), canTouchNative(), _terminalizeNativeTrap() (WebAssembly.RuntimeError + cross-realm string classification), and runWaitCompletion(site, token, prepare, inertResult): prepare runs immediately and owns ALL native work; stale tokens and dead/terminal instances drop loudly without resolving (resolving would resume the parked frame inside the damaged module); a trap latches terminal; a plain JS bug resolves inertResult so the wait fails instead of stranding. beginWait refuses (token 0) when dead/terminal. - all four delayed completion sites route their native work through the gate: 'OCC export completion' (exporter_step_stub), 'OCC model completion' (oce_plugin_stub — the MEMFS cache write moves inside the gate too), 'ngspice request completion' and 'ngspice vector completion' (sharedspice_client — every HEAP32/HEAPF64/malloc write inside prepare, inertResult 1 = transport error). Every wxWasmBeginWait caller in the stubs bails on token <= 0. - deliberately NOT ported from codex: ownerModule, enqueueNativeCompletion, executionBarrier, the byte-credit native-entry FIFO — completions are one-shot per wait token and stream volume is bounded at the E-6 transport credit window. Cross-refs logged for group M (M-2/M-6/M-8). E-5 (re-implemented; codex shape kept, owner APIs replaced with the E-8 gate): js_ngspice_install_events binds the handler to the EXACT installing module (handler.__pcbjamNgspiceOwnerModule stamp; presence is not identity), re-installation replaces a foreign module's handler, a superseded handler disarms itself, native entry goes through installingModule._malloc/ ._pcbjam_ngspice_event (never lexical Module), each dispatch checks canTouchNative() (loud drop on a dead/terminal module), and a trap on the per-line entry latches the terminal gate. Tests: scheduler-shim.test.ts +7 (gate happy/stale/dead/terminal/cross-realm/ js-bug/beginWait-refusal). e2e specs updated from the codex line: occ-export decode-fault recovery (real onmessageerror transition via failDecode, J-4), ngspice-probe direct-service coverage, eeschema-sim rewritten onto the E-7 applied-generation receipt (codex's executionBarrier await replaced with a pendingWaits('ngspice') drain poll — the JSPI-line equivalent). Also bumps the kicad submodule to the E-7/E-9 commit (dd5751038f7). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 16:03:16 +02:00
// E-8: all native work (the MEMFS cache write + the malloc'd path) runs
// inside the scheduler's completion gate — a dead or trapped instance
// drops the completion loudly instead of re-entering wasm, and a trap
// inside the prepare latches the instance terminal without resolving.
const finish = ( writeCache ) => {
globalThis.__wxScheduler.runWaitCompletion( 'OCC model completion', aToken, () => {
const cachePath = writeCache();
const n = lengthBytesUTF8( cachePath ) + 1;
const p = _malloc( n );
stringToUTF8( cachePath, p, n );
return p;
} );
};
let req;
feat(wasm): occ-split — lazy occ_service worker; kicad_editor drops OCC (−31%) 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>
2026-07-03 12:39:58 +02:00
try
{
const hook = globalThis.occService;
if( !hook || typeof hook.request !== 'function' )
{
console.error( '[pcbjam-occ] loadModel: occ_service provider not installed' );
req = Promise.resolve( null );
feat(wasm): occ-split — lazy occ_service worker; kicad_editor drops OCC (−31%) 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>
2026-07-03 12:39:58 +02:00
}
else
{
const bytes = FS.readFile( modelPath ); // Uint8Array copy — transferable
const dot = modelPath.lastIndexOf( '.' );
const ext = dot >= 0 ? modelPath.slice( dot + 1 ) : 'step';
req = Promise.resolve( hook.request( { kind: 'loadModel', bytes, ext } ) );
feat(wasm): occ-split — lazy occ_service worker; kicad_editor drops OCC (−31%) 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>
2026-07-03 12:39:58 +02:00
}
}
catch( e )
{
console.error( '[pcbjam-occ] loadModel request failed:', e );
req = Promise.resolve( null );
feat(wasm): occ-split — lazy occ_service worker; kicad_editor drops OCC (−31%) 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>
2026-07-03 12:39:58 +02:00
}
req.then( ( res ) => {
findings(E-5,E-8,E-9): module-identity ngspice events + runWaitCompletion admission gate E-8 (re-implemented for JSPI — the codex gate is entangled with the dropped execution owner; under JSPI a fresh non-suspending JS→wasm entry while another activation is suspended is structurally safe on its own stack, so the admission boundary for worker completions is liveness + trap state, not execution ownership): - jspi-scheduler.js grows `terminal` (trapped instance; distinct from `dead`), canTouchNative(), _terminalizeNativeTrap() (WebAssembly.RuntimeError + cross-realm string classification), and runWaitCompletion(site, token, prepare, inertResult): prepare runs immediately and owns ALL native work; stale tokens and dead/terminal instances drop loudly without resolving (resolving would resume the parked frame inside the damaged module); a trap latches terminal; a plain JS bug resolves inertResult so the wait fails instead of stranding. beginWait refuses (token 0) when dead/terminal. - all four delayed completion sites route their native work through the gate: 'OCC export completion' (exporter_step_stub), 'OCC model completion' (oce_plugin_stub — the MEMFS cache write moves inside the gate too), 'ngspice request completion' and 'ngspice vector completion' (sharedspice_client — every HEAP32/HEAPF64/malloc write inside prepare, inertResult 1 = transport error). Every wxWasmBeginWait caller in the stubs bails on token <= 0. - deliberately NOT ported from codex: ownerModule, enqueueNativeCompletion, executionBarrier, the byte-credit native-entry FIFO — completions are one-shot per wait token and stream volume is bounded at the E-6 transport credit window. Cross-refs logged for group M (M-2/M-6/M-8). E-5 (re-implemented; codex shape kept, owner APIs replaced with the E-8 gate): js_ngspice_install_events binds the handler to the EXACT installing module (handler.__pcbjamNgspiceOwnerModule stamp; presence is not identity), re-installation replaces a foreign module's handler, a superseded handler disarms itself, native entry goes through installingModule._malloc/ ._pcbjam_ngspice_event (never lexical Module), each dispatch checks canTouchNative() (loud drop on a dead/terminal module), and a trap on the per-line entry latches the terminal gate. Tests: scheduler-shim.test.ts +7 (gate happy/stale/dead/terminal/cross-realm/ js-bug/beginWait-refusal). e2e specs updated from the codex line: occ-export decode-fault recovery (real onmessageerror transition via failDecode, J-4), ngspice-probe direct-service coverage, eeschema-sim rewritten onto the E-7 applied-generation receipt (codex's executionBarrier await replaced with a pendingWaits('ngspice') drain poll — the JSPI-line equivalent). Also bumps the kicad submodule to the E-7/E-9 commit (dd5751038f7). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 16:03:16 +02:00
finish( () => {
let cachePath = '';
if( res && res.ok && res.bytes && res.bytes.length )
{
cachePath = '/tmp/pcbjam_occ_model_cache.3dc';
FS.writeFile( cachePath, res.bytes );
}
else if( res && res.report )
{
console.error( '[pcbjam-occ] loadModel failed:', res.report );
}
return cachePath;
} );
} ).catch( ( e ) => {
findings(E-5,E-8,E-9): module-identity ngspice events + runWaitCompletion admission gate E-8 (re-implemented for JSPI — the codex gate is entangled with the dropped execution owner; under JSPI a fresh non-suspending JS→wasm entry while another activation is suspended is structurally safe on its own stack, so the admission boundary for worker completions is liveness + trap state, not execution ownership): - jspi-scheduler.js grows `terminal` (trapped instance; distinct from `dead`), canTouchNative(), _terminalizeNativeTrap() (WebAssembly.RuntimeError + cross-realm string classification), and runWaitCompletion(site, token, prepare, inertResult): prepare runs immediately and owns ALL native work; stale tokens and dead/terminal instances drop loudly without resolving (resolving would resume the parked frame inside the damaged module); a trap latches terminal; a plain JS bug resolves inertResult so the wait fails instead of stranding. beginWait refuses (token 0) when dead/terminal. - all four delayed completion sites route their native work through the gate: 'OCC export completion' (exporter_step_stub), 'OCC model completion' (oce_plugin_stub — the MEMFS cache write moves inside the gate too), 'ngspice request completion' and 'ngspice vector completion' (sharedspice_client — every HEAP32/HEAPF64/malloc write inside prepare, inertResult 1 = transport error). Every wxWasmBeginWait caller in the stubs bails on token <= 0. - deliberately NOT ported from codex: ownerModule, enqueueNativeCompletion, executionBarrier, the byte-credit native-entry FIFO — completions are one-shot per wait token and stream volume is bounded at the E-6 transport credit window. Cross-refs logged for group M (M-2/M-6/M-8). E-5 (re-implemented; codex shape kept, owner APIs replaced with the E-8 gate): js_ngspice_install_events binds the handler to the EXACT installing module (handler.__pcbjamNgspiceOwnerModule stamp; presence is not identity), re-installation replaces a foreign module's handler, a superseded handler disarms itself, native entry goes through installingModule._malloc/ ._pcbjam_ngspice_event (never lexical Module), each dispatch checks canTouchNative() (loud drop on a dead/terminal module), and a trap on the per-line entry latches the terminal gate. Tests: scheduler-shim.test.ts +7 (gate happy/stale/dead/terminal/cross-realm/ js-bug/beginWait-refusal). e2e specs updated from the codex line: occ-export decode-fault recovery (real onmessageerror transition via failDecode, J-4), ngspice-probe direct-service coverage, eeschema-sim rewritten onto the E-7 applied-generation receipt (codex's executionBarrier await replaced with a pendingWaits('ngspice') drain poll — the JSPI-line equivalent). Also bumps the kicad submodule to the E-7/E-9 commit (dd5751038f7). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 16:03:16 +02:00
// The gate makes this fallback inert after a trap or shutdown — it
// cannot repeat native work in a damaged instance.
console.error( '[pcbjam-occ] loadModel request failed:', e );
findings(E-5,E-8,E-9): module-identity ngspice events + runWaitCompletion admission gate E-8 (re-implemented for JSPI — the codex gate is entangled with the dropped execution owner; under JSPI a fresh non-suspending JS→wasm entry while another activation is suspended is structurally safe on its own stack, so the admission boundary for worker completions is liveness + trap state, not execution ownership): - jspi-scheduler.js grows `terminal` (trapped instance; distinct from `dead`), canTouchNative(), _terminalizeNativeTrap() (WebAssembly.RuntimeError + cross-realm string classification), and runWaitCompletion(site, token, prepare, inertResult): prepare runs immediately and owns ALL native work; stale tokens and dead/terminal instances drop loudly without resolving (resolving would resume the parked frame inside the damaged module); a trap latches terminal; a plain JS bug resolves inertResult so the wait fails instead of stranding. beginWait refuses (token 0) when dead/terminal. - all four delayed completion sites route their native work through the gate: 'OCC export completion' (exporter_step_stub), 'OCC model completion' (oce_plugin_stub — the MEMFS cache write moves inside the gate too), 'ngspice request completion' and 'ngspice vector completion' (sharedspice_client — every HEAP32/HEAPF64/malloc write inside prepare, inertResult 1 = transport error). Every wxWasmBeginWait caller in the stubs bails on token <= 0. - deliberately NOT ported from codex: ownerModule, enqueueNativeCompletion, executionBarrier, the byte-credit native-entry FIFO — completions are one-shot per wait token and stream volume is bounded at the E-6 transport credit window. Cross-refs logged for group M (M-2/M-6/M-8). E-5 (re-implemented; codex shape kept, owner APIs replaced with the E-8 gate): js_ngspice_install_events binds the handler to the EXACT installing module (handler.__pcbjamNgspiceOwnerModule stamp; presence is not identity), re-installation replaces a foreign module's handler, a superseded handler disarms itself, native entry goes through installingModule._malloc/ ._pcbjam_ngspice_event (never lexical Module), each dispatch checks canTouchNative() (loud drop on a dead/terminal module), and a trap on the per-line entry latches the terminal gate. Tests: scheduler-shim.test.ts +7 (gate happy/stale/dead/terminal/cross-realm/ js-bug/beginWait-refusal). e2e specs updated from the codex line: occ-export decode-fault recovery (real onmessageerror transition via failDecode, J-4), ngspice-probe direct-service coverage, eeschema-sim rewritten onto the E-7 applied-generation receipt (codex's executionBarrier await replaced with a pendingWaits('ngspice') drain poll — the JSPI-line equivalent). Also bumps the kicad submodule to the E-7/E-9 commit (dd5751038f7). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 16:03:16 +02:00
finish( () => '' );
} );
feat(wasm): occ-split — lazy occ_service worker; kicad_editor drops OCC (−31%) 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>
2026-07-03 12:39:58 +02:00
} )
// Token waits live in the wx wasm port (evtloop.cpp).
extern "C" int wxWasmBeginWait( const char* aKind );
extern "C" int wxWasmYieldUntil( int aToken );
feat(wasm): occ-split — lazy occ_service worker; kicad_editor drops OCC (−31%) 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>
2026-07-03 12:39:58 +02:00
extern "C"
{
// --- class handshake (include/plugins/3d/3d_plugin.h semantics) -------------
char const* oce3d_GetKicadPluginClass( void )
{
return "PLUGIN_3D";
}
void oce3d_GetClassVersion( unsigned char* Major, unsigned char* Minor, unsigned char* Patch,
unsigned char* Revision )
{
if( Major )
*Major = 1;
if( Minor )
*Minor = 0;
if( Patch )
*Patch = 0;
if( Revision )
*Revision = 0;
}
bool oce3d_CheckClassVersion( unsigned char Major, unsigned char, unsigned char, unsigned char )
{
return Major == 1;
}
// --- plugin identity (plugins/3d/oce/oce.cpp values) -------------------------
const char* oce3d_GetKicadPluginName( void )
{
return "PLUGIN_3D_OCE";
}
void oce3d_GetPluginVersion( unsigned char* Major, unsigned char* Minor, unsigned char* Patch,
unsigned char* Revision )
{
if( Major )
*Major = 1;
if( Minor )
*Minor = 4;
if( Patch )
*Patch = 2;
if( Revision )
*Revision = 0;
}
int oce3d_GetNExtensions( void )
{
return (int) k_extensions.size();
}
char const* oce3d_GetModelExtension( int aIndex )
{
if( aIndex < 0 || aIndex >= (int) k_extensions.size() )
return nullptr;
return k_extensions[aIndex].c_str();
}
int oce3d_GetNFilters( void )
{
return (int) k_filters.size();
}
char const* oce3d_GetFileFilter( int aIndex )
{
if( aIndex < 0 || aIndex >= (int) k_filters.size() )
return nullptr;
return k_filters[aIndex].c_str();
}
bool oce3d_CanRender( void )
{
return true;
}
SCENEGRAPH* oce3d_Load( char const* aFileName )
{
if( !aFileName )
return nullptr;
const int token = wxWasmBeginWait( "occ" );
findings(E-5,E-8,E-9): module-identity ngspice events + runWaitCompletion admission gate E-8 (re-implemented for JSPI — the codex gate is entangled with the dropped execution owner; under JSPI a fresh non-suspending JS→wasm entry while another activation is suspended is structurally safe on its own stack, so the admission boundary for worker completions is liveness + trap state, not execution ownership): - jspi-scheduler.js grows `terminal` (trapped instance; distinct from `dead`), canTouchNative(), _terminalizeNativeTrap() (WebAssembly.RuntimeError + cross-realm string classification), and runWaitCompletion(site, token, prepare, inertResult): prepare runs immediately and owns ALL native work; stale tokens and dead/terminal instances drop loudly without resolving (resolving would resume the parked frame inside the damaged module); a trap latches terminal; a plain JS bug resolves inertResult so the wait fails instead of stranding. beginWait refuses (token 0) when dead/terminal. - all four delayed completion sites route their native work through the gate: 'OCC export completion' (exporter_step_stub), 'OCC model completion' (oce_plugin_stub — the MEMFS cache write moves inside the gate too), 'ngspice request completion' and 'ngspice vector completion' (sharedspice_client — every HEAP32/HEAPF64/malloc write inside prepare, inertResult 1 = transport error). Every wxWasmBeginWait caller in the stubs bails on token <= 0. - deliberately NOT ported from codex: ownerModule, enqueueNativeCompletion, executionBarrier, the byte-credit native-entry FIFO — completions are one-shot per wait token and stream volume is bounded at the E-6 transport credit window. Cross-refs logged for group M (M-2/M-6/M-8). E-5 (re-implemented; codex shape kept, owner APIs replaced with the E-8 gate): js_ngspice_install_events binds the handler to the EXACT installing module (handler.__pcbjamNgspiceOwnerModule stamp; presence is not identity), re-installation replaces a foreign module's handler, a superseded handler disarms itself, native entry goes through installingModule._malloc/ ._pcbjam_ngspice_event (never lexical Module), each dispatch checks canTouchNative() (loud drop on a dead/terminal module), and a trap on the per-line entry latches the terminal gate. Tests: scheduler-shim.test.ts +7 (gate happy/stale/dead/terminal/cross-realm/ js-bug/beginWait-refusal). e2e specs updated from the codex line: occ-export decode-fault recovery (real onmessageerror transition via failDecode, J-4), ngspice-probe direct-service coverage, eeschema-sim rewritten onto the E-7 applied-generation receipt (codex's executionBarrier await replaced with a pendingWaits('ngspice') drain poll — the JSPI-line equivalent). Also bumps the kicad submodule to the E-7/E-9 commit (dd5751038f7). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 16:03:16 +02:00
// Token 0 = the scheduler refused the wait (dead or terminal instance).
if( token <= 0 )
return nullptr;
js_occLoadModelStart( token, aFileName );
// The malloc'd path pointer rides the wait as an int32.
char* cachePath = (char*) (uintptr_t) (uint32_t) wxWasmYieldUntil( token );
feat(wasm): occ-split — lazy occ_service worker; kicad_editor drops OCC (−31%) 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>
2026-07-03 12:39:58 +02:00
if( !cachePath || !*cachePath )
{
std::free( cachePath );
return nullptr;
}
// ReadCache returns the top-level SCENEGRAPH as its SGNODE base.
SGNODE* node = S3D::ReadCache( cachePath, nullptr, &acceptAnyCacheTag );
wxRemoveFile( wxString::FromUTF8( cachePath ) );
std::free( cachePath );
return reinterpret_cast<SCENEGRAPH*>( node );
}
} // extern "C"