2026-07-03 09:26:43 +02:00
|
|
|
/**
|
|
|
|
|
* 3D-renderer WebGL test harness (WASM) — runs the shared scenarios
|
feat(3d): gl1 shim M4+M6+M7 — GLU quadrics, production link, stub retired; 47/47 parity
- gl1_glu: gluCylinder/gluDisk/gluSphere per the SGI GLU reference
tessellation (sin/cos phase, strip emission order, pole fans — Apple's
GLU derives from the same source, so vertices match the goldens);
emission goes through the public glBegin/glVertex entry points so
quadrics record into display lists
- glLineWidth>1 one-time warning (browsers clamp to 1; affected coverage
stays under the parity floor — zero floors.json overrides needed)
- production link site: build-kicad-target.sh now compiles wasm/gl1 from
sources.txt and applies wrapped_symbols.txt --wrap flags (same manifests
as the test harness); kicad_editor's RENDER_3D_OPENGL links against the
real emulation layer instead of no-ops
- wasm/stubs/gl_ffp_stub.c deleted (role fully superseded; history in git)
- docs: 3d-regression README port-status section (floor-blind small
geometry + line-width caveats), harness comments de-red-stated
Parity: 47/47 under the 0.02 floor, zero overrides. Native golden path
byte-identical to bb0e238 (whose gate ran green) — scenarios/, native/,
baseline/, manifest, floors, spec all untouched by the port.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 11:56:19 +02:00
|
|
|
* (tests/3d-regression/scenarios/) in the browser. The legacy GL surface is
|
|
|
|
|
* implemented by the wasm/gl1 GL1->WebGL2 emulation layer; the parity level
|
|
|
|
|
* (npm run 3d:check:parity) compares these renders against the native goldens.
|
2026-07-03 09:26:43 +02:00
|
|
|
*
|
|
|
|
|
* Unlike the GAL harness this needs no wx window: the renderer draws into
|
|
|
|
|
* whatever context is current, so a direct emscripten WebGL2 context on
|
|
|
|
|
* #canvas is enough — created with the attributes the suite requires
|
|
|
|
|
* (stencil for DrawCulled, no MSAA to match the native single-sample FBO,
|
|
|
|
|
* preserveDrawingBuffer for Playwright canvas screenshots).
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <emscripten.h>
|
|
|
|
|
#include <emscripten/html5.h>
|
|
|
|
|
|
|
|
|
|
#include "scene3d_test_ctx.h"
|
|
|
|
|
#include "scene3d_test_scenarios.h"
|
|
|
|
|
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
|
|
|
|
|
static const int CAPTURE_WIDTH = 800; // must match manifest.json + native FBO
|
|
|
|
|
static const int CAPTURE_HEIGHT = 600;
|
|
|
|
|
|
|
|
|
|
static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE g_context = 0;
|
|
|
|
|
static SCENE3D_CTX* g_ctx = nullptr;
|
|
|
|
|
|
fix(3d): blank render + lost position after 3D viewer close/reopen (gl1 context guard)
Closing the viewer destroys its wxGLCanvas's WebGL context; reopening mints a
new one. The gl1 shim cached GL names (FFP program, stream/scratch VBOs) in
never-reset statics behind `if (!handle)` guards — in the new context every
draw died with INVALID_OPERATION and the viewer showed only the clear color
("Reload time 0.031 s" is benign: warm model caches make the rebuild fast).
contextSync() (gl1_state.cpp) now detects the context change in programSync()
— the one choke point every shim draw crosses, and a path the 2D GAL never
reaches (a first attempt checking in the glBindTexture wrap saw the GAL's
context and thrash-rebuilt the program 23x per run) — and drops the cached
names for lazy rebuild in the new context. Context identity is a monotonic id
stamped on Emscripten's per-context record: the numeric
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE is recycled, so a destroy-then-create can
return the same number and a handle comparison detects nothing.
The lost-position half is a wxwidgets wasm fix (pointer bump: GetFromWindow
reports display 0; saved geometry used to carry display=(unsigned)-1, which
LoadWindowState treats as "display not found" and re-centres the frame).
TDD (red observed before each fix, green after):
- tests/kicad/3d-viewer-reopen.spec.ts (new, own worker like the deadlock
spec): load board, open viewer, render-gate, drag by the titlebar, close
via the x, reopen; asserts the board re-renders (was: 1 distinct colour for
90 s) and the window position is restored (was: re-centred to 0,0 after
closing at 40,90). Green run logs exactly one [gl1] context-change line.
- 3d-regression harness: recreateContext() destroys the context AND swaps in
a fresh canvas element (a browser canvas keeps its context for life, so
same-element recreation hands back the live old context and hides the bug);
the new 3d-webgl spec test renders redraw-mini-board-navigator before and
after recreation and requires pixel-identical output. Parity: 47/47, zero
drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 15:55:31 +02:00
|
|
|
// One context-creation recipe for the initial context (main) and recreateContext():
|
|
|
|
|
// the recreated context must carry the exact attributes the suite requires, or a
|
|
|
|
|
// close/reopen render would differ from the goldens for attribute reasons alone.
|
|
|
|
|
static bool createContext()
|
|
|
|
|
{
|
|
|
|
|
EmscriptenWebGLContextAttributes attrs;
|
|
|
|
|
emscripten_webgl_init_context_attributes( &attrs );
|
|
|
|
|
|
|
|
|
|
attrs.majorVersion = 2;
|
|
|
|
|
attrs.minorVersion = 0;
|
|
|
|
|
attrs.alpha = false;
|
|
|
|
|
attrs.depth = true;
|
|
|
|
|
attrs.stencil = true; // DrawCulled hole subtraction
|
|
|
|
|
attrs.antialias = false; // native goldens are single-sample
|
|
|
|
|
attrs.preserveDrawingBuffer = true; // Playwright canvas.screenshot()
|
|
|
|
|
|
|
|
|
|
emscripten_set_canvas_element_size( "#canvas", CAPTURE_WIDTH, CAPTURE_HEIGHT );
|
|
|
|
|
|
|
|
|
|
g_context = emscripten_webgl_create_context( "#canvas", &attrs );
|
|
|
|
|
|
|
|
|
|
if( g_context <= 0 )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
emscripten_webgl_make_context_current( g_context );
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 09:26:43 +02:00
|
|
|
extern "C"
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
int getTotalScenarios()
|
|
|
|
|
{
|
|
|
|
|
return Scene3DTest::GetScenarioCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
const char* getScenarioName( int aIndex )
|
|
|
|
|
{
|
|
|
|
|
return Scene3DTest::GetScenarioName( aIndex );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
int getCanvasWidth()
|
|
|
|
|
{
|
|
|
|
|
return CAPTURE_WIDTH;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
int getCanvasHeight()
|
|
|
|
|
{
|
|
|
|
|
return CAPTURE_HEIGHT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
int runScenario( int aIndex )
|
|
|
|
|
{
|
|
|
|
|
if( aIndex < 0 || aIndex >= Scene3DTest::GetScenarioCount() )
|
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
|
|
if( emscripten_webgl_make_context_current( g_context ) != EMSCRIPTEN_RESULT_SUCCESS )
|
|
|
|
|
return -2;
|
|
|
|
|
|
|
|
|
|
if( !g_ctx )
|
|
|
|
|
{
|
|
|
|
|
g_ctx = new SCENE3D_CTX( CAPTURE_WIDTH, CAPTURE_HEIGHT );
|
|
|
|
|
g_ctx->InitOnce();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::printf( "[3d-webgl] scenario %d: %s\n", aIndex, Scene3DTest::GetScenarioName( aIndex ) );
|
|
|
|
|
|
|
|
|
|
// Start from a cleared frame; scenarios call BeginFrame themselves.
|
|
|
|
|
glViewport( 0, 0, CAPTURE_WIDTH, CAPTURE_HEIGHT );
|
|
|
|
|
glClearColor( 0.0f, 0.0f, 0.0f, 1.0f );
|
|
|
|
|
glClearDepth( 1.0f );
|
|
|
|
|
glClearStencil( 0 );
|
|
|
|
|
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT );
|
|
|
|
|
|
|
|
|
|
Scene3DTest::RenderScenario( *g_ctx, aIndex );
|
|
|
|
|
|
|
|
|
|
glFinish();
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
fix(3d): blank viewer after raytracing round-trip — owner-context FFP routing + VAO isolation (gl1)
Switching OpenGL → raytracing → OpenGL could leave the viewer showing only the
background gradient, with mid-session "[gl1] WebGL context changed" thrash and
INVALID_OPERATION storms on BOTH WebGL contexts. Traced mechanism: the shim's
FFP draw routing keyed on one process-global client-array flag; an interrupted
fixed-function window (MODEL_3D::BeginDrawMulti loops, or the switch-back
reload re-recording display lists with the GL context lock released across
JSPI suspensions) left it set, after which the raytracer blit's and the 2D
GAL's glDrawArrays were routed through the FFP pipeline — and one misrouted
draw permanently repointed the VICTIM's own VAO attributes at shim buffers
(the blit's attribute 0 collides with ATTR_POSITION), so both stayed
broken/blank even after the flag cleared.
Shim fixes (wasm/gl1):
- Owner-context routing gate: __wrap_glDrawArrays/Elements route into the FFP
pipeline only under the shim's owner context (adopted at the first FFP
client-state mutation or programSync in a context); foreign-context draws
always pass through — the 2D GAL can never be misrouted and the context
guard can never thrash.
- VAO isolation (ScopedDefaultVAO): draw executors do their attribute setup on
VAO 0 and restore the caller's binding — a misrouted draw can no longer
corrupt the caller.
- contextSync() resets the whole client-array mirror on a context change
(enables/pointers/VBO names all described the dead context).
kicad pointer bump (d6e3dc1a87a): blit preamble disables the four client
arrays (same-context firewall) + DoRePaint hidden-parent early return now
clears m_is_currently_painting like its six siblings (a standalone
sufficient cause of a permanently blank viewer).
TDD (each observed red before its fix, green after; harness = authoritative):
- T1 VAO corruption, T2 foreign-context routing + guard thrash, T3 stale
client-state surviving context recreation — tests/e2e/3d-webgl.spec.ts over
new harness choreography (appQuad/ffpMakeStale/createSecondContext/
quadDrawFresh). Parity stays 47/47 with zero drift.
- tests/kicad/3d-viewer-engine-toggle.spec.ts (new, CI-skipped like the
deadlock spec): real round-trip happy-path gate — board re-renders, zero
[gl1] lines, zero INVALID_OPERATION. (The raytraced image itself never
displays on the wasm build — the pre-existing inert-toggle KNOWN ISSUE in
3d-viewer-deadlock.spec.ts, out of scope here; the engine switch and the
poisoning reload path run regardless.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 11:24:26 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Engine-toggle regression scaffolding: model a "modern-GL consumer" (the
|
|
|
|
|
// raytracer blit / the 2D GAL) sharing the process with the FFP shim.
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// A minimal GLSL quad drawer with a PERSISTENT VAO (the victim). Mirrors the
|
|
|
|
|
// raytracer blit: attribute location 0 (colliding with the shim's
|
|
|
|
|
// ATTR_POSITION), own VBO, own program.
|
|
|
|
|
static GLuint g_appVAO = 0, g_appVBO = 0, g_appProg = 0;
|
|
|
|
|
|
|
|
|
|
static GLuint compileMini( GLenum type, const char* src )
|
|
|
|
|
{
|
|
|
|
|
GLuint s = glCreateShader( type );
|
|
|
|
|
glShaderSource( s, 1, &src, nullptr );
|
|
|
|
|
glCompileShader( s );
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extern "C"
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
// (Re)build the persistent app quad in the CURRENT context. Green full-screen
|
|
|
|
|
// quad on attribute 0. Returns 0 on success.
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
int appQuadInit()
|
|
|
|
|
{
|
|
|
|
|
static const char* VS = "#version 300 es\nlayout(location=0) in vec2 p;"
|
|
|
|
|
"void main(){ gl_Position = vec4(p,0.,1.); }";
|
|
|
|
|
static const char* FS = "#version 300 es\nprecision mediump float;"
|
|
|
|
|
"out vec4 c; void main(){ c = vec4(0.,1.,0.,1.); }";
|
|
|
|
|
|
|
|
|
|
GLuint vs = compileMini( GL_VERTEX_SHADER, VS );
|
|
|
|
|
GLuint fs = compileMini( GL_FRAGMENT_SHADER, FS );
|
|
|
|
|
GLuint prog = glCreateProgram();
|
|
|
|
|
glAttachShader( prog, vs );
|
|
|
|
|
glAttachShader( prog, fs );
|
|
|
|
|
glLinkProgram( prog );
|
|
|
|
|
glDeleteShader( vs );
|
|
|
|
|
glDeleteShader( fs );
|
|
|
|
|
|
|
|
|
|
GLint linked = GL_FALSE;
|
|
|
|
|
glGetProgramiv( prog, GL_LINK_STATUS, &linked );
|
|
|
|
|
if( !linked )
|
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
|
|
static const float quad[] = { -1.f, -1.f, 1.f, -1.f, -1.f, 1.f,
|
|
|
|
|
-1.f, 1.f, 1.f, -1.f, 1.f, 1.f };
|
|
|
|
|
g_appProg = prog;
|
|
|
|
|
glGenVertexArrays( 1, &g_appVAO );
|
|
|
|
|
glBindVertexArray( g_appVAO );
|
|
|
|
|
glGenBuffers( 1, &g_appVBO );
|
|
|
|
|
glBindBuffer( GL_ARRAY_BUFFER, g_appVBO );
|
|
|
|
|
glBufferData( GL_ARRAY_BUFFER, sizeof( quad ), quad, GL_STATIC_DRAW );
|
|
|
|
|
glEnableVertexAttribArray( 0 );
|
|
|
|
|
glVertexAttribPointer( 0, 2, GL_FLOAT, GL_FALSE, 0, nullptr );
|
|
|
|
|
glBindVertexArray( 0 );
|
|
|
|
|
glBindBuffer( GL_ARRAY_BUFFER, 0 );
|
|
|
|
|
return (int) glGetError();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw the persistent app quad exactly like the blit does: bind ITS VAO, its
|
|
|
|
|
// program, glDrawArrays. aClearFirst=1 clears to black before drawing so the
|
|
|
|
|
// framebuffer afterwards shows only this draw's output. Returns glGetError.
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
int appQuadDraw( int aClearFirst )
|
|
|
|
|
{
|
|
|
|
|
while( glGetError() != GL_NO_ERROR ) {}
|
|
|
|
|
if( aClearFirst )
|
|
|
|
|
{
|
|
|
|
|
glViewport( 0, 0, CAPTURE_WIDTH, CAPTURE_HEIGHT );
|
|
|
|
|
glClearColor( 0.f, 0.f, 0.f, 1.f );
|
|
|
|
|
glClear( GL_COLOR_BUFFER_BIT );
|
|
|
|
|
}
|
|
|
|
|
glUseProgram( g_appProg );
|
|
|
|
|
glBindVertexArray( g_appVAO );
|
|
|
|
|
glDrawArrays( GL_TRIANGLES, 0, 6 );
|
|
|
|
|
glBindVertexArray( 0 );
|
|
|
|
|
glUseProgram( 0 );
|
|
|
|
|
glFinish();
|
|
|
|
|
return (int) glGetError();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Leave the FFP mirror in the poisoned shape the engine switch can produce:
|
|
|
|
|
// GL_VERTEX_ARRAY enabled with a (small) VBO captured by glVertexPointer —
|
|
|
|
|
// the MODEL_3D::BeginDrawMulti window state.
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
void ffpMakeStale()
|
|
|
|
|
{
|
|
|
|
|
static GLuint tinyVBO = 0;
|
|
|
|
|
static const float tri[] = { 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 1.f, 0.f };
|
|
|
|
|
if( !tinyVBO )
|
|
|
|
|
{
|
|
|
|
|
glGenBuffers( 1, &tinyVBO );
|
|
|
|
|
glBindBuffer( GL_ARRAY_BUFFER, tinyVBO );
|
|
|
|
|
glBufferData( GL_ARRAY_BUFFER, sizeof( tri ), tri, GL_STATIC_DRAW );
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
glBindBuffer( GL_ARRAY_BUFFER, tinyVBO );
|
|
|
|
|
glVertexPointer( 3, GL_FLOAT, 0, nullptr ); // captures tinyVBO in the mirror
|
|
|
|
|
glEnableClientState( GL_VERTEX_ARRAY );
|
|
|
|
|
glBindBuffer( GL_ARRAY_BUFFER, 0 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
void ffpClearStale()
|
|
|
|
|
{
|
|
|
|
|
glDisableClientState( GL_VERTEX_ARRAY );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A second live WebGL context on its own canvas — the harness model of the 2D
|
|
|
|
|
// editor's GAL context coexisting with the 3D viewer's.
|
|
|
|
|
static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE g_context2 = 0;
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
int createSecondContext()
|
|
|
|
|
{
|
|
|
|
|
EM_ASM( {
|
|
|
|
|
if( !document.getElementById( "canvas2" ) )
|
|
|
|
|
{
|
|
|
|
|
var c = document.createElement( "canvas" );
|
|
|
|
|
c.id = "canvas2";
|
|
|
|
|
c.width = 320; c.height = 240;
|
|
|
|
|
document.body.appendChild( c );
|
|
|
|
|
if( typeof specialHTMLTargets !== 'undefined' )
|
|
|
|
|
specialHTMLTargets['#canvas2'] = c;
|
|
|
|
|
}
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
EmscriptenWebGLContextAttributes attrs;
|
|
|
|
|
emscripten_webgl_init_context_attributes( &attrs );
|
|
|
|
|
attrs.majorVersion = 2;
|
|
|
|
|
attrs.minorVersion = 0;
|
|
|
|
|
attrs.preserveDrawingBuffer = true;
|
|
|
|
|
g_context2 = emscripten_webgl_create_context( "#canvas2", &attrs );
|
|
|
|
|
return g_context2 > 0 ? 0 : -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Switch the current context: 1 = the main harness context, 2 = the second.
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
int useContext( int aWhich )
|
|
|
|
|
{
|
|
|
|
|
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE h = ( aWhich == 2 ) ? g_context2 : g_context;
|
|
|
|
|
if( h <= 0 )
|
|
|
|
|
return -1;
|
|
|
|
|
return emscripten_webgl_make_context_current( h ) == EMSCRIPTEN_RESULT_SUCCESS ? 0 : -2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build + draw + delete a fresh GLSL quad entirely in the CURRENT context (no
|
|
|
|
|
// persistent state — safe under any context). Returns glGetError after draw.
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
int quadDrawFresh()
|
|
|
|
|
{
|
|
|
|
|
GLuint savedVAO = g_appVAO, savedVBO = g_appVBO, savedProg = g_appProg;
|
|
|
|
|
g_appVAO = g_appVBO = g_appProg = 0;
|
|
|
|
|
int rc = appQuadInit();
|
|
|
|
|
if( rc == 0 )
|
|
|
|
|
rc = appQuadDraw( 1 );
|
|
|
|
|
glDeleteVertexArrays( 1, &g_appVAO );
|
|
|
|
|
glDeleteBuffers( 1, &g_appVBO );
|
|
|
|
|
glDeleteProgram( g_appProg );
|
|
|
|
|
g_appVAO = savedVAO; g_appVBO = savedVBO; g_appProg = savedProg;
|
|
|
|
|
return rc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // extern "C"
|
|
|
|
|
|
fix(3d): blank render + lost position after 3D viewer close/reopen (gl1 context guard)
Closing the viewer destroys its wxGLCanvas's WebGL context; reopening mints a
new one. The gl1 shim cached GL names (FFP program, stream/scratch VBOs) in
never-reset statics behind `if (!handle)` guards — in the new context every
draw died with INVALID_OPERATION and the viewer showed only the clear color
("Reload time 0.031 s" is benign: warm model caches make the rebuild fast).
contextSync() (gl1_state.cpp) now detects the context change in programSync()
— the one choke point every shim draw crosses, and a path the 2D GAL never
reaches (a first attempt checking in the glBindTexture wrap saw the GAL's
context and thrash-rebuilt the program 23x per run) — and drops the cached
names for lazy rebuild in the new context. Context identity is a monotonic id
stamped on Emscripten's per-context record: the numeric
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE is recycled, so a destroy-then-create can
return the same number and a handle comparison detects nothing.
The lost-position half is a wxwidgets wasm fix (pointer bump: GetFromWindow
reports display 0; saved geometry used to carry display=(unsigned)-1, which
LoadWindowState treats as "display not found" and re-centres the frame).
TDD (red observed before each fix, green after):
- tests/kicad/3d-viewer-reopen.spec.ts (new, own worker like the deadlock
spec): load board, open viewer, render-gate, drag by the titlebar, close
via the x, reopen; asserts the board re-renders (was: 1 distinct colour for
90 s) and the window position is restored (was: re-centred to 0,0 after
closing at 40,90). Green run logs exactly one [gl1] context-change line.
- 3d-regression harness: recreateContext() destroys the context AND swaps in
a fresh canvas element (a browser canvas keeps its context for life, so
same-element recreation hands back the live old context and hides the bug);
the new 3d-webgl spec test renders redraw-mini-board-navigator before and
after recreation and requires pixel-identical output. Parity: 47/47, zero
drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 15:55:31 +02:00
|
|
|
// Destroy the current WebGL context and mint a fresh one on a fresh canvas
|
|
|
|
|
// element — the harness model of the app's 3D-viewer close/reopen (~wxGLCanvas
|
|
|
|
|
// destroys its context AND its DOM canvas; a new frame creates new ones).
|
|
|
|
|
// The element swap is essential: a browser canvas keeps its WebGL context for
|
|
|
|
|
// life, so recreating on the SAME element would hand back the same live context
|
|
|
|
|
// and old GL object names would still work — hiding the very bug this models.
|
|
|
|
|
// Deliberately NO shim call here: the gl1 layer itself must detect the context
|
|
|
|
|
// change on its next draw and rebuild its cached GL objects, exactly as
|
|
|
|
|
// production code paths require.
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
int recreateContext()
|
2026-07-03 09:26:43 +02:00
|
|
|
{
|
fix(3d): blank render + lost position after 3D viewer close/reopen (gl1 context guard)
Closing the viewer destroys its wxGLCanvas's WebGL context; reopening mints a
new one. The gl1 shim cached GL names (FFP program, stream/scratch VBOs) in
never-reset statics behind `if (!handle)` guards — in the new context every
draw died with INVALID_OPERATION and the viewer showed only the clear color
("Reload time 0.031 s" is benign: warm model caches make the rebuild fast).
contextSync() (gl1_state.cpp) now detects the context change in programSync()
— the one choke point every shim draw crosses, and a path the 2D GAL never
reaches (a first attempt checking in the glBindTexture wrap saw the GAL's
context and thrash-rebuilt the program 23x per run) — and drops the cached
names for lazy rebuild in the new context. Context identity is a monotonic id
stamped on Emscripten's per-context record: the numeric
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE is recycled, so a destroy-then-create can
return the same number and a handle comparison detects nothing.
The lost-position half is a wxwidgets wasm fix (pointer bump: GetFromWindow
reports display 0; saved geometry used to carry display=(unsigned)-1, which
LoadWindowState treats as "display not found" and re-centres the frame).
TDD (red observed before each fix, green after):
- tests/kicad/3d-viewer-reopen.spec.ts (new, own worker like the deadlock
spec): load board, open viewer, render-gate, drag by the titlebar, close
via the x, reopen; asserts the board re-renders (was: 1 distinct colour for
90 s) and the window position is restored (was: re-centred to 0,0 after
closing at 40,90). Green run logs exactly one [gl1] context-change line.
- 3d-regression harness: recreateContext() destroys the context AND swaps in
a fresh canvas element (a browser canvas keeps its context for life, so
same-element recreation hands back the live old context and hides the bug);
the new 3d-webgl spec test renders redraw-mini-board-navigator before and
after recreation and requires pixel-identical output. Parity: 47/47, zero
drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 15:55:31 +02:00
|
|
|
// The scene ctx is rebuilt per context, like the app's per-canvas renderer.
|
|
|
|
|
// Delete it while the old context is still alive (mirrors releaseOpenGL()).
|
|
|
|
|
delete g_ctx;
|
|
|
|
|
g_ctx = nullptr;
|
|
|
|
|
|
|
|
|
|
if( g_context > 0 )
|
|
|
|
|
emscripten_webgl_destroy_context( g_context );
|
|
|
|
|
|
|
|
|
|
g_context = 0;
|
|
|
|
|
|
|
|
|
|
EM_ASM( {
|
|
|
|
|
var old = Module['canvas'];
|
|
|
|
|
var fresh = old.cloneNode( false ); // same id/width/height, no context yet
|
|
|
|
|
old.parentNode.replaceChild( fresh, old );
|
|
|
|
|
Module['canvas'] = fresh;
|
|
|
|
|
// '#canvas' resolves through specialHTMLTargets before querySelector.
|
|
|
|
|
if( typeof specialHTMLTargets !== 'undefined' )
|
|
|
|
|
specialHTMLTargets['#canvas'] = fresh;
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
if( !createContext() )
|
|
|
|
|
{
|
|
|
|
|
std::fprintf( stderr, "[3d-webgl] recreateContext: failed to create WebGL2 context\n" );
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
2026-07-03 09:26:43 +02:00
|
|
|
|
fix(3d): blank render + lost position after 3D viewer close/reopen (gl1 context guard)
Closing the viewer destroys its wxGLCanvas's WebGL context; reopening mints a
new one. The gl1 shim cached GL names (FFP program, stream/scratch VBOs) in
never-reset statics behind `if (!handle)` guards — in the new context every
draw died with INVALID_OPERATION and the viewer showed only the clear color
("Reload time 0.031 s" is benign: warm model caches make the rebuild fast).
contextSync() (gl1_state.cpp) now detects the context change in programSync()
— the one choke point every shim draw crosses, and a path the 2D GAL never
reaches (a first attempt checking in the glBindTexture wrap saw the GAL's
context and thrash-rebuilt the program 23x per run) — and drops the cached
names for lazy rebuild in the new context. Context identity is a monotonic id
stamped on Emscripten's per-context record: the numeric
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE is recycled, so a destroy-then-create can
return the same number and a handle comparison detects nothing.
The lost-position half is a wxwidgets wasm fix (pointer bump: GetFromWindow
reports display 0; saved geometry used to carry display=(unsigned)-1, which
LoadWindowState treats as "display not found" and re-centres the frame).
TDD (red observed before each fix, green after):
- tests/kicad/3d-viewer-reopen.spec.ts (new, own worker like the deadlock
spec): load board, open viewer, render-gate, drag by the titlebar, close
via the x, reopen; asserts the board re-renders (was: 1 distinct colour for
90 s) and the window position is restored (was: re-centred to 0,0 after
closing at 40,90). Green run logs exactly one [gl1] context-change line.
- 3d-regression harness: recreateContext() destroys the context AND swaps in
a fresh canvas element (a browser canvas keeps its context for life, so
same-element recreation hands back the live old context and hides the bug);
the new 3d-webgl spec test renders redraw-mini-board-navigator before and
after recreation and requires pixel-identical output. Parity: 47/47, zero
drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 15:55:31 +02:00
|
|
|
std::printf( "[3d-webgl] context recreated\n" );
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2026-07-03 09:26:43 +02:00
|
|
|
|
fix(3d): blank render + lost position after 3D viewer close/reopen (gl1 context guard)
Closing the viewer destroys its wxGLCanvas's WebGL context; reopening mints a
new one. The gl1 shim cached GL names (FFP program, stream/scratch VBOs) in
never-reset statics behind `if (!handle)` guards — in the new context every
draw died with INVALID_OPERATION and the viewer showed only the clear color
("Reload time 0.031 s" is benign: warm model caches make the rebuild fast).
contextSync() (gl1_state.cpp) now detects the context change in programSync()
— the one choke point every shim draw crosses, and a path the 2D GAL never
reaches (a first attempt checking in the glBindTexture wrap saw the GAL's
context and thrash-rebuilt the program 23x per run) — and drops the cached
names for lazy rebuild in the new context. Context identity is a monotonic id
stamped on Emscripten's per-context record: the numeric
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE is recycled, so a destroy-then-create can
return the same number and a handle comparison detects nothing.
The lost-position half is a wxwidgets wasm fix (pointer bump: GetFromWindow
reports display 0; saved geometry used to carry display=(unsigned)-1, which
LoadWindowState treats as "display not found" and re-centres the frame).
TDD (red observed before each fix, green after):
- tests/kicad/3d-viewer-reopen.spec.ts (new, own worker like the deadlock
spec): load board, open viewer, render-gate, drag by the titlebar, close
via the x, reopen; asserts the board re-renders (was: 1 distinct colour for
90 s) and the window position is restored (was: re-centred to 0,0 after
closing at 40,90). Green run logs exactly one [gl1] context-change line.
- 3d-regression harness: recreateContext() destroys the context AND swaps in
a fresh canvas element (a browser canvas keeps its context for life, so
same-element recreation hands back the live old context and hides the bug);
the new 3d-webgl spec test renders redraw-mini-board-navigator before and
after recreation and requires pixel-identical output. Parity: 47/47, zero
drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 15:55:31 +02:00
|
|
|
} // extern "C"
|
2026-07-03 09:26:43 +02:00
|
|
|
|
|
|
|
|
|
fix(3d): blank render + lost position after 3D viewer close/reopen (gl1 context guard)
Closing the viewer destroys its wxGLCanvas's WebGL context; reopening mints a
new one. The gl1 shim cached GL names (FFP program, stream/scratch VBOs) in
never-reset statics behind `if (!handle)` guards — in the new context every
draw died with INVALID_OPERATION and the viewer showed only the clear color
("Reload time 0.031 s" is benign: warm model caches make the rebuild fast).
contextSync() (gl1_state.cpp) now detects the context change in programSync()
— the one choke point every shim draw crosses, and a path the 2D GAL never
reaches (a first attempt checking in the glBindTexture wrap saw the GAL's
context and thrash-rebuilt the program 23x per run) — and drops the cached
names for lazy rebuild in the new context. Context identity is a monotonic id
stamped on Emscripten's per-context record: the numeric
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE is recycled, so a destroy-then-create can
return the same number and a handle comparison detects nothing.
The lost-position half is a wxwidgets wasm fix (pointer bump: GetFromWindow
reports display 0; saved geometry used to carry display=(unsigned)-1, which
LoadWindowState treats as "display not found" and re-centres the frame).
TDD (red observed before each fix, green after):
- tests/kicad/3d-viewer-reopen.spec.ts (new, own worker like the deadlock
spec): load board, open viewer, render-gate, drag by the titlebar, close
via the x, reopen; asserts the board re-renders (was: 1 distinct colour for
90 s) and the window position is restored (was: re-centred to 0,0 after
closing at 40,90). Green run logs exactly one [gl1] context-change line.
- 3d-regression harness: recreateContext() destroys the context AND swaps in
a fresh canvas element (a browser canvas keeps its context for life, so
same-element recreation hands back the live old context and hides the bug);
the new 3d-webgl spec test renders redraw-mini-board-navigator before and
after recreation and requires pixel-identical output. Parity: 47/47, zero
drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 15:55:31 +02:00
|
|
|
int main()
|
|
|
|
|
{
|
|
|
|
|
if( !createContext() )
|
2026-07-03 09:26:43 +02:00
|
|
|
{
|
|
|
|
|
std::fprintf( stderr, "[3d-webgl] failed to create WebGL2 context (%ld)\n",
|
|
|
|
|
(long) g_context );
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::printf( "[3d-webgl] ready: %d scenarios, %dx%d\n", Scene3DTest::GetScenarioCount(),
|
|
|
|
|
CAPTURE_WIDTH, CAPTURE_HEIGHT );
|
|
|
|
|
|
|
|
|
|
EM_ASM( { if( window._threeDTestReady ) window._threeDTestReady(); } );
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|