Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
2 changes: 1 addition & 1 deletion crates/jsbindings/css/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub(crate) mod properties;
pub(crate) mod selectors;
pub(crate) mod values;
pub(crate) mod stylesheets;
pub(crate) mod values;
2 changes: 1 addition & 1 deletion crates/jsbindings/css/stylesheets/style_rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl StyleRule {
.borrow()
.to_css(&mut selectors_text)
.expect("Failed to convert selectors to CSS");

Self {
selectors: SelectorList::new(&handle.selectors),
selectors_text,
Expand Down
168 changes: 166 additions & 2 deletions crates/jsbindings/webgl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,72 @@ impl VisitorMut for MyGLSLPatcher {
}
}

/// 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
fn detect_webgl2_syntax(s: &str) -> bool {
// WebGL 2.0 strong indicators (these only exist in WebGL 2.0)
if s.contains("out vec4")
|| s.contains("out mediump")
|| s.contains("out lowp")
|| s.contains("out highp")
|| s.contains("layout(location")
{
return true;
}

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

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

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

// Default to WebGL 1.0 for safety
false
}

fn patch_glsl_source_from_str(s: &str) -> String {
use glsl_lang::{
ast::TranslationUnit, lexer::full::fs::PreprocessorExt, parse::IntoParseBuilderExt,
};

// Apply WebGL standards-compliant version handling first
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 {
// WebGL 1.0 shaders work without version directives per WebGL standard
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 @@ -235,4 +286,117 @@ vec3 test() {
"#
)
}

#[test]
fn test_patch_glsl_source_missing_version_webgl1_fragment() {
let source_str = r#"
precision mediump float;
void main() {
gl_FragColor = vec4(0., 1., 0., 1.);
}
"#;
let patched_source_str = patch_glsl_source_from_str(source_str);
// WebGL 1.0 shaders should remain unchanged (no version injection)
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test comment doesn't match the actual behavior. The test shows WebGL 1.0 shaders DO remain unchanged, but the logic in ensure_version_and_extension_order always injects #version 300 es regardless of WebGL version detection.

Copilot uses AI. Check for mistakes.
assert_eq!(
patched_source_str,
r#"precision mediump float;
void main() {
gl_FragColor = vec4(0., 1., 0., 1.);
}
"#
);
}

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

#[test]
fn test_patch_glsl_source_missing_version_webgl1_vertex() {
let source_str = r#"
attribute vec4 a_position;
varying vec2 v_texcoord;
void main() {
gl_Position = a_position;
v_texcoord = a_position.xy;
}
"#;
let patched_source_str = patch_glsl_source_from_str(source_str);
// WebGL 1.0 vertex shader should remain unchanged
assert_eq!(
patched_source_str,
r#"attribute vec4 a_position;
varying vec2 v_texcoord;
void main() {
gl_Position = a_position;
v_texcoord = a_position.xy;
}
"#
);
}

#[test]
fn test_patch_glsl_source_missing_version_webgl2_vertex() {
let source_str = r#"
layout(location = 0) in vec4 a_position;
out vec2 v_texcoord;
void main() {
gl_Position = a_position;
v_texcoord = a_position.xy;
}
"#;
let patched_source_str = patch_glsl_source_from_str(source_str);
// WebGL 2.0 vertex shader should get #version 300 es injected
assert_eq!(
patched_source_str,
r#"#version 300 es
layout(location = 0) in vec4 a_position;
out vec2 v_texcoord;
void main() {
gl_Position = a_position;
v_texcoord = a_position.xy;
}
"#
);
}

#[test]
fn test_patch_glsl_source_existing_version_unchanged() {
let source_str = r#"#version 100
precision mediump float;
void main() {
gl_FragColor = vec4(0., 1., 0., 1.);
}
"#;
let patched_source_str = patch_glsl_source_from_str(source_str);
// Existing version should remain unchanged
assert_eq!(
patched_source_str,
r#"#version 100
precision mediump float;
void main() {
gl_FragColor = vec4(0., 1., 0., 1.);
}
"#
);
}
}
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>