From 18c73c355ed3df5d1711ebaa2338d45942b1adab Mon Sep 17 00:00:00 2001 From: Viktor Vaczi Date: Thu, 8 Jan 2026 10:13:11 +0100 Subject: [PATCH] feat(webgl): Fix WebGL GAL rendering with proper coordinate setup - Fix coordinate system for 1:1 pixel mapping (was using nm scale) - Set worldUnitLength to 1/96 to match native test setup - Add SetLookAtPoint and SetZoomFactor for proper view transformation - Fix glDrawBuffers for WebGL 2.0 (array index must match attachment) - Add legacy_gl_stubs.js for wxWidgets compatibility - Add isReady() check to prevent race condition in Playwright tests All 28 GAL test scenarios now render correctly in WebGL. Co-Authored-By: Claude Opus 4.5 --- tests/apps/gal-webgl/gal_webgl_test.html | 25 +++-- tests/e2e/gal-webgl.spec.ts | 2 +- tests/gal-regression/wasm/Makefile | 11 ++- tests/gal-regression/wasm/gal_webgl_test.cpp | 21 +++-- tests/gal-regression/wasm/gal_webgl_test.html | 25 +++-- tests/gal-regression/wasm/legacy_gl_stubs.js | 91 +++++++++++++++++++ .../wasm/webgl/webgl_compositor.cpp | 15 ++- tests/gal-regression/wasm/webgl/webgl_gal.cpp | 2 +- 8 files changed, 165 insertions(+), 27 deletions(-) create mode 100644 tests/gal-regression/wasm/legacy_gl_stubs.js diff --git a/tests/apps/gal-webgl/gal_webgl_test.html b/tests/apps/gal-webgl/gal_webgl_test.html index a0d3013..db9ebf3 100644 --- a/tests/apps/gal-webgl/gal_webgl_test.html +++ b/tests/apps/gal-webgl/gal_webgl_test.html @@ -129,13 +129,23 @@ // Run a single scenario function runScenario(index) { - if (!wasmModule) return; + console.log('[DEBUG JS] runScenario called with index:', index, 'wasmModule:', !!wasmModule); + if (!wasmModule) { + console.log('[DEBUG JS] wasmModule is null, returning early'); + return; + } - const result = wasmModule.ccall('runScenario', 'number', ['number'], [index]); - if (result === 0) { - setStatus(`Rendered: ${SCENARIO_NAMES[index]}`); - } else { - setStatus(`ERROR: Failed scenario ${index}`); + try { + console.log('[DEBUG JS] Calling ccall...'); + const result = wasmModule.ccall('runScenario', 'number', ['number'], [index]); + console.log('[DEBUG JS] ccall returned:', result); + if (result === 0) { + setStatus(`Rendered: ${SCENARIO_NAMES[index]}`); + } else { + setStatus(`ERROR: Failed scenario ${index}`); + } + } catch (e) { + console.error('[DEBUG JS] ccall threw error:', e); } } @@ -182,7 +192,8 @@ runAllScenarios, getScenarioName: (index) => SCENARIO_NAMES[index], getTotalScenarios: () => SCENARIO_NAMES.length, - getGLCanvas: getGLCanvas + getGLCanvas: getGLCanvas, + isReady: () => wasmModule !== null }; // Create canvas immediately (before module loads) diff --git a/tests/e2e/gal-webgl.spec.ts b/tests/e2e/gal-webgl.spec.ts index d3ad219..b704295 100644 --- a/tests/e2e/gal-webgl.spec.ts +++ b/tests/e2e/gal-webgl.spec.ts @@ -94,7 +94,7 @@ test.describe('GAL WebGL Regression Tests', () => { // Wait for module to be ready await page.waitForFunction(() => { - return (window as any).galTest !== undefined; + return (window as any).galTest !== undefined && (window as any).galTest.isReady(); }, { timeout: 60000 }); // Run the scenario diff --git a/tests/gal-regression/wasm/Makefile b/tests/gal-regression/wasm/Makefile index a6af6e0..43f4497 100644 --- a/tests/gal-regression/wasm/Makefile +++ b/tests/gal-regression/wasm/Makefile @@ -1,7 +1,7 @@ # Makefile for GAL WebGL Test (WASM) # # Builds WEBGL_GAL (ported from OPENGL_GAL) using wxWidgets WASM build. -# Uses LEGACY_GL_EMULATION for legacy OpenGL compatibility. +# Uses pure WebGL 2.0 / OpenGL ES 3.0 (no LEGACY_GL_EMULATION). # # Usage: # make # Build @@ -72,7 +72,14 @@ BASE_LDFLAGS = -sALLOW_MEMORY_GROWTH=1 \ # No LEGACY_GL_EMULATION - WEBGL_GAL uses VBOs and custom shaders EM_GL_FLAGS = -sFULL_ES3=1 -sMAX_WEBGL_VERSION=2 -LDFLAGS = $(DEBUG_LDFLAGS) $(BASE_LDFLAGS) $(EM_GL_FLAGS) $(WX_LDFLAGS) +# Legacy GL stubs for wxWidgets compatibility +# wxWidgets GL library may reference legacy functions at runtime, but actual +# rendering uses WEBGL_GAL which only uses modern WebGL 2.0 APIs +# Use absolute path to ensure the library is found +MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +LEGACY_GL_STUBS = --js-library=$(MAKEFILE_DIR)legacy_gl_stubs.js + +LDFLAGS = $(DEBUG_LDFLAGS) $(BASE_LDFLAGS) $(EM_GL_FLAGS) $(LEGACY_GL_STUBS) $(WX_LDFLAGS) # Templates JS = $(TOOLS_ROOT)/wx.js diff --git a/tests/gal-regression/wasm/gal_webgl_test.cpp b/tests/gal-regression/wasm/gal_webgl_test.cpp index fd95600..504c1c3 100644 --- a/tests/gal-regression/wasm/gal_webgl_test.cpp +++ b/tests/gal-regression/wasm/gal_webgl_test.cpp @@ -79,6 +79,9 @@ extern "C" { EMSCRIPTEN_KEEPALIVE int runScenario(int scenarioIndex) { + printf("[DEBUG] runScenario called with index %d, g_gal=%p\n", + scenarioIndex, (void*)g_gal); + if (scenarioIndex < 0 || scenarioIndex >= g_totalScenarios) { printf("ERROR: Invalid scenario index %d (valid: 0-%d)\n", scenarioIndex, g_totalScenarios - 1); @@ -163,16 +166,20 @@ bool GALTestApp::OnInit() { // Set up the GAL g_gal->SetScreenSize(VECTOR2I(CANVAS_WIDTH, CANVAS_HEIGHT)); g_gal->ResizeScreen(CANVAS_WIDTH, CANVAS_HEIGHT); + g_gal->SetClearColor(KIGFX::COLOR4D(0.1, 0.1, 0.15, 1.0)); - // Set default world coordinates (matching native test) - double worldScale = 1.0 / 10000.0; // Convert nm to screen units - g_gal->SetWorldUnitLength(worldScale); + // CRITICAL: Set worldUnitLength for 1:1 world-to-screen coordinate mapping + // GAL default worldUnitLength is for PCB nanometers, which would compress + // our pixel-scale coordinates (0-800) to tiny values! + // With screenDPI=96 and zoomFactor=1.0, worldUnitLength should be 1/96 g_gal->SetScreenDPI(96); + g_gal->SetWorldUnitLength(1.0 / 96.0); - // Set world boundaries - VECTOR2D worldSize(CANVAS_WIDTH / worldScale, CANVAS_HEIGHT / worldScale); - BOX2D worldBounds(VECTOR2D(0, 0), worldSize); - g_gal->SetWorldScreenMatrix(g_gal->GetWorldScreenMatrix()); + // Set up coordinate transformation for 1:1 world-to-screen mapping + // LookAtPoint should be at center, ZoomFactor of 1.0 gives 1:1 mapping + g_gal->SetLookAtPoint(VECTOR2D(CANVAS_WIDTH / 2.0, CANVAS_HEIGHT / 2.0)); + g_gal->SetZoomFactor(1.0); + g_gal->ComputeWorldScreenMatrix(); // Initialize the compositor with proper context locking g_gal->LockContext(-1); // Lock with init ID diff --git a/tests/gal-regression/wasm/gal_webgl_test.html b/tests/gal-regression/wasm/gal_webgl_test.html index a0d3013..db9ebf3 100644 --- a/tests/gal-regression/wasm/gal_webgl_test.html +++ b/tests/gal-regression/wasm/gal_webgl_test.html @@ -129,13 +129,23 @@ // Run a single scenario function runScenario(index) { - if (!wasmModule) return; + console.log('[DEBUG JS] runScenario called with index:', index, 'wasmModule:', !!wasmModule); + if (!wasmModule) { + console.log('[DEBUG JS] wasmModule is null, returning early'); + return; + } - const result = wasmModule.ccall('runScenario', 'number', ['number'], [index]); - if (result === 0) { - setStatus(`Rendered: ${SCENARIO_NAMES[index]}`); - } else { - setStatus(`ERROR: Failed scenario ${index}`); + try { + console.log('[DEBUG JS] Calling ccall...'); + const result = wasmModule.ccall('runScenario', 'number', ['number'], [index]); + console.log('[DEBUG JS] ccall returned:', result); + if (result === 0) { + setStatus(`Rendered: ${SCENARIO_NAMES[index]}`); + } else { + setStatus(`ERROR: Failed scenario ${index}`); + } + } catch (e) { + console.error('[DEBUG JS] ccall threw error:', e); } } @@ -182,7 +192,8 @@ runAllScenarios, getScenarioName: (index) => SCENARIO_NAMES[index], getTotalScenarios: () => SCENARIO_NAMES.length, - getGLCanvas: getGLCanvas + getGLCanvas: getGLCanvas, + isReady: () => wasmModule !== null }; // Create canvas immediately (before module loads) diff --git a/tests/gal-regression/wasm/legacy_gl_stubs.js b/tests/gal-regression/wasm/legacy_gl_stubs.js new file mode 100644 index 0000000..f8e561e --- /dev/null +++ b/tests/gal-regression/wasm/legacy_gl_stubs.js @@ -0,0 +1,91 @@ +/** + * Legacy GL stubs for wxWidgets compatibility + * + * wxWidgets GL library may reference these functions at runtime even though: + * 1. wxGLAPI methods are stubbed as no-ops in C++ for WASM builds + * 2. KiCad doesn't use wxGLAPI - it has its own GAL + * 3. Our WEBGL_GAL uses modern WebGL 2.0 / OpenGL ES 3.0 only + * + * These no-op stubs satisfy the linker and runtime without affecting rendering. + * All actual rendering goes through WEBGL_GAL which uses VBOs and shaders. + */ + +addToLibrary({ + // Immediate mode rendering (replaced by VBOs in WEBGL_GAL) + glBegin: function(mode) {}, + glEnd: function() {}, + + // Vertex submission (replaced by vertex manager in WEBGL_GAL) + glVertex2f: function(x, y) {}, + glVertex3f: function(x, y, z) {}, + glVertex2d: function(x, y) {}, + glVertex3d: function(x, y, z) {}, + glVertex4f: function(x, y, z, w) {}, + glVertex4d: function(x, y, z, w) {}, + + // Color (replaced by vertex attributes/uniforms in WEBGL_GAL) + glColor3f: function(r, g, b) {}, + glColor4f: function(r, g, b, a) {}, + glColor3d: function(r, g, b) {}, + glColor4d: function(r, g, b, a) {}, + glColor3ub: function(r, g, b) {}, + glColor4ub: function(r, g, b, a) {}, + + // Texture coordinates (replaced by vertex attributes in WEBGL_GAL) + glTexCoord2f: function(s, t) {}, + glTexCoord2d: function(s, t) {}, + + // Normals (replaced by vertex attributes in WEBGL_GAL) + glNormal3f: function(nx, ny, nz) {}, + glNormal3d: function(nx, ny, nz) {}, + + // Matrix mode (replaced by MVP matrix uniform in WEBGL_GAL) + glMatrixMode: function(mode) {}, + glLoadIdentity: function() {}, + glPushMatrix: function() {}, + glPopMatrix: function() {}, + + // Projection (replaced by computeOrthoMatrix in WEBGL_GAL) + glOrtho: function(left, right, bottom, top, near, far) {}, + glFrustum: function(left, right, bottom, top, near, far) {}, + + // Matrix operations (replaced by glm in WEBGL_GAL) + glLoadMatrixf: function(m) {}, + glLoadMatrixd: function(m) {}, + glMultMatrixf: function(m) {}, + glMultMatrixd: function(m) {}, + glTranslatef: function(x, y, z) {}, + glTranslated: function(x, y, z) {}, + glRotatef: function(angle, x, y, z) {}, + glRotated: function(angle, x, y, z) {}, + glScalef: function(x, y, z) {}, + glScaled: function(x, y, z) {}, + + // Fixed function pipeline (not available in WebGL) + glShadeModel: function(mode) {}, + glLightfv: function(light, pname, params) {}, + glLightf: function(light, pname, param) {}, + glMaterialfv: function(face, pname, params) {}, + glMaterialf: function(face, pname, param) {}, + glColorMaterial: function(face, mode) {}, + + // Legacy client state (replaced by glVertexAttribPointer in WEBGL_GAL) + glEnableClientState: function(array) {}, + glDisableClientState: function(array) {}, + glVertexPointer: function(size, type, stride, pointer) {}, + glColorPointer: function(size, type, stride, pointer) {}, + glTexCoordPointer: function(size, type, stride, pointer) {}, + glNormalPointer: function(type, stride, pointer) {}, + + // Legacy texture environment (replaced by shaders in WEBGL_GAL) + glTexEnvf: function(target, pname, param) {}, + glTexEnvi: function(target, pname, param) {}, + glTexEnvfv: function(target, pname, params) {}, + + // glDrawBuffer (replaced by glDrawBuffers in WebGL 2.0) + glDrawBuffer: function(buf) {}, + + // Legacy index mode (not supported in WebGL) + glIndexi: function(c) {}, + glIndexf: function(c) {}, +}); diff --git a/tests/gal-regression/wasm/webgl/webgl_compositor.cpp b/tests/gal-regression/wasm/webgl/webgl_compositor.cpp index 7a89947..f407f57 100644 --- a/tests/gal-regression/wasm/webgl/webgl_compositor.cpp +++ b/tests/gal-regression/wasm/webgl/webgl_compositor.cpp @@ -329,9 +329,20 @@ void WEBGL_COMPOSITOR::SetBuffer( unsigned int aBufferHandle ) if( m_curFbo != DIRECT_RENDERING ) { m_curBuffer = aBufferHandle - 1; + // WebGL 2.0/OpenGL ES 3.0: use glDrawBuffers instead of glDrawBuffer - GLenum drawBuffers[] = { m_buffers[m_curBuffer].attachmentPoint }; - glDrawBuffers( 1, drawBuffers ); + // In WebGL 2.0, the draw buffer array index must match the attachment index. + // So for GL_COLOR_ATTACHMENTn, we need array[n] = GL_COLOR_ATTACHMENTn + GLenum attachmentPoint = m_buffers[m_curBuffer].attachmentPoint; + unsigned int attachmentIndex = attachmentPoint - GL_COLOR_ATTACHMENT0; + + // Create draw buffers array with GL_NONE for all entries except the target + GLenum drawBuffers[16]; + for( unsigned int i = 0; i <= attachmentIndex && i < 16; i++ ) + drawBuffers[i] = GL_NONE; + drawBuffers[attachmentIndex] = attachmentPoint; + + glDrawBuffers( attachmentIndex + 1, drawBuffers ); checkGlError( "setting draw buffer", __FILE__, __LINE__ ); glViewport( 0, 0, m_buffers[m_curBuffer].dimensions.x, m_buffers[m_curBuffer].dimensions.y ); diff --git a/tests/gal-regression/wasm/webgl/webgl_gal.cpp b/tests/gal-regression/wasm/webgl/webgl_gal.cpp index c9d288e..85570f8 100644 --- a/tests/gal-regression/wasm/webgl/webgl_gal.cpp +++ b/tests/gal-regression/wasm/webgl/webgl_gal.cpp @@ -751,7 +751,7 @@ void WEBGL_GAL::BeginDrawing() m_shader->Use(); m_shader->SetParameter( ufm_fontTexture, (int) FONT_TEXTURE_UNIT ); - m_shader->SetParameter( ufm_fontTextureWidth, (int) font_image.width ); + m_shader->SetParameter( ufm_fontTextureWidth, (float) font_image.width ); m_shader->Deactivate(); checkGlError( "setting bitmap font sampler as shader parameter", __FILE__, __LINE__ );