Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
161 changes: 156 additions & 5 deletions crates/jsbindings/webgl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,25 @@ fn patch_glsl_source_from_str(s: &str) -> String {
ast::TranslationUnit, lexer::full::fs::PreprocessorExt, parse::IntoParseBuilderExt,
};

// Only inject version directive for WebGL 2.0 shaders where it's required by spec
// WebGL 1.0 shaders should work without version directives per WebGL standard
let source_to_parse = if !s.contains("#version") && detect_webgl2_syntax(s) {
// WebGL 2.0 requires #version 300 es per specification
format!("#version 300 es\n{}", s)
} else {
s.to_string()
};

let mut processor = glsl_lang_pp::processor::fs::StdProcessor::new();
let mut tu: TranslationUnit = processor
.open_source(s, Path::new("."))
.open_source(&source_to_parse, Path::new("."))
.builder()
.parse()
.map(|(mut tu, _, iter)| {
iter.into_directives().inject(&mut tu);
tu
})
.expect(format!("Failed to parse GLSL source: \n{}\n", s).as_str());
.expect(format!("Failed to parse GLSL source: \n{}\n", &source_to_parse).as_str());

let mut my_glsl_patcher = MyGLSLPatcher {};
tu.visit_mut(&mut my_glsl_patcher);
Expand Down Expand Up @@ -108,14 +117,50 @@ fn patch_glsl_source_from_str(s: &str) -> String {
tu.0.splice(0..0, versions_list);
}

let mut s = String::new();
let mut result = String::new();
glsl_transpiler::glsl::show_translation_unit(
&mut s,
&mut result,
&tu,
glsl_transpiler::glsl::FormattingState::default(),
)
.expect("Failed to show GLSL");
s
result
}

/// Detect if shader source uses WebGL 2.0 syntax
fn detect_webgl2_syntax(source: &str) -> bool {
// WebGL 2.0 strong indicators (these only exist in WebGL 2.0)
if source.contains("out vec4") ||
source.contains("out mediump") ||
source.contains("out lowp") ||
source.contains("out highp") ||
source.contains("layout(location") {
return true;
}

// Check for WebGL 2.0 built-ins
if source.contains("texture(") && !source.contains("texture2D(") {
return true;
}

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

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

// Default to WebGL 1.0 if uncertain (safer for compatibility)
false
}

#[cxx::bridge(namespace = "holocron::webgl")]
Expand Down Expand Up @@ -204,6 +249,112 @@ void main() {
)
}

#[test]
fn test_patch_glsl_source_missing_version_webgl1() {
// WebGL 1.0 shaders should NOT get version directive injected (per WebGL spec)
let source_str = r#"void main() {
gl_FragColor = vec4(0., 1., 0., 1.);
}"#;
let patched_source_str = patch_glsl_source_from_str(source_str);
// Should remain unchanged - WebGL 1.0 doesn't require #version
assert_eq!(
patched_source_str,
r#"void main() {
gl_FragColor = vec4(0., 1., 0., 1.);
}
"#
);
}

#[test]
fn test_patch_glsl_source_missing_version_webgl2() {
// WebGL 2.0 shaders SHOULD get #version 300 es injected (required by spec)
let source_str = r#"precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(0., 1., 0., 1.);
}"#;
let patched_source_str = patch_glsl_source_from_str(source_str);
assert_eq!(
patched_source_str,
r#"#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(0., 1., 0., 1.);
}
"#
);
}

#[test]
fn test_patch_glsl_source_missing_version_webgl1_vertex() {
// WebGL 1.0 vertex shader should NOT get version directive injected
let source_str = r#"attribute vec4 position;
varying vec2 vTexCoord;
void main() {
gl_Position = position;
vTexCoord = position.xy;
}"#;
let patched_source_str = patch_glsl_source_from_str(source_str);
// Should remain unchanged - WebGL 1.0 doesn't require #version
assert_eq!(
patched_source_str,
r#"attribute vec4 position;
varying vec2 vTexCoord;
void main() {
gl_Position = position;
vTexCoord = position.xy;
}
"#
);
}

#[test]
fn test_patch_glsl_source_missing_version_webgl2_vertex() {
// WebGL 2.0 vertex shader SHOULD get #version 300 es injected
let source_str = r#"in vec4 position;
in vec2 texCoord;
out vec2 vTexCoord;
void main() {
gl_Position = position;
vTexCoord = texCoord;
}"#;
let patched_source_str = patch_glsl_source_from_str(source_str);
assert_eq!(
patched_source_str,
r#"#version 300 es
in vec4 position;
in vec2 texCoord;
out vec2 vTexCoord;
void main() {
gl_Position = position;
vTexCoord = texCoord;
}
"#
);
}

#[test]
fn test_patch_glsl_source_existing_version_unchanged() {
// Test that existing version directives are preserved and reordered
let source_str = r#"precision mediump float;
#version 300 es
void main() {
gl_FragColor = vec4(0., 1., 0., 1.);
}"#;
let patched_source_str = patch_glsl_source_from_str(source_str);
assert_eq!(
patched_source_str,
r#"#version 300 es
precision mediump float;
void main() {
gl_FragColor = vec4(0., 1., 0., 1.);
}
"#
);
}

#[test]
#[ignore]
fn test_patch_glsl_source_elif_expand() {
Expand Down
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>
Loading