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>
172 lines
5.2 KiB
C++
172 lines
5.2 KiB
C++
/**
|
|
* 3D-renderer WebGL test harness (WASM) — runs the shared scenarios
|
|
* (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.
|
|
*
|
|
* 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;
|
|
|
|
// 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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()
|
|
{
|
|
// 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;
|
|
}
|
|
|
|
std::printf( "[3d-webgl] context recreated\n" );
|
|
return 0;
|
|
}
|
|
|
|
} // extern "C"
|
|
|
|
|
|
int main()
|
|
{
|
|
if( !createContext() )
|
|
{
|
|
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;
|
|
}
|