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>
This commit is contained in:
parent
90bc4e0222
commit
3722891d48
13 changed files with 504 additions and 20 deletions
|
|
@ -55,6 +55,22 @@ untouched.
|
|||
the goldens document; tolerating it would *diverge* from native.
|
||||
- **Lighting is per-vertex (Gouraud)** to match fixed-function output;
|
||||
per-fragment lighting visibly mismatches speculars on coarse meshes.
|
||||
- **GL object caches are per-context, and the context is mortal.** Closing the
|
||||
3D viewer destroys its wxGLCanvas's WebGL context; reopening mints a new one
|
||||
in which the cached names (FFP program, stream/scratch VBOs) are invalid —
|
||||
every draw then dies with `INVALID_OPERATION` and the viewer is blank.
|
||||
`contextSync()` (gl1_state.cpp) detects the change in `programSync()` — the
|
||||
one choke point every shim draw crosses and a path the 2D GAL never reaches
|
||||
(a check in any `__wrap_*` would see the GAL's context and ping-pong the
|
||||
owner on 2D↔3D paint alternation) — and drops the caches so they rebuild
|
||||
lazily. Identity comes from a monotonic id stamped on Emscripten's
|
||||
per-context record — NOT the `EMSCRIPTEN_WEBGL_CONTEXT_HANDLE`, which
|
||||
Emscripten recycles (a destroy-then-create can return the same number).
|
||||
Display-list/immediate state is deliberately untouched: it is CPU-only, and
|
||||
the change can be detected mid-scene-rebuild (even inside `glNewList`).
|
||||
Known limit (pre-existing): two *simultaneously live* FFP contexts would
|
||||
thrash the caches on every alternation — the shim still assumes one live
|
||||
3D-viewer context at a time.
|
||||
|
||||
## Layout
|
||||
|
||||
|
|
|
|||
|
|
@ -217,6 +217,23 @@ void stateBlendFunc( GLenum sfactor, GLenum dfactor );
|
|||
void stateLineWidth( GLfloat width );
|
||||
void stateAlphaFunc( GLenum func, GLclampf ref );
|
||||
|
||||
// --- context-generation guard (gl1_state.cpp) ---
|
||||
// GL object names live and die with the WebGL context, and the 3D viewer's
|
||||
// close/reopen destroys and recreates it (~wxGLCanvas destroys the context
|
||||
// with the canvas; the next open mints new ones). Called from programSync()
|
||||
// ONLY: that is the single choke point every shim draw crosses, it always
|
||||
// runs under the 3D context, and — critically — it is a path the 2D GAL never
|
||||
// reaches, so alternating 2D/3D paints cannot ping-pong the owner (a check in
|
||||
// the glBindTexture wrap did exactly that: every caller crosses a wrap).
|
||||
// On a context change it drops every cached name so the shim rebuilds lazily
|
||||
// in the new context. Deliberately never touches display-list or
|
||||
// immediate-mode state: the change can be detected mid-scene-rebuild, and
|
||||
// those modules are context-agnostic CPU state.
|
||||
void contextSync();
|
||||
// Per-TU cache drops invoked by contextSync() on a context change.
|
||||
void shadersDropContextObjects(); // FFP program + uniform locations + fail latch
|
||||
void drawDropContextObjects(); // stream/scratch VBOs
|
||||
|
||||
// GL1 normalized-attribute rule: integer colors and normals are normalized,
|
||||
// floats are not (positions/texcoords are float-only in this codebase).
|
||||
bool attribNormalized( int arrayIndex, GLenum type );
|
||||
|
|
|
|||
|
|
@ -22,6 +22,15 @@ namespace gl1
|
|||
static GLuint s_streamVBO = 0; // immediate-mode interleaved stream
|
||||
static GLuint s_scratchVBO = 0; // client-array upload staging
|
||||
|
||||
|
||||
void drawDropContextObjects()
|
||||
{
|
||||
// The owning context is gone; the buffer names are invalid in the current
|
||||
// one. No glDeleteBuffers — just forget them so the draws re-gen lazily.
|
||||
s_streamVBO = 0;
|
||||
s_scratchVBO = 0;
|
||||
}
|
||||
|
||||
enum
|
||||
{
|
||||
ATTR_POSITION = 0,
|
||||
|
|
|
|||
|
|
@ -116,6 +116,13 @@ void __wrap_glDrawElements( GLenum mode, GLsizei count, GLenum type, const GLvoi
|
|||
|
||||
void __wrap_glBindTexture( GLenum target, GLuint texture )
|
||||
{
|
||||
// NO contextSync() here: this wrap intercepts EVERY caller, including the
|
||||
// 2D GAL binding its own textures under its own context — a check here
|
||||
// ping-pongs the owner on 2D<->3D paint alternation and thrash-rebuilds
|
||||
// the FFP program each flip (observed: 23 resets in one e2e run). The
|
||||
// boundTexture2D mirror this site feeds is write-only bookkeeping, so a
|
||||
// reset zeroing it after a new-context bind loses nothing.
|
||||
|
||||
if( dlistRecording() )
|
||||
{
|
||||
dlistRecordBindTexture( target, texture );
|
||||
|
|
|
|||
|
|
@ -301,6 +301,17 @@ static bool s_buildFailed = false;
|
|||
static ProgramLocs s_locs;
|
||||
|
||||
|
||||
void shadersDropContextObjects()
|
||||
{
|
||||
// The owning context is gone; the program name is invalid in the current
|
||||
// one. No glDeleteProgram — just forget it so programSync() rebuilds.
|
||||
// The fail latch resets too: a fresh context gets a fresh build attempt.
|
||||
s_program = 0;
|
||||
s_buildFailed = false;
|
||||
s_locs = ProgramLocs();
|
||||
}
|
||||
|
||||
|
||||
static GLuint compileShader( GLenum type, const char* source )
|
||||
{
|
||||
GLuint shader = glCreateShader( type );
|
||||
|
|
@ -476,6 +487,11 @@ static int encodeCombineFunc( GLenum func )
|
|||
|
||||
bool programSync()
|
||||
{
|
||||
// Every shim draw funnels through here, so this is the single choke point
|
||||
// where a recreated WebGL context (3D viewer close/reopen) gets detected
|
||||
// before any cached GL name is used.
|
||||
contextSync();
|
||||
|
||||
if( s_buildFailed )
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
#include "gl1_shim.h"
|
||||
|
||||
#include <emscripten.h>
|
||||
|
||||
namespace gl1
|
||||
{
|
||||
|
||||
|
|
@ -14,6 +16,66 @@ State& S()
|
|||
}
|
||||
|
||||
|
||||
// Identity of the current WebGL context, stable for the context's lifetime and
|
||||
// never reused. The EMSCRIPTEN_WEBGL_CONTEXT_HANDLE is NOT that: Emscripten
|
||||
// recycles freed handle slots, so the context created after a destroy can get
|
||||
// the very same numeric handle. Stamp a monotonic id on Emscripten's
|
||||
// per-context record (a fresh JS object per createContext) instead.
|
||||
static int currentContextId()
|
||||
{
|
||||
return EM_ASM_INT( {
|
||||
var ctx = ( typeof GL !== 'undefined' ) ? GL.currentContext : null;
|
||||
if( !ctx )
|
||||
return 0;
|
||||
if( !ctx.gl1ContextId )
|
||||
{
|
||||
GL.gl1NextContextId = ( GL.gl1NextContextId | 0 ) + 1;
|
||||
ctx.gl1ContextId = GL.gl1NextContextId;
|
||||
}
|
||||
return ctx.gl1ContextId;
|
||||
} );
|
||||
}
|
||||
|
||||
|
||||
// The context generation the shim's cached GL objects belong to. 0 until the
|
||||
// first contextSync() under a live context.
|
||||
static int s_ownerContext = 0;
|
||||
|
||||
void contextSync()
|
||||
{
|
||||
int cur = currentContextId();
|
||||
|
||||
if( cur == s_ownerContext || cur == 0 )
|
||||
return;
|
||||
|
||||
if( s_ownerContext != 0 )
|
||||
{
|
||||
// The context that owned the cached names is gone (3D viewer closed and
|
||||
// reopened). No glDelete*: the names are invalid in the current context
|
||||
// — forget them and let each module rebuild lazily.
|
||||
std::printf( "[gl1] WebGL context changed — dropping cached GL objects\n" );
|
||||
|
||||
shadersDropContextObjects();
|
||||
drawDropContextObjects();
|
||||
|
||||
State& s = S();
|
||||
s.boundTexture2D = 0;
|
||||
|
||||
for( int i = 0; i < CA_COUNT; ++i )
|
||||
s.clientArrays[i].boundBuffer = 0;
|
||||
|
||||
// The new program starts with default-initialized uniforms; force a
|
||||
// full re-upload on its first sync.
|
||||
s.matricesDirty = true;
|
||||
s.lightingDirty = true;
|
||||
s.texEnvDirty = true;
|
||||
s.miscDirty = true;
|
||||
}
|
||||
|
||||
s_ownerContext = cur;
|
||||
}
|
||||
|
||||
|
||||
bool* ffpCapSlot( GLenum cap )
|
||||
{
|
||||
State& s = S();
|
||||
|
|
|
|||
Loading…
Reference in a new issue