feat(webgl): Migrate to pure WebGL 2.0 and fix GL canvas layering

Update submodules and build config for pure WebGL 2.0 (drop -sFULL_ES3):
- kicad: VBO-based drawing, compositor FBO rewrite, shader conversion
- wxwidgets: Fix z-index layering so GL canvas renders above 2D UI canvas
- Build: Remove FULL_ES3 from linker flags
- GAL tests: White background, opaque alpha, shared shader converter,
  updated baseline screenshots

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Viktor Vaczi 2026-03-20 12:38:24 +01:00
commit 6fa6c9207f
56 changed files with 32 additions and 196 deletions

2
kicad

@ -1 +1 @@
Subproject commit d8a9ac4304f5747be43e88deca7bfa0ac87c0213
Subproject commit 4ddb9b47f5f9dc3f30ed6d59778e7bc407fdc3cd

View file

@ -243,7 +243,7 @@ emcmake cmake "${KICAD_DIR}" \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_CXX_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -DKICAD_USE_PLATFORM_WASM=1 -I${SYSROOT}/include -I${STUBS_DIR}" \
-DCMAKE_C_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -I${SYSROOT}/include -I${STUBS_DIR}" \
-DCMAKE_EXE_LINKER_FLAGS="${LINKER_DEBUG_FLAGS} -pthread -sUSE_ZLIB=1 -sASYNCIFY=1 -sDYNCALLS=1 -sASYNCIFY_STACK_SIZE=65536 -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' -sPTHREAD_POOL_SIZE_STRICT=0 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB -sMAX_WEBGL_VERSION=2 -sFULL_ES3=1 -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','dynCall'] -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['\$dynCall'] --bind -L${SYSROOT}/lib ${STUBS_BUILD}/libgit2_stub.a ${STUBS_BUILD}/libcurl_stub.a ${STUBS_BUILD}/libpcbnew_scripting_stub.a ${STUBS_BUILD}/libnng_stub.a ${STUBS_BUILD}/pcbnew_embind.o" \
-DCMAKE_EXE_LINKER_FLAGS="${LINKER_DEBUG_FLAGS} -pthread -sUSE_ZLIB=1 -sASYNCIFY=1 -sDYNCALLS=1 -sASYNCIFY_STACK_SIZE=65536 -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' -sPTHREAD_POOL_SIZE_STRICT=0 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB -sMAX_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','dynCall'] -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['\$dynCall'] --bind -L${SYSROOT}/lib ${STUBS_BUILD}/libgit2_stub.a ${STUBS_BUILD}/libcurl_stub.a ${STUBS_BUILD}/libpcbnew_scripting_stub.a ${STUBS_BUILD}/libnng_stub.a ${STUBS_BUILD}/pcbnew_embind.o" \
-DCMAKE_PREFIX_PATH="${SYSROOT};${WX_BUILD}" \
-DwxWidgets_CONFIG_EXECUTABLE="${WX_BUILD}/wx-config" \
\

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 KiB

After

Width:  |  Height:  |  Size: 491 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Before After
Before After

View file

@ -97,6 +97,15 @@ bool SaveScreenshot(const std::string& path, KIGFX::OPENGL_GAL* gal, int width,
if (hasContent) std::cout << " (" << nonBlackCount << " non-black pixels)";
std::cout << std::endl;
// Force all alpha values to 255 (opaque) to match the composited canvas output.
// Native reads from the FBO where alpha < 255 for semi-transparent regions.
// WebGL captures the composited canvas where alpha is always 1.0 (composited on
// black with GL_ONE blend). By making native output opaque, both tests produce
// equivalent "user-visible" screenshots that can be compared directly.
for (size_t i = 3; i < pixels.size(); i += 4) {
pixels[i] = 255;
}
// Flip vertically (OpenGL has origin at bottom-left)
stbi_flip_vertically_on_write(1);
@ -173,7 +182,11 @@ public:
// Set up the viewport - use ResizeScreen to properly initialize compositor
m_gal->ResizeScreen(g_width, g_height);
m_gal->SetClearColor(KIGFX::COLOR4D(0.1, 0.1, 0.15, 1.0));
// Use white background to match WebGL test (which captures the composited canvas).
// Native reads from the FBO (pre-compositing), so we set alpha to 255 in SaveScreenshot
// to simulate the composited result. White background ensures both tests see the same
// blended colors for semi-transparent shapes.
m_gal->SetClearColor(KIGFX::COLOR4D(1.0, 1.0, 1.0, 1.0));
// CRITICAL: Set worldUnitLength for 1:1 world-to-screen coordinate mapping
// GAL default worldUnitLength is for PCB nanometers (3.937e-8), which would

View file

@ -69,11 +69,11 @@ BASE_LDFLAGS = -sALLOW_MEMORY_GROWTH=1 \
-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','HEAPU8'] \
-sMODULARIZE=1 \
-sEXPORT_NAME='createGALTest' \
-sENVIRONMENT=web
-sENVIRONMENT=web,worker
# GL-specific flags - WebGL 2.0 with full ES3 support
# GL-specific flags - pure WebGL 2.0 (matching KiCad PCBnew build, no FULL_ES3 emulation)
# No LEGACY_GL_EMULATION - WEBGL_GAL uses VBOs and custom shaders
EM_GL_FLAGS = -sFULL_ES3=1 -sMAX_WEBGL_VERSION=2
EM_GL_FLAGS = -sMAX_WEBGL_VERSION=2
# Legacy GL stubs for wxWidgets compatibility
# wxWidgets GL library may reference legacy functions at runtime, but actual

View file

@ -9,191 +9,14 @@ KiCad's BUILTIN_SHADERS namespace.
import os
import sys
import re
def convert_glsl120_to_es300(shader_source, is_fragment_shader):
"""
Convert GLSL 1.20 (OpenGL 2.1) shader to GLSL ES 3.00 (WebGL 2.0).
Key changes:
- #version 120 -> #version 300 es (must be ABSOLUTE first line!)
- Move any comments before #version to after declarations
- Add precision qualifiers
- attribute -> in
- varying -> out (vertex) / in (fragment)
- gl_FragColor -> custom output variable (fragment)
- texture2D -> texture
- Fix int * float type issues (2 * x -> 2.0 * x)
- Fix int / float type issues (x / 4 -> x / 4.0)
Legacy GL built-in conversions (for KiCad shaders):
- gl_ModelViewProjectionMatrix -> uniform u_modelViewProjectionMatrix
- gl_Vertex -> attribute a_vertex
- gl_Color -> attribute a_color (vertex) / varying v_color (fragment)
- gl_FrontColor -> varying v_color (vertex output)
- gl_TexCoord[0] -> varying v_texCoord
- ftransform() -> u_modelViewProjectionMatrix * a_vertex
"""
lines = shader_source.split('\n')
result = []
version_replaced = False
# Track what legacy built-ins are used so we can add declarations
uses_mvp_matrix = 'gl_ModelViewProjectionMatrix' in shader_source or 'ftransform()' in shader_source
uses_gl_vertex = 'gl_Vertex' in shader_source or 'ftransform()' in shader_source
uses_gl_color = 'gl_Color' in shader_source
uses_gl_front_color = 'gl_FrontColor' in shader_source
uses_gl_texcoord = 'gl_TexCoord' in shader_source
uses_gl_multitexcoord0 = 'gl_MultiTexCoord0' in shader_source
# GLSL ES 3.00 requires #version to be the ABSOLUTE first line
# Collect any comments before #version to add after declarations
pre_version_comments = []
in_multiline_comment = False
# Patterns to fix type issues
int_mult_pattern = re.compile(r'\b(\d+)\s*\*\s*([a-zA-Z_])')
div_int_pattern = re.compile(r'([a-zA-Z_)\]]+)\s*/\s*(\d+)(?!\.)')
for line in lines:
stripped = line.strip()
# Before #version is found, collect comments and empty lines
if not version_replaced:
# Track multiline comment state
if '/*' in stripped and '*/' not in stripped:
in_multiline_comment = True
pre_version_comments.append(line)
continue
elif in_multiline_comment:
pre_version_comments.append(line)
if '*/' in stripped:
in_multiline_comment = False
continue
elif stripped.startswith('//') or stripped == '' or ('/*' in stripped and '*/' in stripped):
pre_version_comments.append(line)
continue
# Replace #version 120 with #version 300 es + precision + legacy built-in replacements
if stripped.startswith('#version'):
result.append('#version 300 es')
result.append('precision highp float;')
result.append('precision highp int;')
if is_fragment_shader:
result.append('out vec4 fragColor;')
# Fragment shader receives varyings from vertex shader
if uses_gl_color or uses_gl_front_color:
result.append('in vec4 v_color;')
if uses_gl_texcoord:
result.append('in vec2 v_texCoord;')
else:
# Vertex shader - add uniforms and attributes for legacy built-ins
if uses_mvp_matrix:
result.append('uniform mat4 u_modelViewProjectionMatrix;')
if uses_gl_vertex:
result.append('in vec4 a_vertex;')
if uses_gl_color:
result.append('in vec4 a_color;')
if uses_gl_multitexcoord0:
result.append('in vec4 a_texCoord0;')
# Vertex shader outputs varyings
if uses_gl_front_color or uses_gl_color:
result.append('out vec4 v_color;')
if uses_gl_texcoord:
result.append('out vec2 v_texCoord;')
# Add back any pre-version comments after the declarations
if pre_version_comments:
result.append('') # Blank line before comments
result.extend(pre_version_comments)
version_replaced = True
continue
# Convert attribute to in (vertex shaders only)
if not is_fragment_shader and stripped.startswith('attribute '):
line = line.replace('attribute ', 'in ', 1)
# Convert varying to out (vertex) or in (fragment)
if stripped.startswith('varying '):
if is_fragment_shader:
line = line.replace('varying ', 'in ', 1)
else:
line = line.replace('varying ', 'out ', 1)
# Convert gl_FragColor to fragColor (fragment shaders)
if is_fragment_shader and 'gl_FragColor' in line:
line = line.replace('gl_FragColor', 'fragColor')
# Convert texture2D to texture
if 'texture2D' in line:
line = line.replace('texture2D', 'texture')
# Convert legacy GL built-ins
# ftransform() -> u_modelViewProjectionMatrix * a_vertex (must be done before other replacements)
if 'ftransform()' in line:
line = line.replace('ftransform()', 'u_modelViewProjectionMatrix * a_vertex')
# gl_ModelViewProjectionMatrix -> u_modelViewProjectionMatrix
if 'gl_ModelViewProjectionMatrix' in line:
line = line.replace('gl_ModelViewProjectionMatrix', 'u_modelViewProjectionMatrix')
# gl_Vertex -> a_vertex
if 'gl_Vertex' in line:
line = line.replace('gl_Vertex', 'a_vertex')
# gl_FrontColor -> v_color (vertex shader output)
if 'gl_FrontColor' in line:
line = line.replace('gl_FrontColor', 'v_color')
# gl_Color -> a_color (vertex) or v_color (fragment)
if 'gl_Color' in line:
if is_fragment_shader:
line = line.replace('gl_Color', 'v_color')
else:
line = line.replace('gl_Color', 'a_color')
# gl_TexCoord[0].st or gl_TexCoord[0].xy -> v_texCoord
if 'gl_TexCoord' in line:
# Handle gl_TexCoord[0].st and gl_TexCoord[0].xy
line = re.sub(r'gl_TexCoord\[0\]\.st', 'v_texCoord', line)
line = re.sub(r'gl_TexCoord\[0\]\.xy', 'v_texCoord', line)
# Handle bare gl_TexCoord[0] (less common)
line = re.sub(r'gl_TexCoord\[0\]', 'vec4(v_texCoord, 0.0, 0.0)', line)
# gl_MultiTexCoord0 -> a_texCoord0 (for SMAA shaders)
if 'gl_MultiTexCoord0' in line:
line = re.sub(r'gl_MultiTexCoord0\.st', 'a_texCoord0.st', line)
line = re.sub(r'gl_MultiTexCoord0\.xy', 'a_texCoord0.xy', line)
line = line.replace('gl_MultiTexCoord0', 'a_texCoord0')
# Fix uniform int -> uniform float (GLSL ES 3.00 doesn't allow implicit int/float conversion)
# This specifically handles u_fontTextureWidth which is multiplied with floats
if 'uniform int ' in line:
line = line.replace('uniform int ', 'uniform float ')
# Fix int * float type issues: "2 * x" -> "2.0 * x"
def fix_int_mult(match):
int_val = match.group(1)
var_start = match.group(2)
return f'{int_val}.0 * {var_start}'
line = int_mult_pattern.sub(fix_int_mult, line)
# Fix float / int type issues: "x / 4" -> "x / 4.0"
def fix_div_int(match):
var_part = match.group(1)
int_val = match.group(2)
return f'{var_part} / {int_val}.0'
line = div_int_pattern.sub(fix_div_int, line)
result.append(line)
# If no #version was found, DON'T add one - the shader will get its version
# from a runtime preamble (e.g. SMAA shaders). Just do basic conversions.
# NOTE: If the shader has no #version, we still need to convert legacy GL built-ins
return '\n'.join(result)
# Import the canonical GLSL converter from the KiCad shaders directory.
# This ensures the test uses the exact same conversion as the KiCad WASM build.
_kicad_shaders_dir = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'..', '..', '..', 'kicad', 'common', 'gal', 'shaders'
)
sys.path.insert(0, os.path.abspath(_kicad_shaders_dir))
from convert_glsl_es3 import convert_glsl120_to_es300
def convert_shader_to_cpp(source_path, var_name):

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit c5aaa19ca9cffe739d9ccbc99003bc712eda031e
Subproject commit 915a2ac4d87afc7dbd8005dab3238cb9bbd0a52c