feat(webgl): Replace legacy GL with modern OpenGL ES 3.0

Major refactoring to remove ALL legacy OpenGL immediate mode calls from
WEBGL_GAL and replace them with WebGL 2.0/OpenGL ES 3.0 compatible code.

Key changes:
- Add MVP matrix uniform support (replaces glMatrixMode/glOrtho/glLoadMatrix)
- Add matrix math helpers (computeOrthoMatrix, multiplyMatrix4x4, etc.)
- Replace glBegin/glEnd/glVertex with vertex manager pattern
- Rewrite DrawCursor to use vertex manager and DrawLine() calls
- Rewrite DrawBitmap to use vertex manager with SHADER_FONT mode
- Replace glEnableClientState with glVertexAttribPointer
- Replace glDrawBuffer with glDrawBuffers (WebGL 2.0 API)
- Add fullscreen_quad.cpp/h for compositor Present()
- Update SHADER class with mat4 uniform support
- Add MultiplyMatrix() to vertex_manager for Transform()
- Remove all legacy GL stubs from wasm_stubs.cpp

Build now completes with FULL_ES3 mode (no LEGACY_GL_EMULATION).
Remaining issue: wxWidgets GL library calls glColor3f internally.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Viktor Vaczi 2026-01-07 23:33:49 +01:00
commit d2f1a496f5
27 changed files with 840 additions and 349 deletions

View file

@ -11,61 +11,187 @@ import sys
import re
def convert_glsl120_to_es100(shader_source, is_fragment_shader):
def convert_glsl120_to_es300(shader_source, is_fragment_shader):
"""
Convert GLSL 1.20 (OpenGL 2.1) shader to GLSL ES 1.00 (WebGL 1.0).
Convert GLSL 1.20 (OpenGL 2.1) shader to GLSL ES 3.00 (WebGL 2.0).
GLSL ES 1.00 is similar to GLSL 1.20 but:
- No #version directive needed (defaults to 100)
- Needs precision qualifiers
- attribute/varying are the correct keywords (not in/out)
- gl_FragColor is correct (not custom output)
- texture2D is correct (not texture)
- STRICT: int * float not allowed - need explicit float literals
Main changes needed:
- Remove #version 120 (ES 1.00 is default)
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 = []
added_precision = False
version_replaced = False
# Pattern to find integer literals multiplied by variables
# Matches patterns like "2 * var" or "2* var" etc
# 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()
# Skip the #version directive - ES 1.00 doesn't use it
# 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'):
# Add precision qualifiers instead
if not added_precision:
result.append('// GLSL ES 1.00 (WebGL 1.0) - converted from GLSL 1.20')
result.append('precision highp float;')
result.append('precision highp int;')
result.append('')
added_precision = True
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"
# Only convert integers that don't already have a decimal point
def fix_int_mult(match):
int_val = match.group(1)
var_start = match.group(2)
# Add .0 to make it a float literal
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 but we haven't added precision yet, add it at the start
if not added_precision:
prefix = ['// GLSL ES 1.00 (WebGL 1.0)', 'precision highp float;', 'precision highp int;', '']
result = prefix + result
# 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)
@ -75,11 +201,10 @@ def convert_shader_to_cpp(source_path, var_name):
with open(source_path, 'rb') as f:
data = f.read()
# Convert GLSL 1.20 to GLSL ES 1.00 for WebGL 1.0 compatibility
# (ES 1.00 is closer to GLSL 1.20 and doesn't conflict with Emscripten prepends)
# Convert GLSL 1.20 to GLSL ES 3.00 for WebGL 2.0
shader_source = data.decode('utf-8')
is_fragment = '_frag' in var_name or 'frag' in source_path.lower()
shader_source = convert_glsl120_to_es100(shader_source, is_fragment)
shader_source = convert_glsl120_to_es300(shader_source, is_fragment)
data = shader_source.encode('utf-8')
# Convert to hex array