Phase E complete: K4-K6 become token waits — the bridge set is converted, suite at baseline

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2SU74acXyviwSxBhMunFe
This commit is contained in:
Gergő Törcsvári 2026-08-08 16:44:53 +02:00
commit d6c4efcf1f
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
5 changed files with 190 additions and 86 deletions

View file

@ -53,16 +53,28 @@ extern "C" void Pcbjam_SetExportJobJson( const char* aJson )
s_nextJobJson = aJson ? aJson : "";
}
// Suspends pcbnew (Asyncify; the __asyncjs__* import is auto-covered by
// scripts/common/asyncify-imports.txt) while the worker exports. JS returns a
// malloc'd JSON string: { ok, report } — the download already happened there.
EM_ASYNC_JS( char*, js_occExportRequest,
( const char* aBoardPath, const char* aJobJson, const char* aFileName ),
// 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 Asyncify-parking 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 );
let res;
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
{
@ -70,27 +82,30 @@ EM_ASYNC_JS( char*, js_occExportRequest,
if( !hook || typeof hook.request !== 'function' )
{
res = { ok: false, report: 'occ_service provider not installed' };
req = Promise.resolve( { ok: false, report: 'occ_service provider not installed' } );
}
else
{
const board = FS.readFile( boardPath ); // Uint8Array copy — transferable
res = await hook.request( { kind: 'export', board, jobJson, fileName } );
req = Promise.resolve( hook.request( { kind: 'export', board, jobJson, fileName } ) );
}
}
catch( e )
{
console.error( '[pcbjam-occ] export request failed:', e );
res = { ok: false, report: 'occ_service request failed: ' + e };
req = Promise.resolve( { ok: false, report: 'occ_service request failed: ' + e } );
}
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 );
return p;
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 ) :
@ -152,8 +167,11 @@ bool EXPORTER_STEP::Export()
const wxString downloadName = wxFileName( m_outputFile ).GetFullName();
char* response = js_occExportRequest( TMP_BOARD, jobJson.c_str(),
downloadName.utf8_string().c_str() );
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;

View file

@ -58,13 +58,23 @@ bool acceptAnyCacheTag( const char*, void* )
} // namespace
// Suspends pcbnew while the worker parses + tessellates the model. JS returns
// a malloc'd path string: the scenegraph-cache file it wrote into this
// module's MEMFS ("" on failure).
EM_ASYNC_JS( char*, js_occLoadModelRequest, ( const char* aModelPath ),
// Phase E shape (docs/features/async/22 §5, K5): waits for the worker
// parse+tessellate via a token wait instead of Asyncify-parking 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 ),
{
const modelPath = UTF8ToString( aModelPath );
let cachePath = '';
const finish = ( cachePath ) => {
const n = lengthBytesUTF8( cachePath ) + 1;
const p = _malloc( n );
stringToUTF8( cachePath, p, n );
globalThis.__wxScheduler.resolveWait( aToken, p );
};
let req;
try
{
@ -73,37 +83,46 @@ EM_ASYNC_JS( char*, js_occLoadModelRequest, ( const char* aModelPath ),
if( !hook || typeof hook.request !== 'function' )
{
console.error( '[pcbjam-occ] loadModel: occ_service provider not installed' );
req = Promise.resolve( null );
}
else
{
const bytes = FS.readFile( modelPath ); // Uint8Array copy — transferable
const dot = modelPath.lastIndexOf( '.' );
const ext = dot >= 0 ? modelPath.slice( dot + 1 ) : 'step';
const res = await hook.request( { kind: 'loadModel', bytes, ext } );
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 );
}
req = Promise.resolve( hook.request( { kind: 'loadModel', bytes, ext } ) );
}
}
catch( e )
{
console.error( '[pcbjam-occ] loadModel request failed:', e );
cachePath = '';
req = Promise.resolve( null );
}
const n = lengthBytesUTF8( cachePath ) + 1;
const p = _malloc( n );
stringToUTF8( cachePath, p, n );
return p;
req.then( ( res ) => {
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 );
}
finish( cachePath );
} ).catch( ( e ) => {
console.error( '[pcbjam-occ] loadModel request failed:', e );
finish( '' );
} );
} )
// Token waits live in the wx wasm port (evtloop.cpp).
extern "C" int wxWasmBeginWait( const char* aKind );
extern "C" int wxWasmYieldUntil( int aToken );
extern "C"
{
@ -205,7 +224,11 @@ SCENEGRAPH* oce3d_Load( char const* aFileName )
if( !aFileName )
return nullptr;
char* cachePath = js_occLoadModelRequest( aFileName );
const int token = wxWasmBeginWait( "occ" );
js_occLoadModelStart( token, aFileName );
// The malloc'd path pointer rides the wait as an int32.
char* cachePath = (char*) (uintptr_t) (uint32_t) wxWasmYieldUntil( token );
if( !cachePath || !*cachePath )
{

View file

@ -56,69 +56,88 @@ using nlohmann::json;
// Generic request: JSON in, JSON out (malloc'd; caller frees). Vector data
// never travels this path — see js_ngspice_get_vec.
//
// Phase E shape (docs/features/async/22 §5, K6): token wait instead of an
// in-place Asyncify park; resolution ALWAYS deferred to at least a microtask
// (the early-resolve contract, doc 22 §10 Phase E retry entry).
// clang-format off
EM_ASYNC_JS( char*, js_ngspice_request, ( const char* aReqJson ), {
let res;
EM_JS( void, js_ngspice_request_start, ( int aToken, const char* aReqJson ), {
const finish = ( res ) => {
const s = JSON.stringify( res ?? {} );
const n = lengthBytesUTF8( s ) + 1;
const p = _malloc( n );
stringToUTF8( s, p, n );
globalThis.__wxScheduler.resolveWait( aToken, p );
};
let req;
try {
const svc = globalThis.ngspiceService;
if( !svc )
res = { error: 'ngspiceService provider not installed' };
req = Promise.resolve( { error: 'ngspiceService provider not installed' } );
else
res = await svc.request( JSON.parse( UTF8ToString( aReqJson ) ) );
req = Promise.resolve( svc.request( JSON.parse( UTF8ToString( aReqJson ) ) ) );
} catch( e ) {
res = { error: String( e ) };
req = Promise.resolve( { error: String( e ) } );
}
const s = JSON.stringify( res ?? {} );
const n = lengthBytesUTF8( s ) + 1;
const p = _malloc( n );
stringToUTF8( s, p, n );
return p;
req.then( finish ).catch( ( e ) => finish( { error: String( e ) } ) );
} );
// Vector fetch: fills editor-heap buffers directly (no JSON for MB arrays).
// aMeta: int[4] = { found, vtype, flags, length }; aReal/aComp receive
// malloc'd double buffers (comp interleaved re,im — the ngcomplex_t layout);
// aVName receives a malloc'd name string. Returns non-zero on transport error.
EM_ASYNC_JS( int, js_ngspice_get_vec,
( const char* aName, int* aMeta, double** aReal, double** aComp, char** aVName ), {
let res;
// aVName receives a malloc'd name string. The wait result is non-zero on
// transport error. All output writes happen in the resolve callback, BEFORE
// resolveWait — the parked caller reads them only after it resumes, the same
// ordering the in-place park had. Phase E shape, resolution always deferred.
EM_JS( void, js_ngspice_get_vec_start,
( int aToken, const char* aName, int* aMeta, double** aReal, double** aComp,
char** aVName ), {
const finish = ( status ) => globalThis.__wxScheduler.resolveWait( aToken, status );
let req;
try {
const svc = globalThis.ngspiceService;
res = svc ? await svc.request( { kind: 'get_vec_info', name: UTF8ToString( aName ) } )
: { error: 'ngspiceService provider not installed' };
req = svc ? Promise.resolve( svc.request( { kind: 'get_vec_info',
name: UTF8ToString( aName ) } ) )
: Promise.resolve( { error: 'ngspiceService provider not installed' } );
} catch( e ) {
res = { error: String( e ) };
req = Promise.resolve( { error: String( e ) } );
}
HEAP32[aMeta >> 2] = 0;
HEAPU32[aReal >> 2] = 0;
HEAPU32[aComp >> 2] = 0;
HEAPU32[aVName >> 2] = 0;
if( !res || res.error )
return 1;
if( !res.found )
return 0;
HEAP32[( aMeta >> 2 ) + 1] = res.vtype | 0;
HEAP32[( aMeta >> 2 ) + 2] = res.flags | 0;
HEAP32[( aMeta >> 2 ) + 3] = res.length | 0;
if( res.real && res.real.length ) {
const p = _malloc( res.real.length * 8 );
HEAPF64.set( res.real, p >> 3 );
HEAPU32[aReal >> 2] = p;
}
if( res.comp && res.comp.length ) {
const p = _malloc( res.comp.length * 8 );
HEAPF64.set( res.comp, p >> 3 );
HEAPU32[aComp >> 2] = p;
}
const s = res.vname || '';
const n = lengthBytesUTF8( s ) + 1;
const vp = _malloc( n );
stringToUTF8( s, vp, n );
HEAPU32[aVName >> 2] = vp;
HEAP32[aMeta >> 2] = 1;
return 0;
req.catch( ( e ) => ( { error: String( e ) } ) ).then( ( res ) => {
HEAP32[aMeta >> 2] = 0;
HEAPU32[aReal >> 2] = 0;
HEAPU32[aComp >> 2] = 0;
HEAPU32[aVName >> 2] = 0;
if( !res || res.error )
return finish( 1 );
if( !res.found )
return finish( 0 );
HEAP32[( aMeta >> 2 ) + 1] = res.vtype | 0;
HEAP32[( aMeta >> 2 ) + 2] = res.flags | 0;
HEAP32[( aMeta >> 2 ) + 3] = res.length | 0;
if( res.real && res.real.length ) {
const p = _malloc( res.real.length * 8 );
HEAPF64.set( res.real, p >> 3 );
HEAPU32[aReal >> 2] = p;
}
if( res.comp && res.comp.length ) {
const p = _malloc( res.comp.length * 8 );
HEAPF64.set( res.comp, p >> 3 );
HEAPU32[aComp >> 2] = p;
}
const s = res.vname || '';
const n = lengthBytesUTF8( s ) + 1;
const vp = _malloc( n );
stringToUTF8( s, vp, n );
HEAPU32[aVName >> 2] = vp;
HEAP32[aMeta >> 2] = 1;
finish( 0 );
} );
} );
// Token waits live in the wx wasm port (evtloop.cpp).
extern "C" int wxWasmBeginWait( const char* aKind );
extern "C" int wxWasmYieldUntil( int aToken );
// Event dispatcher: provider `{ evt }` frames -> KiCad's registered callbacks
// via the exported pcbjam_ngspice_event (fresh wasm entries; see header
// comment). Installed once, at first pcbjam_ngSpice_Init.
@ -166,7 +185,11 @@ std::atomic<bool> s_bgRunning{ false };
json rpc( const json& aReq )
{
char* raw = js_ngspice_request( aReq.dump().c_str() );
const int token = wxWasmBeginWait( "ngspice" );
js_ngspice_request_start( token, aReq.dump().c_str() );
// The malloc'd JSON pointer rides the wait as an int32.
char* raw = (char*) (uintptr_t) (uint32_t) wxWasmYieldUntil( token );
json res = json::parse( raw ? raw : "{}", nullptr, /* allow_exceptions */ false );
std::free( raw );
@ -408,7 +431,10 @@ pvector_info pcbjam_ngGet_Vec_Info( char* aVecName )
double* comp = nullptr;
char* vname = nullptr;
if( js_ngspice_get_vec( aVecName ? aVecName : "", meta, &real, &comp, &vname ) != 0 )
const int token = wxWasmBeginWait( "ngspice" );
js_ngspice_get_vec_start( token, aVecName ? aVecName : "", meta, &real, &comp, &vname );
if( wxWasmYieldUntil( token ) != 0 )
return nullptr;
if( !meta[0] )