pcbjam/tests/apps/standalone/raytrace-modal/raytrace_modal_test.cpp
Viktor Vaczi c1ef489cfa feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on)
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>
2026-06-30 09:40:26 +02:00

269 lines
8.9 KiB
C++

/**
* raytrace_modal_test.cpp
*
* A raytracer-style worker-join run inside a wx modal pump, in both join styles.
*
* A pass is dispatched from a wxTimer that fires while a ShowModal() dialog is open. The modal pump
* runs ProcessEvents via ccall(async:true), so the timer handler runs in a fresh managed Asyncify
* context at state == Normal (the handler probes and logs the state). Both join styles complete
* multi-core there:
* ?m=0 busywait : the join is a sleep_for() busy-wait; the pre-warmed pool's workers complete it.
* ?m=1 yield : the join yields via emscripten_sleep; legal at state == Normal, so it suspends
* and resumes normally.
*
* The persistent pool is warmed in OnInit (on the free top-level slot) so the workers are alive
* before the modal opens, which lets the in-modal busy-wait complete without on-demand Worker spawn.
*/
#include "wx/wx.h"
#include <atomic>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include <cstdarg>
#include <cstdio>
#include <mutex>
#include <thread>
#include <vector>
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#endif
using clk = std::chrono::steady_clock;
static void rtlog( const char* fmt, ... )
{
char buf[512];
va_list ap;
va_start( ap, fmt );
vsnprintf( buf, sizeof( buf ), fmt, ap );
va_end( ap );
#ifdef __EMSCRIPTEN__
EM_ASM( { console.log( UTF8ToString( $0 ) ); }, buf );
#else
printf( "%s\n", buf );
#endif
}
static int readMode()
{
#ifdef __EMSCRIPTEN__
return EM_ASM_INT( {
var raw = location.search ? location.search.slice( 1 ) : location.hash.slice( 1 );
var v = parseInt( new URLSearchParams( raw ).get( 'm' ), 10 );
return isNaN( v ) ? 0 : v;
} );
#else
return 0;
#endif
}
static long ms( clk::time_point t0 )
{
return (long) std::chrono::duration_cast<std::chrono::milliseconds>( clk::now() - t0 ).count();
}
// ---------------------------------------------------------------------------
// shared raytracer-style work (atomic work-stealing; mirrors render_3d_raytrace_base.cpp)
// ---------------------------------------------------------------------------
static std::atomic<size_t> g_nextBlock{ 0 };
static std::atomic<int> g_workersRan{ 0 };
static std::atomic<double> g_sink{ 0.0 };
static int g_numBlocks = 48;
static long g_iters = 600000;
static double computeBlock( int b, long iters )
{
double acc = 0.0;
for( long i = 0; i < iters; ++i )
acc += std::sin( (double) ( b * 131 + i ) * 1e-6 ) * std::cos( (double) i * 1e-7 );
return acc;
}
static void workerBody()
{
bool counted = false;
for( size_t b = g_nextBlock.fetch_add( 1 ); b < (size_t) g_numBlocks; b = g_nextBlock.fetch_add( 1 ) )
{
if( !counted )
{
g_workersRan.fetch_add( 1 );
counted = true;
}
g_sink.store( g_sink.load( std::memory_order_relaxed ) + computeBlock( (int) b, g_iters ),
std::memory_order_relaxed );
}
}
// ---------------------------------------------------------------------------
// persistent pre-warmed pool (warmed in OnInit, free slot) — so the IN-MODAL busy-wait completes
// rather than deadlocking on on-demand Worker creation.
// ---------------------------------------------------------------------------
struct PersistentPool
{
int n = 0;
std::vector<std::thread> ts;
std::mutex mtx;
std::condition_variable cv;
unsigned long generation = 0;
bool stop = false;
std::atomic<size_t> finished{ 0 };
std::atomic<size_t> started{ 0 };
void start( int n_ )
{
n = n_;
for( int i = 0; i < n; ++i )
ts.emplace_back( [this] { loop(); } );
}
bool ready() const { return started.load() == (size_t) n; }
void loop()
{
started.fetch_add( 1 );
unsigned long seen = 0;
for( ;; )
{
std::unique_lock<std::mutex> lk( mtx );
cv.wait( lk, [&] { return stop || generation != seen; } );
if( stop )
return;
seen = generation;
lk.unlock();
workerBody();
finished.fetch_add( 1 );
}
}
// Called on the MAIN thread, from the modal pump's ProcessEvents (state == Normal).
void runPass( bool yieldJoin )
{
finished.store( 0 );
g_nextBlock.store( 0 );
g_workersRan.store( 0 );
{
std::lock_guard<std::mutex> lk( mtx );
++generation;
}
cv.notify_all();
while( finished.load() < (size_t) n )
{
if( yieldJoin )
#ifdef __EMSCRIPTEN__
emscripten_sleep( 10 ); // Asyncify yield (legal here — state == Normal)
#else
std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) );
#endif
else
std::this_thread::sleep_for( std::chrono::microseconds( 200 ) ); // busy-wait join
}
}
};
static PersistentPool g_pool;
// ---------------------------------------------------------------------------
// modal scaffold (mirrors coroutine-nested/nested_test.cpp's AutoClosingDialog)
// ---------------------------------------------------------------------------
static constexpr int ID_MODAL_CLOSE = wxID_HIGHEST + 800;
static constexpr int ID_SCENARIO = wxID_HIGHEST + 801;
static constexpr int ID_MODAL_WORK = wxID_HIGHEST + 802;
class AutoClosingDialog : public wxDialog
{
public:
AutoClosingDialog( wxWindow* parent ) :
wxDialog( parent, wxID_ANY, "rt-modal", wxDefaultPosition, wxSize( 280, 120 ) ) {}
void EndModalExternal( int code ) { EndModal( code ); }
};
class RtModalFrame : public wxFrame
{
public:
RtModalFrame( int mode ) :
wxFrame( nullptr, wxID_ANY, "Raytrace Modal Test", wxDefaultPosition, wxSize( 420, 140 ) ),
m_mode( mode ),
m_scenarioTimer( this, ID_SCENARIO ),
m_modalWorkTimer( this, ID_MODAL_WORK )
{
wxPanel* p = new wxPanel( this );
wxBoxSizer* s = new wxBoxSizer( wxVERTICAL );
s->Add( new wxStaticText( p, wxID_ANY, "Raytrace-in-modal repro — see console." ), 0, wxALL, 16 );
p->SetSizer( s );
Bind( wxEVT_TIMER, &RtModalFrame::OnScenario, this, ID_SCENARIO );
Bind( wxEVT_TIMER, &RtModalFrame::OnModalWork, this, ID_MODAL_WORK );
}
void armScenario() { m_scenarioTimer.StartOnce( 50 ); }
private:
// In the main loop: arm the modal-work timer BEFORE ShowModal so it fires from within the modal
// pump, then show the modal.
void OnScenario( wxTimerEvent& )
{
AutoClosingDialog* dlg = new AutoClosingDialog( this );
m_activeDialog = dlg;
m_modalWorkTimer.StartOnce( 30 );
rtlog( "[RTPOOL] showing modal (mode=%d) — work runs from the modal pump's ProcessEvents", m_mode );
dlg->ShowModal(); // returns when OnModalWork's pass finishes and closes it
m_activeDialog = nullptr;
rtlog( "[RTPOOL] SUCCESS mode=%d workersRan=%d totalMs=%ld",
m_mode, g_workersRan.load(), ms( m_t0 ) );
dlg->Destroy();
}
// Fires from the modal pump's ProcessEvents — a fresh managed entry at Asyncify state == Normal.
void OnModalWork( wxTimerEvent& )
{
#ifdef __EMSCRIPTEN__
int st = EM_ASM_INT( {
return ( typeof Asyncify !== 'undefined' && Asyncify.state !== undefined ) ? Asyncify.state : -1;
} );
rtlog( "[RTPOOL] modal-work: Asyncify.state=%d (0=Normal,1=Unwinding,2=Rewinding) mode=%d", st, m_mode );
#endif
rtlog( "[RTPOOL] modal-work: running pass from the modal pump (mode=%d)", m_mode );
m_t0 = clk::now();
g_pool.runPass( m_mode == 1 ); // m=1 yields via emscripten_sleep; m=0 busy-waits
rtlog( "[RTPOOL] PASS done workersRan=%d", g_workersRan.load() );
if( m_activeDialog )
m_activeDialog->EndModalExternal( wxID_OK );
}
int m_mode;
AutoClosingDialog* m_activeDialog = nullptr;
clk::time_point m_t0;
wxTimer m_scenarioTimer;
wxTimer m_modalWorkTimer;
};
class RtModalApp : public wxApp
{
public:
bool OnInit() override
{
const int mode = readMode();
const int hwc = std::max( 1u, std::thread::hardware_concurrency() );
rtlog( "[RTPOOL] START mode=%d hwc=%d", mode, hwc );
// Warm the persistent pool on the FREE top-level slot (emscripten_sleep is legal here), so
// the in-modal join runs against already-alive workers.
g_pool.start( hwc );
while( !g_pool.ready() )
#ifdef __EMSCRIPTEN__
emscripten_sleep( 5 );
#else
std::this_thread::sleep_for( std::chrono::milliseconds( 5 ) );
#endif
rtlog( "[RTPOOL] pool warmed (%d workers)", hwc );
RtModalFrame* f = new RtModalFrame( mode );
f->Show();
f->armScenario();
return true;
}
};
wxIMPLEMENT_APP( RtModalApp );