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:
parent
d2f1a496f5
commit
18c73c355e
8 changed files with 165 additions and 27 deletions
|
|
@ -129,13 +129,23 @@
|
||||||
|
|
||||||
// Run a single scenario
|
// Run a single scenario
|
||||||
function runScenario(index) {
|
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]);
|
try {
|
||||||
if (result === 0) {
|
console.log('[DEBUG JS] Calling ccall...');
|
||||||
setStatus(`Rendered: ${SCENARIO_NAMES[index]}`);
|
const result = wasmModule.ccall('runScenario', 'number', ['number'], [index]);
|
||||||
} else {
|
console.log('[DEBUG JS] ccall returned:', result);
|
||||||
setStatus(`ERROR: Failed scenario ${index}`);
|
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,
|
runAllScenarios,
|
||||||
getScenarioName: (index) => SCENARIO_NAMES[index],
|
getScenarioName: (index) => SCENARIO_NAMES[index],
|
||||||
getTotalScenarios: () => SCENARIO_NAMES.length,
|
getTotalScenarios: () => SCENARIO_NAMES.length,
|
||||||
getGLCanvas: getGLCanvas
|
getGLCanvas: getGLCanvas,
|
||||||
|
isReady: () => wasmModule !== null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create canvas immediately (before module loads)
|
// Create canvas immediately (before module loads)
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ test.describe('GAL WebGL Regression Tests', () => {
|
||||||
|
|
||||||
// Wait for module to be ready
|
// Wait for module to be ready
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
return (window as any).galTest !== undefined;
|
return (window as any).galTest !== undefined && (window as any).galTest.isReady();
|
||||||
}, { timeout: 60000 });
|
}, { timeout: 60000 });
|
||||||
|
|
||||||
// Run the scenario
|
// Run the scenario
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Makefile for GAL WebGL Test (WASM)
|
# Makefile for GAL WebGL Test (WASM)
|
||||||
#
|
#
|
||||||
# Builds WEBGL_GAL (ported from OPENGL_GAL) using wxWidgets WASM build.
|
# 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:
|
# Usage:
|
||||||
# make # Build
|
# make # Build
|
||||||
|
|
@ -72,7 +72,14 @@ BASE_LDFLAGS = -sALLOW_MEMORY_GROWTH=1 \
|
||||||
# No LEGACY_GL_EMULATION - WEBGL_GAL uses VBOs and custom shaders
|
# No LEGACY_GL_EMULATION - WEBGL_GAL uses VBOs and custom shaders
|
||||||
EM_GL_FLAGS = -sFULL_ES3=1 -sMAX_WEBGL_VERSION=2
|
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
|
# Templates
|
||||||
JS = $(TOOLS_ROOT)/wx.js
|
JS = $(TOOLS_ROOT)/wx.js
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,9 @@ extern "C" {
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
EMSCRIPTEN_KEEPALIVE
|
||||||
int runScenario(int scenarioIndex) {
|
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) {
|
if (scenarioIndex < 0 || scenarioIndex >= g_totalScenarios) {
|
||||||
printf("ERROR: Invalid scenario index %d (valid: 0-%d)\n",
|
printf("ERROR: Invalid scenario index %d (valid: 0-%d)\n",
|
||||||
scenarioIndex, g_totalScenarios - 1);
|
scenarioIndex, g_totalScenarios - 1);
|
||||||
|
|
@ -163,16 +166,20 @@ bool GALTestApp::OnInit() {
|
||||||
// Set up the GAL
|
// Set up the GAL
|
||||||
g_gal->SetScreenSize(VECTOR2I(CANVAS_WIDTH, CANVAS_HEIGHT));
|
g_gal->SetScreenSize(VECTOR2I(CANVAS_WIDTH, CANVAS_HEIGHT));
|
||||||
g_gal->ResizeScreen(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)
|
// CRITICAL: Set worldUnitLength for 1:1 world-to-screen coordinate mapping
|
||||||
double worldScale = 1.0 / 10000.0; // Convert nm to screen units
|
// GAL default worldUnitLength is for PCB nanometers, which would compress
|
||||||
g_gal->SetWorldUnitLength(worldScale);
|
// 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->SetScreenDPI(96);
|
||||||
|
g_gal->SetWorldUnitLength(1.0 / 96.0);
|
||||||
|
|
||||||
// Set world boundaries
|
// Set up coordinate transformation for 1:1 world-to-screen mapping
|
||||||
VECTOR2D worldSize(CANVAS_WIDTH / worldScale, CANVAS_HEIGHT / worldScale);
|
// LookAtPoint should be at center, ZoomFactor of 1.0 gives 1:1 mapping
|
||||||
BOX2D worldBounds(VECTOR2D(0, 0), worldSize);
|
g_gal->SetLookAtPoint(VECTOR2D(CANVAS_WIDTH / 2.0, CANVAS_HEIGHT / 2.0));
|
||||||
g_gal->SetWorldScreenMatrix(g_gal->GetWorldScreenMatrix());
|
g_gal->SetZoomFactor(1.0);
|
||||||
|
g_gal->ComputeWorldScreenMatrix();
|
||||||
|
|
||||||
// Initialize the compositor with proper context locking
|
// Initialize the compositor with proper context locking
|
||||||
g_gal->LockContext(-1); // Lock with init ID
|
g_gal->LockContext(-1); // Lock with init ID
|
||||||
|
|
|
||||||
|
|
@ -129,13 +129,23 @@
|
||||||
|
|
||||||
// Run a single scenario
|
// Run a single scenario
|
||||||
function runScenario(index) {
|
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]);
|
try {
|
||||||
if (result === 0) {
|
console.log('[DEBUG JS] Calling ccall...');
|
||||||
setStatus(`Rendered: ${SCENARIO_NAMES[index]}`);
|
const result = wasmModule.ccall('runScenario', 'number', ['number'], [index]);
|
||||||
} else {
|
console.log('[DEBUG JS] ccall returned:', result);
|
||||||
setStatus(`ERROR: Failed scenario ${index}`);
|
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,
|
runAllScenarios,
|
||||||
getScenarioName: (index) => SCENARIO_NAMES[index],
|
getScenarioName: (index) => SCENARIO_NAMES[index],
|
||||||
getTotalScenarios: () => SCENARIO_NAMES.length,
|
getTotalScenarios: () => SCENARIO_NAMES.length,
|
||||||
getGLCanvas: getGLCanvas
|
getGLCanvas: getGLCanvas,
|
||||||
|
isReady: () => wasmModule !== null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create canvas immediately (before module loads)
|
// Create canvas immediately (before module loads)
|
||||||
|
|
|
||||||
91
tests/gal-regression/wasm/legacy_gl_stubs.js
Normal file
91
tests/gal-regression/wasm/legacy_gl_stubs.js
Normal 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) {},
|
||||||
|
});
|
||||||
|
|
@ -329,9 +329,20 @@ void WEBGL_COMPOSITOR::SetBuffer( unsigned int aBufferHandle )
|
||||||
if( m_curFbo != DIRECT_RENDERING )
|
if( m_curFbo != DIRECT_RENDERING )
|
||||||
{
|
{
|
||||||
m_curBuffer = aBufferHandle - 1;
|
m_curBuffer = aBufferHandle - 1;
|
||||||
|
|
||||||
// WebGL 2.0/OpenGL ES 3.0: use glDrawBuffers instead of glDrawBuffer
|
// WebGL 2.0/OpenGL ES 3.0: use glDrawBuffers instead of glDrawBuffer
|
||||||
GLenum drawBuffers[] = { m_buffers[m_curBuffer].attachmentPoint };
|
// In WebGL 2.0, the draw buffer array index must match the attachment index.
|
||||||
glDrawBuffers( 1, drawBuffers );
|
// 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__ );
|
checkGlError( "setting draw buffer", __FILE__, __LINE__ );
|
||||||
|
|
||||||
glViewport( 0, 0, m_buffers[m_curBuffer].dimensions.x, m_buffers[m_curBuffer].dimensions.y );
|
glViewport( 0, 0, m_buffers[m_curBuffer].dimensions.x, m_buffers[m_curBuffer].dimensions.y );
|
||||||
|
|
|
||||||
|
|
@ -751,7 +751,7 @@ void WEBGL_GAL::BeginDrawing()
|
||||||
|
|
||||||
m_shader->Use();
|
m_shader->Use();
|
||||||
m_shader->SetParameter( ufm_fontTexture, (int) FONT_TEXTURE_UNIT );
|
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();
|
m_shader->Deactivate();
|
||||||
checkGlError( "setting bitmap font sampler as shader parameter", __FILE__, __LINE__ );
|
checkGlError( "setting bitmap font sampler as shader parameter", __FILE__, __LINE__ );
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue