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 <noreply@anthropic.com>
This commit is contained in:
Viktor Vaczi 2026-01-08 10:13:11 +01:00
commit 18c73c355e
8 changed files with 165 additions and 27 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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) {},
});

View file

@ -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 );

View file

@ -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__ );