Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions fixtures/html/webgl-conformance/test-shader-version-fix.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGL Standards-Compliant GLSL Version Handling Test</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.test-result { margin: 10px 0; padding: 8px; border-radius: 4px; }
.pass { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
.fail { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
.test-section { margin: 20px 0; border: 1px solid #ddd; padding: 15px; border-radius: 4px; }
pre { background: #f8f9fa; padding: 10px; border-radius: 4px; overflow-x: auto; }
</style>
</head>
<body>
<h1>WebGL Standards-Compliant GLSL Version Handling Test</h1>
<p>This test validates WebGL standards-compliant GLSL version handling:</p>
<ul>
<li><strong>WebGL 1.0</strong>: Version directives are optional (should default to GLSL ES 1.00)</li>
<li><strong>WebGL 2.0</strong>: Version directives are required (#version 300 es)</li>
</ul>

<div id="results"></div>

<script>
const results = document.getElementById('results');

function addResult(message, isPass, section = null) {
const div = document.createElement('div');
div.className = `test-result ${isPass ? 'pass' : 'fail'}`;
div.innerHTML = `${isPass ? '✅' : '❌'} ${message}`;

if (section) {
section.appendChild(div);
} else {
results.appendChild(div);
}
}

function addSection(title) {
const section = document.createElement('div');
section.className = 'test-section';
const header = document.createElement('h2');
header.textContent = title;
section.appendChild(header);
results.appendChild(section);
return section;
}

function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);

if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compilation error:', gl.getShaderInfoLog(shader));
return null;
}
return shader;
}

function testShaderCompilation(gl, shaderType, source, testName, section) {
try {
const shader = createShader(gl, shaderType, source);
if (shader) {
addResult(`${testName}: Shader compiled successfully`, true, section);
gl.deleteShader(shader);
return true;
} else {
const error = gl.getShaderInfoLog(createShader(gl, shaderType, source));
addResult(`${testName}: Compilation failed - ${error}`, false, section);
return false;
}
} catch (e) {
addResult(`${testName}: Exception - ${e.message}`, false, section);
return false;
}
}

function runTests() {
const gl = navigator.gl;
if (!gl) {
addResult('WebGL not supported - test cannot run', false);
return;
}

addResult(`WebGL context available: ${gl.constructor.name}`, true);
addResult(`WebGL version: ${gl.getParameter(gl.VERSION)}`, true);

// Test 1: WebGL 1.0 shaders without version directive
// Per WebGL spec, these should work WITHOUT version directives
const webgl1Section = addSection('WebGL 1.0 Shaders (should work without #version per WebGL spec)');

// The exact failing case from the issue
const fragmentShaderNoVersion = `
void main() {
gl_FragColor = vec4(0., 1., 0., 1.);
}`;

const vertexShaderNoVersion = `
attribute vec4 position;
void main() {
gl_Position = position;
}`;

testShaderCompilation(gl, gl.VERTEX_SHADER, vertexShaderNoVersion,
'Vertex shader without #version (using attribute)', webgl1Section);
testShaderCompilation(gl, gl.FRAGMENT_SHADER, fragmentShaderNoVersion,
'Fragment shader without #version (using gl_FragColor)', webgl1Section);

// Test 2: WebGL 2.0 style shaders without version directive
// Per WebGL spec, these REQUIRE #version 300 es
if (gl.constructor.name.includes('WebGL2')) {
const webgl2Section = addSection('WebGL 2.0 Shaders (MUST have #version 300 es per WebGL spec)');

const vertexShaderWebGL2 = `
in vec4 position;
void main() {
gl_Position = position;
}`;

const fragmentShaderWebGL2 = `
precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(0., 1., 0., 1.);
}`;

testShaderCompilation(gl, gl.VERTEX_SHADER, vertexShaderWebGL2,
'Vertex shader without #version (using in)', webgl2Section);
testShaderCompilation(gl, gl.FRAGMENT_SHADER, fragmentShaderWebGL2,
'Fragment shader without #version (using out)', webgl2Section);
}

// Test 3: Existing shaders with version directives (should work as before)
const existingSection = addSection('Existing Shaders (should remain unchanged)');

const vertexWithVersion = `
#version 100
attribute vec4 position;
void main() {
gl_Position = position;
}`;

const fragmentWithVersion = `
#version 100
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}`;

testShaderCompilation(gl, gl.VERTEX_SHADER, vertexWithVersion,
'Vertex shader with existing #version 100', existingSection);
testShaderCompilation(gl, gl.FRAGMENT_SHADER, fragmentWithVersion,
'Fragment shader with existing #version 100', existingSection);
}

// Run tests when page loads
document.addEventListener('DOMContentLoaded', runTests);
</script>
</body>
</html>
71 changes: 70 additions & 1 deletion src/client/graphics/webgl_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,71 @@ namespace client_graphics
throw runtime_error(msg); \
}

/// Detect if shader source uses WebGL 2.0 syntax
/// WebGL 2.0 requires #version 300 es per specification, WebGL 1.0 doesn't require version
bool detectWebGL2Syntax(const string &source)
{
// WebGL 2.0 strong indicators (these only exist in WebGL 2.0)
if (source.find("out vec4") != string::npos ||
source.find("out mediump") != string::npos ||
source.find("out lowp") != string::npos ||
source.find("out highp") != string::npos ||
source.find("layout(location") != string::npos)
{
return true;
}

// Check for WebGL 2.0 built-ins - texture() without texture2D()
if (source.find("texture(") != string::npos && source.find("texture2D(") == string::npos)
{
return true;
}

// WebGL 1.0 strong indicators (these are deprecated/removed in WebGL 2.0)
if (source.find("gl_FragColor") != string::npos ||
source.find("gl_FragData") != string::npos ||
source.find("attribute ") != string::npos ||
source.find("varying ") != string::npos ||
source.find("texture2D(") != string::npos ||
source.find("textureCube(") != string::npos)
{
return false;
}

// Check for 'in ' keyword (could be WebGL 2.0, but be more specific)
if (source.find("in vec") != string::npos ||
source.find("in mediump") != string::npos ||
source.find("in lowp") != string::npos ||
source.find("in highp") != string::npos)
{
return true;
}

// Default to WebGL 1.0 for safety
return false;
}

/// Apply WebGL standards-compliant version handling
/// WebGL 1.0: Version directives are optional (defaults to GLSL ES 1.00)
/// WebGL 2.0: Version directives are required (#version 300 es)
string applyWebGLVersionHandling(const string &source)
{
// If shader already has a version directive, leave it unchanged
if (source.find("#version") != string::npos)
{
return source;
}

// WebGL 2.0 requires #version 300 es per specification
if (detectWebGL2Syntax(source))
{
return "#version 300 es\n" + source;
}

// WebGL 1.0 shaders work without version directives per WebGL standard
return source;
}

void WebGLState::Restore(WebGLState &state, shared_ptr<WebGL2Context> context)
{
context->useProgram(state.program.value_or(nullptr));
Expand Down Expand Up @@ -350,7 +415,11 @@ namespace client_graphics

void WebGLContext::shaderSource(shared_ptr<WebGLShader> shader, const string &source)
{
shader->source = GLSLSourcePatcher2::GetPatchedSource(source);
// Apply WebGL standards-compliant version handling first
string processedSource = applyWebGLVersionHandling(source);

// Then apply GLSL patching (reordering of directives, etc.)
shader->source = GLSLSourcePatcher2::GetPatchedSource(processedSource);
auto req = ShaderSourceCommandBufferRequest(shader->id, shader->source);
sendCommandBufferRequest(req);
}
Expand Down