Add GL immediate mode shim for Emscripten compatibility

Create custom JS library (lib/gl_immediate_shim.js) that fixes two
Emscripten LEGACY_GL_EMULATION issues without modifying KiCad source:

1. Color-per-vertex requirement: Emscripten requires glColor before
   each glVertex, but standard OpenGL uses persistent "current color".
   The shim tracks color state and auto-injects before each vertex.

2. Missing double-precision functions: glVertex2d, glVertex3d,
   glColor3d, glColor4d are unimplemented in Emscripten (wontfix).
   The shim provides these as wrappers to float variants.

Update test code to use standard OpenGL patterns (color set once,
multiple vertices) to verify the shim works correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Viktor Vaczi 2025-11-28 21:22:09 +01:00
commit 3287ec7616
3 changed files with 222 additions and 45 deletions

178
lib/gl_immediate_shim.js Normal file
View file

@ -0,0 +1,178 @@
/**
* gl_immediate_shim.js
*
* Custom OpenGL immediate mode shims for KiCad WASM port.
* Fixes Emscripten LEGACY_GL_EMULATION issues:
* 1. Color-per-vertex requirement - injects color before each vertex automatically
* 2. Missing double-precision functions (glVertex2d, glVertex3d, glColor3d, glColor4d)
*
* Usage: emcc ... --js-library=lib/gl_immediate_shim.js
*/
addToLibrary({
// ==================================================================
// GLImmediateShim - State tracking and function wrapping
// ==================================================================
$GLImmediateShim__deps: ['$GLImmediate'],
$GLImmediateShim__postset: 'GLImmediateShim.init();',
$GLImmediateShim: {
// Current color state (persistent across vertices)
currentColor: null,
// Track if we're inside glBegin/glEnd block
inBeginEnd: false,
// Track if color was called since the last vertex
// This prevents double-injection when code already calls color per vertex
colorCalledSinceLastVertex: false,
// Original functions we're wrapping
origFns: {},
// Initialization flag
initialized: false,
init: function() {
if (GLImmediateShim.initialized) return;
if (typeof GLImmediate === 'undefined') {
// GLImmediate not ready yet, will be called again
console.log('[GLImmediateShim] Waiting for GLImmediate...');
return;
}
console.log('[GLImmediateShim] Initializing OpenGL immediate mode shims');
// Initialize current color to white (OpenGL default)
GLImmediateShim.currentColor = new Float32Array([1.0, 1.0, 1.0, 1.0]);
// Store original functions
GLImmediateShim.origFns = {
glBegin: _glBegin,
glEnd: _glEnd,
glVertex2f: _glVertex2f,
glVertex3f: _glVertex3f,
glColor3f: _glColor3f,
glColor4f: _glColor4f,
};
// Install shims
_glBegin = GLImmediateShim.shimBegin;
_glEnd = GLImmediateShim.shimEnd;
_glVertex2f = GLImmediateShim.shimVertex2f;
_glVertex3f = GLImmediateShim.shimVertex3f;
_glColor3f = GLImmediateShim.shimColor3f;
_glColor4f = GLImmediateShim.shimColor4f;
GLImmediateShim.initialized = true;
console.log('[GLImmediateShim] Initialized successfully');
},
// ==================================================================
// Shim implementations
// ==================================================================
shimBegin: function(mode) {
GLImmediateShim.inBeginEnd = true;
GLImmediateShim.colorCalledSinceLastVertex = false;
GLImmediateShim.origFns.glBegin(mode);
},
shimEnd: function() {
GLImmediateShim.inBeginEnd = false;
GLImmediateShim.origFns.glEnd();
},
shimColor3f: function(r, g, b) {
var c = GLImmediateShim.currentColor;
c[0] = r;
c[1] = g;
c[2] = b;
c[3] = 1.0;
if (GLImmediateShim.inBeginEnd) {
// Mark that color was explicitly called for this vertex
GLImmediateShim.colorCalledSinceLastVertex = true;
}
GLImmediateShim.origFns.glColor3f(r, g, b);
},
shimColor4f: function(r, g, b, a) {
var c = GLImmediateShim.currentColor;
c[0] = r;
c[1] = g;
c[2] = b;
c[3] = a;
if (GLImmediateShim.inBeginEnd) {
// Mark that color was explicitly called for this vertex
GLImmediateShim.colorCalledSinceLastVertex = true;
}
GLImmediateShim.origFns.glColor4f(r, g, b, a);
},
// Inject current color before each vertex (only if not already called)
injectColor: function() {
if (!GLImmediateShim.inBeginEnd) return;
// Only inject color if it wasn't already called for this vertex
if (!GLImmediateShim.colorCalledSinceLastVertex) {
var c = GLImmediateShim.currentColor;
GLImmediateShim.origFns.glColor4f(c[0], c[1], c[2], c[3]);
}
// Reset the flag for the next vertex
GLImmediateShim.colorCalledSinceLastVertex = false;
},
shimVertex2f: function(x, y) {
GLImmediateShim.injectColor();
GLImmediateShim.origFns.glVertex2f(x, y);
},
shimVertex3f: function(x, y, z) {
GLImmediateShim.injectColor();
GLImmediateShim.origFns.glVertex3f(x, y, z);
},
},
// ==================================================================
// Double-precision vertex functions (missing in Emscripten)
// ==================================================================
glVertex2d__deps: ['glVertex2f'],
glVertex2d: function(x, y) {
_glVertex2f(x, y);
},
glVertex3d__deps: ['glVertex3f'],
glVertex3d: function(x, y, z) {
_glVertex3f(x, y, z);
},
glVertex4d__deps: ['glVertex4f'],
glVertex4d: function(x, y, z, w) {
_glVertex4f(x, y, z, w);
},
// ==================================================================
// Double-precision color functions
// ==================================================================
glColor3d__deps: ['glColor3f'],
glColor3d: function(r, g, b) {
_glColor3f(r, g, b);
},
glColor4d__deps: ['glColor4f'],
glColor4d: function(r, g, b, a) {
_glColor4f(r, g, b, a);
},
});
// Ensure GLImmediateShim is included in the build
if (typeof extraLibraryFuncs !== 'undefined') {
extraLibraryFuncs.push('$GLImmediateShim');
}

View file

@ -20,9 +20,13 @@ CXXFLAGS = -O2 $(WX_CXXFLAGS)
# Note: Cannot combine LEGACY_GL_EMULATION with FULL_ES2 - they are mutually exclusive
EM_GL_FLAGS = -sLEGACY_GL_EMULATION -sMAX_WEBGL_VERSION=2
# GL immediate mode shim - fixes Emscripten's color-per-vertex requirement
# and provides missing glVertex2d/glColor4d implementations
GL_SHIM = ../../lib/gl_immediate_shim.js
LDFLAGS = -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
-s "EXPORTED_RUNTIME_METHODS=['HEAPU8','HEAP8','HEAP32']" \
$(EM_GL_FLAGS) $(WX_LDFLAGS)
$(EM_GL_FLAGS) --js-library=$(GL_SHIM) $(WX_LDFLAGS)
JS = $(TOOLS_ROOT)/wx.js
HTML = $(TOOLS_ROOT)/template.html

View file

@ -352,13 +352,16 @@ void GLTestCanvas::Render()
}
// Test 1: Immediate Mode Drawing (glBegin/glEnd) - KiCad uses this heavily
// Using STANDARD OpenGL patterns (color once, multiple vertices)
// This tests the GL shim that handles Emscripten's color-per-vertex requirement
void GLTestCanvas::TestImmediateMode()
{
wxPrintf("[GL TEST] Testing immediate mode (glBegin/glEnd)...\n");
wxPrintf("[GL TEST] Using STANDARD OpenGL patterns (color set once per primitive)\n");
fflush(stdout);
// Test GL_TRIANGLES with glVertex3f and glColor3f
// RGB triangle - each vertex has a different color
// RGB triangle - each vertex has a different color (smooth shading)
glBegin(GL_TRIANGLES);
glColor3f(1.0f, 0.0f, 0.0f); // Red
glVertex3f(-1.0f, -0.5f, 0.0f);
@ -369,50 +372,39 @@ void GLTestCanvas::TestImmediateMode()
glEnd();
// Test GL_QUADS with glVertex2f and glColor4f
// NOTE: Emscripten's immediate mode requires color per vertex, not OpenGL's "current color" semantic
// STANDARD OPENGL: Set color ONCE, then multiple vertices
// The GL shim should inject color before each vertex automatically
glColor4f(1.0f, 1.0f, 0.0f, 0.8f); // Yellow, semi-transparent - SET ONCE
glBegin(GL_QUADS);
glColor4f(1.0f, 1.0f, 0.0f, 0.8f); // Yellow, semi-transparent
glVertex2f(-1.8f, -1.8f);
glColor4f(1.0f, 1.0f, 0.0f, 0.8f);
glVertex2f(-1.8f, -1.8f); // All 4 vertices use the same yellow color
glVertex2f(-1.2f, -1.8f);
glColor4f(1.0f, 1.0f, 0.0f, 0.8f);
glVertex2f(-1.2f, -1.2f);
glColor4f(1.0f, 1.0f, 0.0f, 0.8f);
glVertex2f(-1.8f, -1.2f);
glEnd();
// Test GL_LINES with glVertex3f (glVertex2d not supported in Emscripten)
// NOTE: color per vertex required
// Test GL_LINES with standard pattern
// STANDARD OPENGL: Set color ONCE, then multiple vertices
glColor3f(1.0f, 1.0f, 1.0f); // White - SET ONCE
glBegin(GL_LINES);
glColor3f(1.0f, 1.0f, 1.0f); // White
glVertex3f(-1.5f, 1.5f, 0.0f);
glColor3f(1.0f, 1.0f, 1.0f);
glVertex3f(1.5f, 1.5f, 0.0f);
glEnd();
// Test GL_LINE_STRIP
// NOTE: color per vertex required
// Test GL_LINE_STRIP with standard pattern
glColor3f(0.0f, 1.0f, 1.0f); // Cyan - SET ONCE
glBegin(GL_LINE_STRIP);
glColor3f(0.0f, 1.0f, 1.0f); // Cyan
glVertex3f(1.2f, -1.8f, 0.0f);
glColor3f(0.0f, 1.0f, 1.0f);
glVertex3f(1.4f, -1.4f, 0.0f);
glColor3f(0.0f, 1.0f, 1.0f);
glVertex3f(1.6f, -1.6f, 0.0f);
glColor3f(0.0f, 1.0f, 1.0f);
glVertex3f(1.8f, -1.2f, 0.0f);
glEnd();
// Test GL_LINE_LOOP
// NOTE: color per vertex required
// Test GL_LINE_LOOP with standard pattern
glColor3f(1.0f, 0.0f, 1.0f); // Magenta - SET ONCE
glBegin(GL_LINE_LOOP);
glColor3f(1.0f, 0.0f, 1.0f); // Magenta
glVertex3f(1.2f, 1.2f, 0.0f);
glColor3f(1.0f, 0.0f, 1.0f);
glVertex3f(1.8f, 1.2f, 0.0f);
glColor3f(1.0f, 0.0f, 1.0f);
glVertex3f(1.8f, 1.8f, 0.0f);
glColor3f(1.0f, 0.0f, 1.0f);
glVertex3f(1.2f, 1.8f, 0.0f);
glEnd();
@ -421,26 +413,24 @@ void GLTestCanvas::TestImmediateMode()
}
// Test 2: Matrix Operations - KiCad uses glPushMatrix/glPopMatrix extensively
// Using STANDARD OpenGL patterns (color once, multiple vertices)
void GLTestCanvas::TestMatrixOperations()
{
wxPrintf("[GL TEST] Testing matrix operations...\n");
fflush(stdout);
// Draw centered triangle
// NOTE: Emscripten requires color per vertex
// Draw centered triangle with standard GL pattern
glPushMatrix();
glTranslatef(0.0f, 0.0f, 0.0f);
glColor3f(0.5f, 0.5f, 0.5f); // Gray - SET ONCE
glBegin(GL_TRIANGLES);
glColor3f(0.5f, 0.5f, 0.5f);
glVertex3f(-0.3f, -0.3f, 0.0f);
glColor3f(0.5f, 0.5f, 0.5f);
glVertex3f(0.3f, -0.3f, 0.0f);
glColor3f(0.5f, 0.5f, 0.5f);
glVertex3f(0.0f, 0.3f, 0.0f);
glEnd();
glPopMatrix();
// Draw 4 rotated/translated copies
// Draw 4 rotated/translated copies with standard GL pattern
for (int i = 0; i < 4; i++) {
glPushMatrix();
float angle = i * 90.0f;
@ -451,20 +441,16 @@ void GLTestCanvas::TestMatrixOperations()
glRotatef(angle, 0.0f, 0.0f, 1.0f);
glScalef(0.5f, 0.5f, 1.0f);
// Draw colored square
// NOTE: Emscripten requires color per vertex
// Draw colored square with standard GL pattern
float r = (i == 0 || i == 3) ? 1.0f : 0.3f;
float g = (i == 1 || i == 3) ? 1.0f : 0.3f;
float b = (i == 2 || i == 3) ? 1.0f : 0.3f;
glColor3f(r, g, b); // SET ONCE before glBegin
glBegin(GL_QUADS);
glColor3f(r, g, b);
glVertex3f(-0.5f, -0.5f, 0.0f);
glColor3f(r, g, b);
glVertex3f(0.5f, -0.5f, 0.0f);
glColor3f(r, g, b);
glVertex3f(0.5f, 0.5f, 0.0f);
glColor3f(r, g, b);
glVertex3f(-0.5f, 0.5f, 0.0f);
glEnd();
glPopMatrix();
@ -530,6 +516,7 @@ void GLTestCanvas::TestVertexArrays()
}
// Test 4: State Management - glEnable/glDisable, blending
// Using STANDARD OpenGL patterns with separate glBegin/glEnd for each color
void GLTestCanvas::TestStateManagement()
{
wxPrintf("[GL TEST] Testing state management...\n");
@ -539,24 +526,30 @@ void GLTestCanvas::TestStateManagement()
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Draw overlapping semi-transparent squares
// Draw overlapping semi-transparent squares using standard GL pattern
// Each quad has its own color set before glBegin
// Red square
glColor4f(1.0f, 0.0f, 0.0f, 0.5f); // SET ONCE
glBegin(GL_QUADS);
// Red square
glColor4f(1.0f, 0.0f, 0.0f, 0.5f);
glVertex2f(-1.0f, -1.0f);
glVertex2f(0.5f, -1.0f);
glVertex2f(0.5f, 0.5f);
glVertex2f(-1.0f, 0.5f);
glEnd();
// Green square
glColor4f(0.0f, 1.0f, 0.0f, 0.5f);
// Green square
glColor4f(0.0f, 1.0f, 0.0f, 0.5f); // SET ONCE
glBegin(GL_QUADS);
glVertex2f(-0.5f, -0.5f);
glVertex2f(1.0f, -0.5f);
glVertex2f(1.0f, 1.0f);
glVertex2f(-0.5f, 1.0f);
glEnd();
// Blue square
glColor4f(0.0f, 0.0f, 1.0f, 0.5f);
// Blue square
glColor4f(0.0f, 0.0f, 1.0f, 0.5f); // SET ONCE
glBegin(GL_QUADS);
glVertex2f(0.0f, 0.0f);
glVertex2f(1.5f, 0.0f);
glVertex2f(1.5f, 1.5f);
@ -570,13 +563,14 @@ void GLTestCanvas::TestStateManagement()
}
// Test 5: Texture coordinates (no actual texture, just testing the calls)
// Note: glTexCoord is per-vertex anyway, and color is set once before glBegin
void GLTestCanvas::TestTexCoords()
{
wxPrintf("[GL TEST] Testing texture coordinates...\n");
fflush(stdout);
glColor3f(0.8f, 0.8f, 0.8f); // SET ONCE
glBegin(GL_QUADS);
glColor3f(0.8f, 0.8f, 0.8f);
glTexCoord2f(0.0f, 0.0f); glVertex2f(-1.0f, -1.0f);
glTexCoord2f(1.0f, 0.0f); glVertex2f(1.0f, -1.0f);
glTexCoord2f(1.0f, 1.0f); glVertex2f(1.0f, 1.0f);
@ -588,13 +582,14 @@ void GLTestCanvas::TestTexCoords()
}
// Test 6: Normal vectors (for lighting, which we're not testing but calls should work)
// Note: glNormal is per-vertex, and color is set once before glBegin
void GLTestCanvas::TestNormals()
{
wxPrintf("[GL TEST] Testing normal vectors...\n");
fflush(stdout);
glColor3f(0.7f, 0.7f, 0.9f); // SET ONCE
glBegin(GL_TRIANGLES);
glColor3f(0.7f, 0.7f, 0.9f);
glNormal3f(0.0f, 0.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, 0.0f);
glNormal3f(0.0f, 0.0f, 1.0f);