diagnostics: configurable --diag logging flags + asyncify setupUIConditions fix

- build-pcbnew.sh: add --diag=<gal,coroutine,ctor,all> -> -DKICAD_DIAG_*,
  off by default (forwarded by docker/build.sh)
- diagnostics.js: emit at console.log level (no longer error/warn); still
  gated by SHIM_DIAGNOSTICS=1
- apply-asyncify.sh: exclude PCB_EDIT_FRAME::setupUIConditions() from
  asyncify instrumentation (V8 cannot run the instrumented huge function
  on the rewound ctor stack -> Chrome startup stall; Firefox unaffected)
- DEBUG.md: reusable WASM/asyncify/browser debugging guide, diagnostic
  flag docs, and a production-build (release + -O2 asyncify) recipe
- tests: standalone coroutine vcall/gl repro probes
- bump kicad + wxwidgets submodules (diagnostic gating / debug cleanup)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Viktor Vaczi 2026-05-27 16:14:03 +02:00
commit 7331619404
10 changed files with 768 additions and 18 deletions

View file

@ -650,3 +650,45 @@ $(S)/coroutine-pthread/mainloop_repro.html: $(S)/coroutine-pthread/mainloop_repr
coroutine-pthread-mainloop: $(S)/coroutine-pthread/mainloop_repro.html
.PHONY: coroutine-pthread-mainloop
# WebGL2 + coroutine reproduction (no wx, no pthreads, default shell with #canvas)
LDFLAGS_COROUTINE_GL = $(DEBUG_LDFLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
-sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=65536 -sASYNCIFY_IMPORTS=['emscripten_fiber_swap'] \
-sDYNCALLS=1 -sMAX_WEBGL_VERSION=2 -sMIN_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['ccall']
$(S)/coroutine-pthread/gl_repro.o: $(S)/coroutine-pthread/gl_repro.cpp $(S)/coroutine/kicad_coroutine_harness.h
@mkdir -p $(S)/coroutine-pthread
$(CXX) -c $(CXXFLAGS) -I$(KICAD_ROOT)/thirdparty/libcontext -I$(S)/coroutine $< -o $@
$(S)/coroutine-pthread/gl_repro.html: $(S)/coroutine-pthread/gl_repro.o $(S)/coroutine/libcontext.o
$(CXX) $^ $(LDFLAGS_COROUTINE_GL) -o $@
../../scripts/common/inject-dyncall-shims.sh $(basename $@).js
coroutine-pthread-gl: $(S)/coroutine-pthread/gl_repro.html
.PHONY: coroutine-pthread-gl
# WebGL2 + coroutine + PTHREADS (the last untested combo: KiCad uses GL + pthreads together)
LDFLAGS_COROUTINE_GL_PTHREAD = $(LDFLAGS_COROUTINE_GL) -pthread \
-sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' -sPTHREAD_POOL_SIZE_STRICT=0
$(S)/coroutine-pthread/gl_repro_pt.o: $(S)/coroutine-pthread/gl_repro.cpp $(S)/coroutine/kicad_coroutine_harness.h
@mkdir -p $(S)/coroutine-pthread
$(CXX) -c $(CXXFLAGS) -pthread -I$(KICAD_ROOT)/thirdparty/libcontext -I$(S)/coroutine $< -o $@
$(S)/coroutine-pthread/gl_repro_pt.html: $(S)/coroutine-pthread/gl_repro_pt.o $(S)/coroutine-pthread/libcontext_pt.o
$(CXX) $^ $(LDFLAGS_COROUTINE_GL_PTHREAD) -o $@
../../scripts/common/inject-dyncall-shims.sh $(basename $@).js
coroutine-pthread-gl-pt: $(S)/coroutine-pthread/gl_repro_pt.html
.PHONY: coroutine-pthread-gl-pt
$(S)/coroutine-pthread/vcall_repro.o: $(S)/coroutine-pthread/vcall_repro.cpp $(S)/coroutine/kicad_coroutine_harness.h
@mkdir -p $(S)/coroutine-pthread
$(CXX) -c $(CXXFLAGS) -pthread -I$(KICAD_ROOT)/thirdparty/libcontext -I$(S)/coroutine $< -o $@
$(S)/coroutine-pthread/vcall_repro.html: $(S)/coroutine-pthread/vcall_repro.o $(S)/coroutine-pthread/libcontext_pt.o
$(CXX) $^ $(LDFLAGS_COROUTINE_PTHREAD_NOWX) -o $@
../../scripts/common/inject-dyncall-shims.sh $(basename $@).js
coroutine-pthread-vcall: $(S)/coroutine-pthread/vcall_repro.html
.PHONY: coroutine-pthread-vcall

View file

@ -0,0 +1,78 @@
// Reproduction probe #5: WebGL 2.0 + coroutine, the last untested KiCad factor.
//
// KiCad's GAL renders via WebGL 2.0 in the rAF refresh, and tool coroutines activate
// during the same refresh — so the Asyncify unwind/rewind happens MID-RENDER-FRAME with
// the GL context current. This probe creates a real WebGL-2.0 context and activates the
// coroutine between GL draw calls inside an emscripten_set_main_loop(rAF) frame, then the
// coroutine yields back -> main rewinds the render frame.
//
// No-wx (single-threaded first; GL+pthreads needs OFFSCREEN proxying — add later if this
// passes). Firefox should reach "[REPRO] DONE"; if system Chrome crashes before DONE, the
// WebGL x coroutine-rewind interaction is the missing factor.
#include "kicad_coroutine_harness.h"
#include <emscripten.h>
#include <emscripten/html5.h>
#include <GLES3/gl3.h>
#include <cstdio>
using coroutine_test::TestCoroutine;
static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE g_ctx = 0;
static int g_frame = 0;
static void run_coroutine()
{
TestCoroutine co( []( TestCoroutine& self ) {
std::printf( "[REPRO] coroutine body running (mid-GL-frame), about to yield\n" );
std::fflush( stdout );
self.Yield( 42 );
} );
bool running = co.Call( 1 ); // unwinds the render frame back to dynCall_v; yields back
std::printf( "[REPRO] after Call: running=%d lastValue=%ld\n",
(int) running, (long) co.LastReturnValue() );
std::fflush( stdout );
running = co.Resume( 2 );
std::printf( "[REPRO] after Resume: running=%d\n", (int) running );
std::fflush( stdout );
}
static void render_frame()
{
++g_frame;
glClearColor( 0.1f, 0.2f, 0.3f, 1.0f );
glClear( GL_COLOR_BUFFER_BIT ); // a real WebGL2 draw call before the coroutine
if( g_frame >= 2 )
{
std::printf( "[REPRO] frame %d: activating coroutine mid-GL-frame\n", g_frame );
std::fflush( stdout );
run_coroutine(); // coroutine yields -> Asyncify rewinds the render frame
glClearColor( 0.3f, 0.2f, 0.1f, 1.0f );
glClear( GL_COLOR_BUFFER_BIT ); // another GL call after the coroutine resumes
std::printf( "[REPRO] DONE\n" );
std::fflush( stdout );
emscripten_cancel_main_loop();
}
}
int main()
{
EmscriptenWebGLContextAttributes attrs;
emscripten_webgl_init_context_attributes( &attrs );
attrs.majorVersion = 2;
attrs.minorVersion = 0;
g_ctx = emscripten_webgl_create_context( "#canvas", &attrs );
emscripten_webgl_make_context_current( g_ctx );
std::printf( "[REPRO] start; WebGL2 context=%d\n", (int) g_ctx );
std::fflush( stdout );
emscripten_set_main_loop( render_frame, 0, 0 );
return 0;
}

View file

@ -0,0 +1,135 @@
// Reproduction probe #N for the KiCad Asyncify-fiber Chrome crash.
//
// Root-cause finding (DEBUG.md): the crash is the PCB_EDIT_FRAME ctor calling the
// VIRTUAL setupUIConditions() *after* the first tool coroutine (InvokeTool) has
// unwound+rewound the (deep) ctor stack via asyncify. That virtual call dispatches
// indirectly (-fexceptions) as: wasm -> invoke_vi(JS) -> instrumented dynCall_vi(JS)
// -> setupUIConditions. Chrome's V8 hard-crashes on it; Firefox tolerates it.
//
// nested_repro.cpp recreated the nested invoke_/dynCall chain + a coroutine yield and
// PASSED in both browsers. The factor it did NOT have: a NEW indirect "vi" call made
// from the rewound frame AFTER the coroutine round-trip. This probe adds exactly that.
//
// Shape (mirrors KiCad):
// main -> level(N) ... -> level(0) (deep stack via invoke_ try-hops)
// -> run_coroutine(): co.Call -> Yield -> co.Resume (asyncify unwind+rewind of main)
// -> THEN g_obj->setupConditions() (virtual, in try => invoke_vi -> dynCall_vi)
//
// Built no-wx + pthreads + -fexceptions + DYNCALLS + asyncify + the dyncall shim
// (LDFLAGS_COROUTINE_PTHREAD_NOWX). Firefox should reach "[REPRO] DONE"; if system
// Chrome crashes before DONE, we've reproduced the crash in isolation.
#include "kicad_coroutine_harness.h"
#include <emscripten.h>
#include <cstdint>
#include <cstdio>
using coroutine_test::TestCoroutine;
static const int kBoundaries = 20; // nested invoke_/dynCall JS<->wasm hops (KiCad had ~11)
typedef void ( *LevelFn )( int );
static LevelFn g_level = nullptr;
// Polymorphic hierarchy so the post-coroutine call is a genuine (non-devirtualizable)
// virtual dispatch => call_indirect signature "vi" (the `this` pointer) => invoke_vi ->
// dynCall_vi, exactly like PCB_EDIT_FRAME's virtual setupUIConditions().
struct Base
{
virtual void setupConditions() { std::printf( "[REPRO] Base::setupConditions\n" ); }
virtual ~Base() {}
};
struct Derived : Base
{
int m_n = 0;
void setupConditions() override
{
// Mimic setupUIConditions: a biggish body with calls + allocations.
volatile int s = 0;
for( int i = 0; i < 64; ++i )
s += i;
m_n = s;
std::printf( "[REPRO] Derived::setupConditions ran (n=%d)\n", m_n );
std::fflush( stdout );
}
};
// noinline factory returning a base pointer of a runtime-chosen type so the compiler
// cannot devirtualize the later g_obj->setupConditions() call.
static Base* makeObj( int seed ) __attribute__( ( noinline ) );
static Base* makeObj( int seed )
{
return ( seed & 1 ) ? static_cast<Base*>( new Derived() ) : new Base();
}
static Base* g_obj = nullptr;
static void run_coroutine()
{
// Mirror KiCad's TOOL_MANAGER pattern: RunMainStack (ContinueAfterRoot bounce) + Yield.
TestCoroutine co( []( TestCoroutine& self ) {
self.RunMainStack( []() {} );
self.Yield( 42 );
} );
bool running = co.Call( 1 ); // drives the bounce + unwinds main through the invoke_ chain
running = co.Resume( 2 ); // rewinds main + resumes
std::printf( "[REPRO] coroutine done running=%d\n", (int) running );
std::fflush( stdout );
// *** THE CRASH FACTOR ***
// Now (main stack just unwound+rewound) make a VIRTUAL call via invoke_vi ->
// instrumented dynCall_vi from this rewound frame — exactly what the PCB_EDIT_FRAME
// ctor does when it calls the virtual setupUIConditions() after InvokeTool.
try
{
g_obj->setupConditions();
}
catch( ... )
{
}
std::printf( "[REPRO] post-coroutine virtual call returned OK\n" );
std::fflush( stdout );
}
extern "C" EMSCRIPTEN_KEEPALIVE void level( int depth )
{
if( depth > 0 )
{
// Indirect call inside a try-region => invoke_vi wrapper => one nested
// asyncify-unwindable JS<->wasm boundary per hop (the KiCad shape).
try
{
g_level( depth - 1 );
}
catch( ... )
{
throw;
}
return;
}
run_coroutine(); // deepest hop
}
int main()
{
g_obj = makeObj( 1 ); // a Derived, but via a noinline factory (non-devirtualizable)
g_level = &level;
std::printf( "[REPRO] start, %d nested boundaries, then a virtual call after the coroutine\n",
kBoundaries );
std::fflush( stdout );
try
{
g_level( kBoundaries );
}
catch( ... )
{
}
std::printf( "[REPRO] DONE\n" );
std::fflush( stdout );
return 0;
}

View file

@ -95,4 +95,56 @@ test.describe('Coroutine pthread main() reproduction', () => {
'main loop should have run and activated the coroutine'
).toBe(true);
});
// Probe #6: WebGL 2.0 + coroutine activated mid-render-frame (KiCad's GAL render path).
test('WebGL2 + mid-frame fiber reaches DONE without renderer crash', async ({ page, testLogger }) => {
await page.goto('/standalone/coroutine-pthread/gl_repro.html');
await tryLoadApp(page, 20000).catch(() => {});
await expect
.poll(() => testLogger.consoleLogs.some((l) => l.includes('[REPRO] DONE')), {
timeout: 30000,
message: 'should reach [REPRO] DONE (rewind of a mid-GL-frame survived)',
})
.toBe(true);
expect(
testLogger.consoleLogs.some((l) => l.includes('WebGL2 context=')),
'a WebGL2 context should have been created'
).toBe(true);
});
// Probe #7: WebGL2 + coroutine mid-frame + PTHREADS (the GL x pthreads combo KiCad uses).
test('WebGL2 + pthreads mid-frame fiber reaches DONE without renderer crash', async ({ page, testLogger }) => {
await page.goto('/standalone/coroutine-pthread/gl_repro_pt.html');
await tryLoadApp(page, 25000).catch(() => {});
await expect
.poll(() => testLogger.consoleLogs.some((l) => l.includes('[REPRO] DONE')), {
timeout: 30000,
message: 'should reach [REPRO] DONE (GL + pthreads mid-frame rewind survived)',
})
.toBe(true);
});
// Probe #8: a VIRTUAL call via invoke_vi -> instrumented dynCall_vi made from the
// asyncify-rewound frame AFTER a coroutine round-trip. This is the exact factor the
// KiCad crash has that nested_repro lacked: PCB_EDIT_FRAME's ctor calls the virtual
// setupUIConditions() after InvokeTool's first coroutine unwinds/rewinds the ctor stack.
test('post-coroutine virtual call (invoke_vi->dynCall_vi) reaches DONE without renderer crash', async ({ page, testLogger }) => {
await page.goto('/standalone/coroutine-pthread/vcall_repro.html');
await tryLoadApp(page, 25000).catch(() => {});
await expect
.poll(() => testLogger.consoleLogs.some((l) => l.includes('[REPRO] DONE')), {
timeout: 30000,
message: 'should reach [REPRO] DONE (virtual call from the rewound frame survived)',
})
.toBe(true);
expect(
testLogger.consoleLogs.some((l) => l.includes('post-coroutine virtual call returned OK')),
'the post-coroutine virtual call should have completed'
).toBe(true);
});
});