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
201 lines
6.3 KiB
C++
201 lines
6.3 KiB
C++
/*
|
|
* WASM shadow of EXPORTER_STEP — the editor build compiles this INSTEAD of
|
|
* exporters/step/exporter_step.cpp (no OCC in pcbnew.wasm; see
|
|
* docs/features/occ-split/README.md). The class layout comes from the real
|
|
* header; only the three symbols other TUs reference are defined here:
|
|
* constructor, destructor, Export().
|
|
*
|
|
* Export() bridges to the occ_service worker: stage the live BOARD as sexpr
|
|
* text in MEMFS, ship it + the official JOB_EXPORT_PCB_3D JSON through an
|
|
* EM_ASYNC_JS suspend (globalThis.occService.request, installed by the web
|
|
* app), and report the outcome. The exported file's bytes never enter this
|
|
* module — the JS provider hands them straight to the browser download path.
|
|
*
|
|
* Callers stay untouched: PCBNEW_JOBS_HANDLER::JobExportStep and the browser
|
|
* branch of DIALOG_EXPORT_STEP construct EXPORTER_STEP exactly as on desktop.
|
|
*/
|
|
|
|
#include <cstdlib>
|
|
#include <string>
|
|
|
|
#include <emscripten.h>
|
|
|
|
#include <wx/filename.h>
|
|
#include <wx/string.h>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <board.h>
|
|
#include <reporter.h>
|
|
#include <jobs/job_export_pcb_3d.h>
|
|
#include <pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h>
|
|
#include <exporters/step/exporter_step.h>
|
|
|
|
// Complete types for the unique_ptr members destroyed in ~EXPORTER_STEP (their
|
|
// headers still exist in the tree/sysroot; only the OCC *link* is gone).
|
|
#include <exporters/step/step_pcb_model.h>
|
|
#include <filename_resolver.h>
|
|
|
|
namespace
|
|
{
|
|
const char* const TMP_BOARD = "/tmp/pcbjam_occ_export_board.kicad_pcb";
|
|
|
|
// Optional side-channel: the dialog seam serializes the FULL job (including
|
|
// fields EXPORTER_STEP_PARAMS doesn't carry, e.g. the assembly variant) right
|
|
// before calling Export(). Consumed once. When unset (jobs-handler path), the
|
|
// job JSON is reconstructed from m_params.
|
|
std::string s_nextJobJson;
|
|
} // namespace
|
|
|
|
// One-shot job-JSON override for the next EXPORTER_STEP::Export() call.
|
|
extern "C" void Pcbjam_SetExportJobJson( const char* aJson )
|
|
{
|
|
s_nextJobJson = aJson ? aJson : "";
|
|
}
|
|
|
|
// Phase E shape (docs/features/async/22 §5, K4): waits for the worker export
|
|
// via a token wait (context park when the frame stands on a scheduler context)
|
|
// instead of suspending the stack in place. The wait result is a malloc'd
|
|
// JSON string: { ok, report } — the download already happened in JS.
|
|
// Every resolution defers to at least a microtask (the early-resolve
|
|
// contract, doc 22 §10 Phase E retry entry).
|
|
EM_JS( void, js_occExportStart,
|
|
( int aToken, const char* aBoardPath, const char* aJobJson, const char* aFileName ),
|
|
{
|
|
const boardPath = UTF8ToString( aBoardPath );
|
|
const jobJson = UTF8ToString( aJobJson );
|
|
const fileName = UTF8ToString( aFileName );
|
|
|
|
const finish = ( res ) => {
|
|
const s = JSON.stringify( res || { ok: false, report: 'occ_service: no response' } );
|
|
const n = lengthBytesUTF8( s ) + 1;
|
|
const p = _malloc( n );
|
|
stringToUTF8( s, p, n );
|
|
globalThis.__wxScheduler.resolveWait( aToken, p );
|
|
};
|
|
|
|
let req;
|
|
|
|
try
|
|
{
|
|
const hook = globalThis.occService;
|
|
|
|
if( !hook || typeof hook.request !== 'function' )
|
|
{
|
|
req = Promise.resolve( { ok: false, report: 'occ_service provider not installed' } );
|
|
}
|
|
else
|
|
{
|
|
const board = FS.readFile( boardPath ); // Uint8Array copy — transferable
|
|
req = Promise.resolve( hook.request( { kind: 'export', board, jobJson, fileName } ) );
|
|
}
|
|
}
|
|
catch( e )
|
|
{
|
|
console.error( '[pcbjam-occ] export request failed:', e );
|
|
req = Promise.resolve( { ok: false, report: 'occ_service request failed: ' + e } );
|
|
}
|
|
|
|
req.then( finish ).catch( ( e ) => {
|
|
console.error( '[pcbjam-occ] export request failed:', e );
|
|
finish( { ok: false, report: 'occ_service request failed: ' + e } );
|
|
} );
|
|
} )
|
|
|
|
// Token waits live in the wx wasm port (evtloop.cpp).
|
|
extern "C" int wxWasmBeginWait( const char* aKind );
|
|
extern "C" int wxWasmYieldUntil( int aToken );
|
|
|
|
|
|
EXPORTER_STEP::EXPORTER_STEP( BOARD* aBoard, const EXPORTER_STEP_PARAMS& aParams,
|
|
REPORTER* aReporter ) :
|
|
m_params( aParams ),
|
|
m_reporter( aReporter ),
|
|
m_board( aBoard ),
|
|
m_platingThickness( 0 )
|
|
{
|
|
}
|
|
|
|
|
|
EXPORTER_STEP::~EXPORTER_STEP()
|
|
{
|
|
}
|
|
|
|
|
|
bool EXPORTER_STEP::Export()
|
|
{
|
|
if( !m_board )
|
|
return false;
|
|
|
|
// Stage the LIVE board (unsaved edits included) as sexpr text in MEMFS.
|
|
try
|
|
{
|
|
PCB_IO_KICAD_SEXPR io;
|
|
io.SaveBoard( wxString::FromUTF8( TMP_BOARD ), m_board );
|
|
}
|
|
catch( const std::exception& e )
|
|
{
|
|
if( m_reporter )
|
|
{
|
|
m_reporter->Report( wxString::Format( wxT( "Failed to stage board for export: %s" ),
|
|
e.what() ),
|
|
RPT_SEVERITY_ERROR );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// The official job JSON. Prefer the seam-provided full job; otherwise
|
|
// rebuild one from the params we were constructed with.
|
|
std::string jobJson;
|
|
|
|
if( !s_nextJobJson.empty() )
|
|
{
|
|
jobJson = std::move( s_nextJobJson );
|
|
s_nextJobJson.clear();
|
|
}
|
|
else
|
|
{
|
|
JOB_EXPORT_PCB_3D job;
|
|
job.m_3dparams = m_params;
|
|
job.SetStepFormat( m_params.m_Format );
|
|
|
|
nlohmann::json j;
|
|
job.ToJson( j );
|
|
jobJson = j.dump();
|
|
}
|
|
|
|
const wxString downloadName = wxFileName( m_outputFile ).GetFullName();
|
|
|
|
const int token = wxWasmBeginWait( "occ" );
|
|
js_occExportStart( token, TMP_BOARD, jobJson.c_str(), downloadName.utf8_string().c_str() );
|
|
|
|
// The malloc'd JSON pointer rides the wait as an int32.
|
|
char* response = (char*) (uintptr_t) (uint32_t) wxWasmYieldUntil( token );
|
|
|
|
bool ok = false;
|
|
|
|
try
|
|
{
|
|
nlohmann::json res = nlohmann::json::parse( response ? response : "{}" );
|
|
ok = res.value( "ok", false );
|
|
|
|
const std::string report = res.value( "report", std::string() );
|
|
|
|
if( m_reporter && !report.empty() )
|
|
{
|
|
m_reporter->Report( wxString::FromUTF8( report.c_str() ),
|
|
ok ? RPT_SEVERITY_INFO : RPT_SEVERITY_ERROR );
|
|
}
|
|
}
|
|
catch( ... )
|
|
{
|
|
if( m_reporter )
|
|
m_reporter->Report( wxT( "occ_service: malformed response" ), RPT_SEVERITY_ERROR );
|
|
}
|
|
|
|
std::free( response );
|
|
wxRemoveFile( wxString::FromUTF8( TMP_BOARD ) );
|
|
|
|
return ok;
|
|
}
|