From 6049f8ce7360409842fe8a5f8fcb74940c732f42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Sep 2025 03:06:03 +0000 Subject: [PATCH 01/15] Initial plan From b54d9c819e3e3392b8fad4877e9a4c0ca96e575d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Sep 2025 03:20:57 +0000 Subject: [PATCH 02/15] Fix GLSL shader compilation by injecting missing #version directive Co-authored-by: EndlessJour9527 <155411404+EndlessJour9527@users.noreply.github.com> --- crates/jsbindings/webgl.rs | 83 +++++++++++++++++-- test-shader-version-fix.html | 156 +++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 test-shader-version-fix.html diff --git a/crates/jsbindings/webgl.rs b/crates/jsbindings/webgl.rs index b19796942..64644e0fe 100644 --- a/crates/jsbindings/webgl.rs +++ b/crates/jsbindings/webgl.rs @@ -67,16 +67,28 @@ fn patch_glsl_source_from_str(s: &str) -> String { ast::TranslationUnit, lexer::full::fs::PreprocessorExt, parse::IntoParseBuilderExt, }; + // Check if version directive is missing and inject appropriate version + let source_to_parse = if !s.contains("#version") { + let version_directive = if detect_webgl2_syntax(s) { + "#version 300 es\n" + } else { + "#version 100\n" + }; + format!("{}{}", version_directive, 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); @@ -108,14 +120,37 @@ 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 indicators + if source.contains("out vec4") || + source.contains("in ") || + source.contains("layout(") || + source.contains("uniform") && source.contains("buffer") || + source.contains("texture(") { + return true; + } + + // WebGL 1.0 indicators + if source.contains("gl_FragColor") || + source.contains("attribute ") || + source.contains("varying ") || + source.contains("texture2D(") { + return false; + } + + // Default to WebGL 1.0 if uncertain + false } #[cxx::bridge(namespace = "holocron::webgl")] @@ -204,6 +239,44 @@ void main() { ) } + #[test] + fn test_patch_glsl_source_missing_version_webgl1() { + // Test case for the issue: shader without #version directive should get #version 100 + let source_str = r#"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 100 +void main() { + gl_FragColor = vec4(0., 1., 0., 1.); +} +"# + ); + } + + #[test] + fn test_patch_glsl_source_missing_version_webgl2() { + // Test case for WebGL 2.0 shader without #version directive + 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] #[ignore] fn test_patch_glsl_source_elif_expand() { diff --git a/test-shader-version-fix.html b/test-shader-version-fix.html new file mode 100644 index 000000000..310cdc4db --- /dev/null +++ b/test-shader-version-fix.html @@ -0,0 +1,156 @@ + + + + + + GLSL Version Directive Fix Test + + + +

GLSL Version Directive Fix Test

+

This test validates that shaders without #version directives are properly handled by the JSAR Runtime GLSL patcher.

+ +
+ + + + \ No newline at end of file From 9db99f4b8cfffb1693d9e8a4d394135c5e451521 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Sep 2025 03:24:20 +0000 Subject: [PATCH 03/15] Refine GLSL version detection and add comprehensive test coverage Co-authored-by: EndlessJour9527 <155411404+EndlessJour9527@users.noreply.github.com> --- crates/jsbindings/webgl.rs | 97 +++++++++++++++++-- .../test-shader-version-fix.html | 0 2 files changed, 89 insertions(+), 8 deletions(-) rename test-shader-version-fix.html => fixtures/html/webgl-conformance/test-shader-version-fix.html (100%) diff --git a/crates/jsbindings/webgl.rs b/crates/jsbindings/webgl.rs index 64644e0fe..4cfc4504f 100644 --- a/crates/jsbindings/webgl.rs +++ b/crates/jsbindings/webgl.rs @@ -132,24 +132,37 @@ fn patch_glsl_source_from_str(s: &str) -> String { /// Detect if shader source uses WebGL 2.0 syntax fn detect_webgl2_syntax(source: &str) -> bool { - // WebGL 2.0 indicators + // WebGL 2.0 strong indicators (these only exist in WebGL 2.0) if source.contains("out vec4") || - source.contains("in ") || - source.contains("layout(") || - source.contains("uniform") && source.contains("buffer") || - source.contains("texture(") { + source.contains("out mediump") || + source.contains("out lowp") || + source.contains("out highp") || + source.contains("layout(location") { return true; } - // WebGL 1.0 indicators + // 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("texture2D(") || + source.contains("textureCube(") { return false; } - // Default to WebGL 1.0 if uncertain + // 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 } @@ -277,6 +290,74 @@ void main() { ); } + #[test] + fn test_patch_glsl_source_missing_version_webgl1_vertex() { + // Test WebGL 1.0 vertex shader without version directive + 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); + assert_eq!( + patched_source_str, + r#"#version 100 +attribute vec4 position; +varying vec2 vTexCoord; +void main() { + gl_Position = position; + vTexCoord = position.xy; +} +"# + ); + } + + #[test] + fn test_patch_glsl_source_missing_version_webgl2_vertex() { + // Test WebGL 2.0 vertex shader without version directive + 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() { diff --git a/test-shader-version-fix.html b/fixtures/html/webgl-conformance/test-shader-version-fix.html similarity index 100% rename from test-shader-version-fix.html rename to fixtures/html/webgl-conformance/test-shader-version-fix.html From caf493ca7dc3bb8f28a622ebfc2c9d7e6b2590cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Sep 2025 09:23:56 +0000 Subject: [PATCH 04/15] Make GLSL version handling WebGL standards-compliant Co-authored-by: EndlessJour9527 <155411404+EndlessJour9527@users.noreply.github.com> --- crates/jsbindings/webgl.rs | 29 +++++++++---------- .../test-shader-version-fix.html | 18 ++++++++---- src/renderer/render_api_opengles.cpp | 9 ++++++ 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/crates/jsbindings/webgl.rs b/crates/jsbindings/webgl.rs index 4cfc4504f..b2d360cd9 100644 --- a/crates/jsbindings/webgl.rs +++ b/crates/jsbindings/webgl.rs @@ -67,14 +67,11 @@ fn patch_glsl_source_from_str(s: &str) -> String { ast::TranslationUnit, lexer::full::fs::PreprocessorExt, parse::IntoParseBuilderExt, }; - // Check if version directive is missing and inject appropriate version - let source_to_parse = if !s.contains("#version") { - let version_directive = if detect_webgl2_syntax(s) { - "#version 300 es\n" - } else { - "#version 100\n" - }; - format!("{}{}", version_directive, s) + // 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() }; @@ -254,15 +251,15 @@ void main() { #[test] fn test_patch_glsl_source_missing_version_webgl1() { - // Test case for the issue: shader without #version directive should get #version 100 + // 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#"#version 100 -void main() { + r#"void main() { gl_FragColor = vec4(0., 1., 0., 1.); } "# @@ -271,7 +268,7 @@ void main() { #[test] fn test_patch_glsl_source_missing_version_webgl2() { - // Test case for WebGL 2.0 shader without #version directive + // 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() { @@ -292,7 +289,7 @@ void main() { #[test] fn test_patch_glsl_source_missing_version_webgl1_vertex() { - // Test WebGL 1.0 vertex shader without version directive + // WebGL 1.0 vertex shader should NOT get version directive injected let source_str = r#"attribute vec4 position; varying vec2 vTexCoord; void main() { @@ -300,10 +297,10 @@ void main() { 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#"#version 100 -attribute vec4 position; + r#"attribute vec4 position; varying vec2 vTexCoord; void main() { gl_Position = position; @@ -315,7 +312,7 @@ void main() { #[test] fn test_patch_glsl_source_missing_version_webgl2_vertex() { - // Test WebGL 2.0 vertex shader without version directive + // WebGL 2.0 vertex shader SHOULD get #version 300 es injected let source_str = r#"in vec4 position; in vec2 texCoord; out vec2 vTexCoord; diff --git a/fixtures/html/webgl-conformance/test-shader-version-fix.html b/fixtures/html/webgl-conformance/test-shader-version-fix.html index 310cdc4db..5a0f1ff3a 100644 --- a/fixtures/html/webgl-conformance/test-shader-version-fix.html +++ b/fixtures/html/webgl-conformance/test-shader-version-fix.html @@ -3,7 +3,7 @@ - GLSL Version Directive Fix Test + WebGL Standards-Compliant GLSL Version Handling Test -

GLSL Version Directive Fix Test

-

This test validates that shaders without #version directives are properly handled by the JSAR Runtime GLSL patcher.

+

WebGL Standards-Compliant GLSL Version Handling Test

+

This test validates WebGL standards-compliant GLSL version handling:

+
@@ -84,8 +88,9 @@

GLSL Version Directive Fix Test

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 - const webgl1Section = addSection('WebGL 1.0 Shaders (should get #version 100)'); + // 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 = ` @@ -105,8 +110,9 @@

GLSL Version Directive Fix Test

'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 (should get #version 300 es)'); + const webgl2Section = addSection('WebGL 2.0 Shaders (MUST have #version 300 es per WebGL spec)'); const vertexShaderWebGL2 = ` in vec4 position; diff --git a/src/renderer/render_api_opengles.cpp b/src/renderer/render_api_opengles.cpp index 606c09cbf..e88b4d9f5 100644 --- a/src/renderer/render_api_opengles.cpp +++ b/src/renderer/render_api_opengles.cpp @@ -740,6 +740,15 @@ class RHI_OpenGL : public TrRenderHardwareInterface { string line; size_t pos = 0; + bool hasVersionDirective = source.find("#version") != string::npos; + +#ifdef __APPLE__ + // On macOS, we need to ensure all shaders have a version directive for compatibility + if (!hasVersionDirective) + { + fixedSource += "#version 410 core\n"; + } +#endif while (pos < source.size()) { From e7ca3f79b535c65620962a354b516884ea8976d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Sep 2025 11:26:30 +0000 Subject: [PATCH 05/15] Implement ANGLE-style WebGL 1.0 to desktop OpenGL translation Co-authored-by: EndlessJour9527 <155411404+EndlessJour9527@users.noreply.github.com> --- src/renderer/render_api_opengles.cpp | 115 +++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 8 deletions(-) diff --git a/src/renderer/render_api_opengles.cpp b/src/renderer/render_api_opengles.cpp index e88b4d9f5..0e31b05a1 100644 --- a/src/renderer/render_api_opengles.cpp +++ b/src/renderer/render_api_opengles.cpp @@ -50,6 +50,108 @@ void getline(const string &input, string &line, size_t &pos, char delim = '\n') } } +/** + * Patch WebGL/GLSL ES shader source to work with desktop OpenGL Core Profile. + * This implements the same kind of translation that browsers like Chrome do via ANGLE. + * + * Translates: + * - attribute -> in (vertex shaders) + * - varying -> out (vertex shaders) / in (fragment shaders) + * - gl_FragColor -> custom out vec4 fragColor + * - removes precision qualifiers (not valid in desktop GL) + * - adds appropriate #version directive + */ +string PatchWebGLShaderForDesktopGL(const string &source, GLuint shaderHandle) +{ + // Query the shader type from OpenGL + GLint shaderType; + glGetShaderiv(shaderHandle, GL_SHADER_TYPE, &shaderType); + + istringstream ss(source); + string line, patched; + bool hasFragColor = false; + bool hasVersionDirective = source.find("#version") != string::npos; + + // Add version directive if not present + if (!hasVersionDirective) + { + patched += "#version 410 core\n"; + } + + while (getline(ss, line)) + { + string processedLine = line; + + // Replace existing #version with desktop GL version + if (processedLine.find("#version") != string::npos) + { + processedLine = "#version 410 core"; + } + + // Remove precision qualifiers (not valid in desktop GL core) + if (processedLine.find("precision ") != string::npos) + { + continue; // Skip precision lines entirely + } + + // Replace WebGL 1.0 keywords with desktop GL equivalents + size_t pos; + + // attribute -> in (vertex shaders only) + if (shaderType == GL_VERTEX_SHADER && (pos = processedLine.find("attribute ")) != string::npos) + { + processedLine.replace(pos, 10, "in "); + } + + // varying -> out (vertex) / in (fragment) + if ((pos = processedLine.find("varying ")) != string::npos) + { + if (shaderType == GL_VERTEX_SHADER) + { + processedLine.replace(pos, 8, "out "); + } + else + { + processedLine.replace(pos, 8, "in "); + } + } + + // gl_FragColor -> fragColor (fragment shaders only) + if (shaderType == GL_FRAGMENT_SHADER && (pos = processedLine.find("gl_FragColor")) != string::npos) + { + hasFragColor = true; + processedLine.replace(pos, 12, "fragColor"); + } + + // gl_FragData -> fragColor (fragment shaders only) + if (shaderType == GL_FRAGMENT_SHADER && (pos = processedLine.find("gl_FragData")) != string::npos) + { + // This is more complex as it involves array access, but for now replace with fragColor + processedLine.replace(pos, 11, "fragColor"); + } + + patched += processedLine + "\n"; + } + + // Add output declaration for fragment color if gl_FragColor was used + if (shaderType == GL_FRAGMENT_SHADER && hasFragColor) + { + // Insert after version directive but before other content + size_t versionPos = patched.find("#version 410 core\n"); + if (versionPos != string::npos) + { + size_t insertPos = versionPos + strlen("#version 410 core\n"); + patched.insert(insertPos, "out vec4 fragColor;\n"); + } + else + { + patched = "out vec4 fragColor;\n" + patched; + } + } + + return patched; +} + class RHI_OpenGL : public TrRenderHardwareInterface { private: @@ -743,27 +845,24 @@ class RHI_OpenGL : public TrRenderHardwareInterface bool hasVersionDirective = source.find("#version") != string::npos; #ifdef __APPLE__ - // On macOS, we need to ensure all shaders have a version directive for compatibility + // On macOS, we need to translate WebGL/GLSL ES shaders to desktop OpenGL Core Profile + fixedSource = PatchWebGLShaderForDesktopGL(source, shader); +#else + // On other platforms, use the original approach if (!hasVersionDirective) { fixedSource += "#version 410 core\n"; } -#endif while (pos < source.size()) { getline(source, line, pos); string newLine = line; -#ifdef __APPLE__ - /** - * FIXME(Yorkie): This is a workaround for the shader source on macOS, we need to replace the version to 410 core - * directly, a better solution is to use the shader preprocessor like google/angle to handle this. - */ if (line.find("#version") != string::npos) newLine = "#version 410 core"; -#endif fixedSource += newLine + "\n"; } +#endif } const char *sourceStr = fixedSource.c_str(); From 84dae549570a7d0087eaa6bb38e66263687a9303 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Sep 2025 12:02:36 +0000 Subject: [PATCH 06/15] Fix gl_FragData array access translation for desktop OpenGL compatibility Co-authored-by: EndlessJour9527 <155411404+EndlessJour9527@users.noreply.github.com> --- src/renderer/render_api_opengles.cpp | 52 +++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/src/renderer/render_api_opengles.cpp b/src/renderer/render_api_opengles.cpp index 0e31b05a1..224db65ac 100644 --- a/src/renderer/render_api_opengles.cpp +++ b/src/renderer/render_api_opengles.cpp @@ -58,6 +58,8 @@ void getline(const string &input, string &line, size_t &pos, char delim = '\n') * - attribute -> in (vertex shaders) * - varying -> out (vertex shaders) / in (fragment shaders) * - gl_FragColor -> custom out vec4 fragColor + * - gl_FragData[index] -> fragColor (single output, no MRT support yet) + * - fragColor[index] -> fragColor (fix array access patterns) * - removes precision qualifiers (not valid in desktop GL) * - adds appropriate #version directive */ @@ -123,11 +125,53 @@ string PatchWebGLShaderForDesktopGL(const string &source, GLuint shaderHandle) processedLine.replace(pos, 12, "fragColor"); } - // gl_FragData -> fragColor (fragment shaders only) - if (shaderType == GL_FRAGMENT_SHADER && (pos = processedLine.find("gl_FragData")) != string::npos) + // gl_FragData[0] -> fragColor (fragment shaders only) + // Handle the common case of gl_FragData[0] which should become fragColor (single output) + if (shaderType == GL_FRAGMENT_SHADER && (pos = processedLine.find("gl_FragData[0]")) != string::npos) { - // This is more complex as it involves array access, but for now replace with fragColor - processedLine.replace(pos, 11, "fragColor"); + hasFragColor = true; + processedLine.replace(pos, 14, "fragColor"); + } + // Handle general gl_FragData array access (convert to fragColor for single output) + else if (shaderType == GL_FRAGMENT_SHADER && (pos = processedLine.find("gl_FragData")) != string::npos) + { + hasFragColor = true; + // Look for array access pattern like gl_FragData[index] + size_t arrayStart = processedLine.find("[", pos); + if (arrayStart != string::npos) + { + size_t arrayEnd = processedLine.find("]", arrayStart); + if (arrayEnd != string::npos) + { + // Replace gl_FragData[index] with fragColor (assume single output for now) + processedLine.replace(pos, arrayEnd - pos + 1, "fragColor"); + } + } + else + { + // No array access, just replace gl_FragData with fragColor + processedLine.replace(pos, 11, "fragColor"); + } + } + + // Handle any remaining fragColor[0] patterns that might have been missed + // This covers cases where gl_FragData[0] might have been partially processed + if (shaderType == GL_FRAGMENT_SHADER && (pos = processedLine.find("fragColor[0]")) != string::npos) + { + hasFragColor = true; + processedLine.replace(pos, 12, "fragColor"); + } + // Handle other fragColor array access patterns (for single output case) + else if (shaderType == GL_FRAGMENT_SHADER && processedLine.find("fragColor[") != string::npos) + { + hasFragColor = true; + size_t fragPos = processedLine.find("fragColor["); + size_t arrayEnd = processedLine.find("]", fragPos); + if (arrayEnd != string::npos) + { + // Replace fragColor[index] with fragColor (assume single output) + processedLine.replace(fragPos, arrayEnd - fragPos + 1, "fragColor"); + } } patched += processedLine + "\n"; From f97f4715665a37664e48dc0cb13e5b3645c1bf84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Sep 2025 12:12:59 +0000 Subject: [PATCH 07/15] Move version patch logic from Rust to C++ as requested Co-authored-by: EndlessJour9527 <155411404+EndlessJour9527@users.noreply.github.com> --- crates/jsbindings/webgl.rs | 161 +-------------------------- src/renderer/render_api_opengles.cpp | 64 ++++++++++- 2 files changed, 66 insertions(+), 159 deletions(-) diff --git a/crates/jsbindings/webgl.rs b/crates/jsbindings/webgl.rs index b2d360cd9..b19796942 100644 --- a/crates/jsbindings/webgl.rs +++ b/crates/jsbindings/webgl.rs @@ -67,25 +67,16 @@ 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(&source_to_parse, Path::new(".")) + .open_source(s, Path::new(".")) .builder() .parse() .map(|(mut tu, _, iter)| { iter.into_directives().inject(&mut tu); tu }) - .expect(format!("Failed to parse GLSL source: \n{}\n", &source_to_parse).as_str()); + .expect(format!("Failed to parse GLSL source: \n{}\n", s).as_str()); let mut my_glsl_patcher = MyGLSLPatcher {}; tu.visit_mut(&mut my_glsl_patcher); @@ -117,50 +108,14 @@ fn patch_glsl_source_from_str(s: &str) -> String { tu.0.splice(0..0, versions_list); } - let mut result = String::new(); + let mut s = String::new(); glsl_transpiler::glsl::show_translation_unit( - &mut result, + &mut s, &tu, glsl_transpiler::glsl::FormattingState::default(), ) .expect("Failed to show GLSL"); - 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 + s } #[cxx::bridge(namespace = "holocron::webgl")] @@ -249,112 +204,6 @@ 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() { diff --git a/src/renderer/render_api_opengles.cpp b/src/renderer/render_api_opengles.cpp index 224db65ac..b2de2e694 100644 --- a/src/renderer/render_api_opengles.cpp +++ b/src/renderer/render_api_opengles.cpp @@ -50,6 +50,52 @@ void getline(const string &input, string &line, size_t &pos, char delim = '\n') } } +/** + * 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 + 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; +} + /** * Patch WebGL/GLSL ES shader source to work with desktop OpenGL Core Profile. * This implements the same kind of translation that browsers like Chrome do via ANGLE. @@ -892,18 +938,30 @@ class RHI_OpenGL : public TrRenderHardwareInterface // On macOS, we need to translate WebGL/GLSL ES shaders to desktop OpenGL Core Profile fixedSource = PatchWebGLShaderForDesktopGL(source, shader); #else - // On other platforms, use the original approach + // On other platforms, implement WebGL standards-compliant version handling if (!hasVersionDirective) { - fixedSource += "#version 410 core\n"; + if (DetectWebGL2Syntax(source)) + { + // WebGL 2.0 requires #version 300 es per specification + fixedSource += "#version 300 es\n"; + } + // WebGL 1.0 shaders work without version directives per WebGL standard + // No version injection needed for WebGL 1.0 } while (pos < source.size()) { getline(source, line, pos); string newLine = line; + // Replace existing #version with appropriate version for backend if (line.find("#version") != string::npos) - newLine = "#version 410 core"; + { + if (DetectWebGL2Syntax(source)) + newLine = "#version 300 es"; + else + newLine = "#version 100"; // WebGL 1.0 default + } fixedSource += newLine + "\n"; } #endif From 77bba30d69616f21f631b73ee27dec332b51e601 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 03:00:56 +0000 Subject: [PATCH 08/15] Implement client-side WebGL version handling in MyGLSLPatcher Co-authored-by: EndlessJour9527 <155411404+EndlessJour9527@users.noreply.github.com> --- crates/jsbindings/webgl.rs | 167 ++++++++++++++++++++++++++- src/renderer/render_api_opengles.cpp | 78 +------------ 2 files changed, 164 insertions(+), 81 deletions(-) diff --git a/crates/jsbindings/webgl.rs b/crates/jsbindings/webgl.rs index b19796942..46892bf82 100644 --- a/crates/jsbindings/webgl.rs +++ b/crates/jsbindings/webgl.rs @@ -67,16 +67,27 @@ fn patch_glsl_source_from_str(s: &str) -> String { ast::TranslationUnit, lexer::full::fs::PreprocessorExt, parse::IntoParseBuilderExt, }; + // 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) + 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); @@ -108,14 +119,51 @@ 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 +/// WebGL 2.0 requires #version 300 es per specification, WebGL 1.0 doesn't require version +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 for safety + false } #[cxx::bridge(namespace = "holocron::webgl")] @@ -232,6 +280,115 @@ vec3 test() { vec3 test() { return vec3(1., 0., 0.); } +"# + ) + } + + #[test] + fn test_patch_glsl_source_missing_version_webgl1() { + // WebGL 1.0 fragment shader without version - should remain unchanged per spec + 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); + 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() { + // WebGL 2.0 fragment shader without version - should get #version 300 es + 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); + 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() { + // WebGL 1.0 vertex shader without version - should remain unchanged + 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); + 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() { + // WebGL 2.0 vertex shader without version - should get #version 300 es + 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); + 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() { + // Shader with existing version directive should remain unchanged + let source_str = r#" +#version 100 +precision mediump float; +void main() { + gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0); +}"#; + let patched_source_str = patch_glsl_source_from_str(source_str); + assert_eq!( + patched_source_str, + r#"#version 100 +precision mediump float; +void main() { + gl_FragColor = vec4(.5, .5, .5, 1.); +} "# ) } diff --git a/src/renderer/render_api_opengles.cpp b/src/renderer/render_api_opengles.cpp index b2de2e694..3f6a00bee 100644 --- a/src/renderer/render_api_opengles.cpp +++ b/src/renderer/render_api_opengles.cpp @@ -50,52 +50,6 @@ void getline(const string &input, string &line, size_t &pos, char delim = '\n') } } -/** - * 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 - 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; -} - /** * Patch WebGL/GLSL ES shader source to work with desktop OpenGL Core Profile. * This implements the same kind of translation that browsers like Chrome do via ANGLE. @@ -930,40 +884,12 @@ class RHI_OpenGL : public TrRenderHardwareInterface string fixedSource; { - string line; - size_t pos = 0; - bool hasVersionDirective = source.find("#version") != string::npos; - #ifdef __APPLE__ // On macOS, we need to translate WebGL/GLSL ES shaders to desktop OpenGL Core Profile fixedSource = PatchWebGLShaderForDesktopGL(source, shader); #else - // On other platforms, implement WebGL standards-compliant version handling - if (!hasVersionDirective) - { - if (DetectWebGL2Syntax(source)) - { - // WebGL 2.0 requires #version 300 es per specification - fixedSource += "#version 300 es\n"; - } - // WebGL 1.0 shaders work without version directives per WebGL standard - // No version injection needed for WebGL 1.0 - } - - while (pos < source.size()) - { - getline(source, line, pos); - string newLine = line; - // Replace existing #version with appropriate version for backend - if (line.find("#version") != string::npos) - { - if (DetectWebGL2Syntax(source)) - newLine = "#version 300 es"; - else - newLine = "#version 100"; // WebGL 1.0 default - } - fixedSource += newLine + "\n"; - } + // On other platforms, pass through the source as-is since client-side handles version logic + fixedSource = source; #endif } From a347c64910aff909f8537ac302a463cd27c86e63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 03:33:50 +0000 Subject: [PATCH 09/15] Move WebGL version handling to client-side WebGLContext as requested Co-authored-by: EndlessJour9527 <155411404+EndlessJour9527@users.noreply.github.com> --- crates/jsbindings/webgl.rs | 167 +------------------------- src/client/graphics/webgl_context.cpp | 71 ++++++++++- src/renderer/render_api_opengles.cpp | 166 +++---------------------- 3 files changed, 90 insertions(+), 314 deletions(-) diff --git a/crates/jsbindings/webgl.rs b/crates/jsbindings/webgl.rs index 46892bf82..b19796942 100644 --- a/crates/jsbindings/webgl.rs +++ b/crates/jsbindings/webgl.rs @@ -67,27 +67,16 @@ fn patch_glsl_source_from_str(s: &str) -> String { ast::TranslationUnit, lexer::full::fs::PreprocessorExt, parse::IntoParseBuilderExt, }; - // 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) - 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(&source_to_parse, Path::new(".")) + .open_source(s, Path::new(".")) .builder() .parse() .map(|(mut tu, _, iter)| { iter.into_directives().inject(&mut tu); tu }) - .expect(format!("Failed to parse GLSL source: \n{}\n", &source_to_parse).as_str()); + .expect(format!("Failed to parse GLSL source: \n{}\n", s).as_str()); let mut my_glsl_patcher = MyGLSLPatcher {}; tu.visit_mut(&mut my_glsl_patcher); @@ -119,51 +108,14 @@ fn patch_glsl_source_from_str(s: &str) -> String { tu.0.splice(0..0, versions_list); } - let mut result = String::new(); + let mut s = String::new(); glsl_transpiler::glsl::show_translation_unit( - &mut result, + &mut s, &tu, glsl_transpiler::glsl::FormattingState::default(), ) .expect("Failed to show GLSL"); - result -} - -/// 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(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 for safety - false + s } #[cxx::bridge(namespace = "holocron::webgl")] @@ -280,115 +232,6 @@ vec3 test() { vec3 test() { return vec3(1., 0., 0.); } -"# - ) - } - - #[test] - fn test_patch_glsl_source_missing_version_webgl1() { - // WebGL 1.0 fragment shader without version - should remain unchanged per spec - 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); - 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() { - // WebGL 2.0 fragment shader without version - should get #version 300 es - 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); - 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() { - // WebGL 1.0 vertex shader without version - should remain unchanged - 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); - 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() { - // WebGL 2.0 vertex shader without version - should get #version 300 es - 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); - 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() { - // Shader with existing version directive should remain unchanged - let source_str = r#" -#version 100 -precision mediump float; -void main() { - gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0); -}"#; - let patched_source_str = patch_glsl_source_from_str(source_str); - assert_eq!( - patched_source_str, - r#"#version 100 -precision mediump float; -void main() { - gl_FragColor = vec4(.5, .5, .5, 1.); -} "# ) } diff --git a/src/client/graphics/webgl_context.cpp b/src/client/graphics/webgl_context.cpp index 07ab5957e..9f23a0b55 100644 --- a/src/client/graphics/webgl_context.cpp +++ b/src/client/graphics/webgl_context.cpp @@ -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 context) { context->useProgram(state.program.value_or(nullptr)); @@ -350,7 +415,11 @@ namespace client_graphics void WebGLContext::shaderSource(shared_ptr 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); } diff --git a/src/renderer/render_api_opengles.cpp b/src/renderer/render_api_opengles.cpp index 3f6a00bee..606c09cbf 100644 --- a/src/renderer/render_api_opengles.cpp +++ b/src/renderer/render_api_opengles.cpp @@ -50,152 +50,6 @@ void getline(const string &input, string &line, size_t &pos, char delim = '\n') } } -/** - * Patch WebGL/GLSL ES shader source to work with desktop OpenGL Core Profile. - * This implements the same kind of translation that browsers like Chrome do via ANGLE. - * - * Translates: - * - attribute -> in (vertex shaders) - * - varying -> out (vertex shaders) / in (fragment shaders) - * - gl_FragColor -> custom out vec4 fragColor - * - gl_FragData[index] -> fragColor (single output, no MRT support yet) - * - fragColor[index] -> fragColor (fix array access patterns) - * - removes precision qualifiers (not valid in desktop GL) - * - adds appropriate #version directive - */ -string PatchWebGLShaderForDesktopGL(const string &source, GLuint shaderHandle) -{ - // Query the shader type from OpenGL - GLint shaderType; - glGetShaderiv(shaderHandle, GL_SHADER_TYPE, &shaderType); - - istringstream ss(source); - string line, patched; - bool hasFragColor = false; - bool hasVersionDirective = source.find("#version") != string::npos; - - // Add version directive if not present - if (!hasVersionDirective) - { - patched += "#version 410 core\n"; - } - - while (getline(ss, line)) - { - string processedLine = line; - - // Replace existing #version with desktop GL version - if (processedLine.find("#version") != string::npos) - { - processedLine = "#version 410 core"; - } - - // Remove precision qualifiers (not valid in desktop GL core) - if (processedLine.find("precision ") != string::npos) - { - continue; // Skip precision lines entirely - } - - // Replace WebGL 1.0 keywords with desktop GL equivalents - size_t pos; - - // attribute -> in (vertex shaders only) - if (shaderType == GL_VERTEX_SHADER && (pos = processedLine.find("attribute ")) != string::npos) - { - processedLine.replace(pos, 10, "in "); - } - - // varying -> out (vertex) / in (fragment) - if ((pos = processedLine.find("varying ")) != string::npos) - { - if (shaderType == GL_VERTEX_SHADER) - { - processedLine.replace(pos, 8, "out "); - } - else - { - processedLine.replace(pos, 8, "in "); - } - } - - // gl_FragColor -> fragColor (fragment shaders only) - if (shaderType == GL_FRAGMENT_SHADER && (pos = processedLine.find("gl_FragColor")) != string::npos) - { - hasFragColor = true; - processedLine.replace(pos, 12, "fragColor"); - } - - // gl_FragData[0] -> fragColor (fragment shaders only) - // Handle the common case of gl_FragData[0] which should become fragColor (single output) - if (shaderType == GL_FRAGMENT_SHADER && (pos = processedLine.find("gl_FragData[0]")) != string::npos) - { - hasFragColor = true; - processedLine.replace(pos, 14, "fragColor"); - } - // Handle general gl_FragData array access (convert to fragColor for single output) - else if (shaderType == GL_FRAGMENT_SHADER && (pos = processedLine.find("gl_FragData")) != string::npos) - { - hasFragColor = true; - // Look for array access pattern like gl_FragData[index] - size_t arrayStart = processedLine.find("[", pos); - if (arrayStart != string::npos) - { - size_t arrayEnd = processedLine.find("]", arrayStart); - if (arrayEnd != string::npos) - { - // Replace gl_FragData[index] with fragColor (assume single output for now) - processedLine.replace(pos, arrayEnd - pos + 1, "fragColor"); - } - } - else - { - // No array access, just replace gl_FragData with fragColor - processedLine.replace(pos, 11, "fragColor"); - } - } - - // Handle any remaining fragColor[0] patterns that might have been missed - // This covers cases where gl_FragData[0] might have been partially processed - if (shaderType == GL_FRAGMENT_SHADER && (pos = processedLine.find("fragColor[0]")) != string::npos) - { - hasFragColor = true; - processedLine.replace(pos, 12, "fragColor"); - } - // Handle other fragColor array access patterns (for single output case) - else if (shaderType == GL_FRAGMENT_SHADER && processedLine.find("fragColor[") != string::npos) - { - hasFragColor = true; - size_t fragPos = processedLine.find("fragColor["); - size_t arrayEnd = processedLine.find("]", fragPos); - if (arrayEnd != string::npos) - { - // Replace fragColor[index] with fragColor (assume single output) - processedLine.replace(fragPos, arrayEnd - fragPos + 1, "fragColor"); - } - } - - patched += processedLine + "\n"; - } - - // Add output declaration for fragment color if gl_FragColor was used - if (shaderType == GL_FRAGMENT_SHADER && hasFragColor) - { - // Insert after version directive but before other content - size_t versionPos = patched.find("#version 410 core\n"); - if (versionPos != string::npos) - { - size_t insertPos = versionPos + strlen("#version 410 core\n"); - patched.insert(insertPos, "out vec4 fragColor;\n"); - } - else - { - patched = "out vec4 fragColor;\n" + patched; - } - } - - return patched; -} - class RHI_OpenGL : public TrRenderHardwareInterface { private: @@ -884,13 +738,23 @@ class RHI_OpenGL : public TrRenderHardwareInterface string fixedSource; { + string line; + size_t pos = 0; + + while (pos < source.size()) + { + getline(source, line, pos); + string newLine = line; #ifdef __APPLE__ - // On macOS, we need to translate WebGL/GLSL ES shaders to desktop OpenGL Core Profile - fixedSource = PatchWebGLShaderForDesktopGL(source, shader); -#else - // On other platforms, pass through the source as-is since client-side handles version logic - fixedSource = source; + /** + * FIXME(Yorkie): This is a workaround for the shader source on macOS, we need to replace the version to 410 core + * directly, a better solution is to use the shader preprocessor like google/angle to handle this. + */ + if (line.find("#version") != string::npos) + newLine = "#version 410 core"; #endif + fixedSource += newLine + "\n"; + } } const char *sourceStr = fixedSource.c_str(); From a223e7c922b6e1697422c4d0ad896d7f739bce82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 04:09:43 +0000 Subject: [PATCH 10/15] Move WebGL version handling to MyGLSLPatcher in webgl.rs as requested Co-authored-by: EndlessJour9527 <155411404+EndlessJour9527@users.noreply.github.com> --- crates/jsbindings/css/mod.rs | 2 +- .../jsbindings/css/stylesheets/style_rule.rs | 2 +- crates/jsbindings/webgl.rs | 168 +++++++++++++++++- src/client/graphics/webgl_context.cpp | 71 +------- 4 files changed, 169 insertions(+), 74 deletions(-) diff --git a/crates/jsbindings/css/mod.rs b/crates/jsbindings/css/mod.rs index db12d37bf..e7bd1bfbb 100644 --- a/crates/jsbindings/css/mod.rs +++ b/crates/jsbindings/css/mod.rs @@ -1,4 +1,4 @@ pub(crate) mod properties; pub(crate) mod selectors; -pub(crate) mod values; pub(crate) mod stylesheets; +pub(crate) mod values; diff --git a/crates/jsbindings/css/stylesheets/style_rule.rs b/crates/jsbindings/css/stylesheets/style_rule.rs index a27b1dc38..f0afe79e5 100644 --- a/crates/jsbindings/css/stylesheets/style_rule.rs +++ b/crates/jsbindings/css/stylesheets/style_rule.rs @@ -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, diff --git a/crates/jsbindings/webgl.rs b/crates/jsbindings/webgl.rs index b19796942..88c741b71 100644 --- a/crates/jsbindings/webgl.rs +++ b/crates/jsbindings/webgl.rs @@ -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); @@ -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) + 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.); +} +"# + ); + } } diff --git a/src/client/graphics/webgl_context.cpp b/src/client/graphics/webgl_context.cpp index 9f23a0b55..07ab5957e 100644 --- a/src/client/graphics/webgl_context.cpp +++ b/src/client/graphics/webgl_context.cpp @@ -28,71 +28,6 @@ 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 context) { context->useProgram(state.program.value_or(nullptr)); @@ -415,11 +350,7 @@ namespace client_graphics void WebGLContext::shaderSource(shared_ptr shader, const string &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); + shader->source = GLSLSourcePatcher2::GetPatchedSource(source); auto req = ShaderSourceCommandBufferRequest(shader->id, shader->source); sendCommandBufferRequest(req); } From 944794cdafb14605af3c42a5e9bb2a5bf3dc897f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 04:15:01 +0000 Subject: [PATCH 11/15] Revert style_rule.rs file to original state as requested Co-authored-by: EndlessJour9527 <155411404+EndlessJour9527@users.noreply.github.com> --- crates/jsbindings/css/stylesheets/style_rule.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/jsbindings/css/stylesheets/style_rule.rs b/crates/jsbindings/css/stylesheets/style_rule.rs index f0afe79e5..a27b1dc38 100644 --- a/crates/jsbindings/css/stylesheets/style_rule.rs +++ b/crates/jsbindings/css/stylesheets/style_rule.rs @@ -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, From 9b998fdce0dcdcaa7d51996dd67b719e9a6207f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 05:07:26 +0000 Subject: [PATCH 12/15] Revert css/mod.rs file to original state as requested Co-authored-by: EndlessJour9527 <155411404+EndlessJour9527@users.noreply.github.com> --- crates/jsbindings/css/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/jsbindings/css/mod.rs b/crates/jsbindings/css/mod.rs index e7bd1bfbb..db12d37bf 100644 --- a/crates/jsbindings/css/mod.rs +++ b/crates/jsbindings/css/mod.rs @@ -1,4 +1,4 @@ pub(crate) mod properties; pub(crate) mod selectors; -pub(crate) mod stylesheets; pub(crate) mod values; +pub(crate) mod stylesheets; From 1c263db4a94b1fc1958c565f4b5a9b680b84f461 Mon Sep 17 00:00:00 2001 From: yjh Date: Thu, 9 Oct 2025 14:19:40 +0800 Subject: [PATCH 13/15] Refactor MyGLSLPatcher to enhance fragment output handling and shader stage detection --- crates/jsbindings/webgl.rs | 573 +++++++++++++++++++++++++++++-------- 1 file changed, 455 insertions(+), 118 deletions(-) diff --git a/crates/jsbindings/webgl.rs b/crates/jsbindings/webgl.rs index 88c741b71..614c14db1 100644 --- a/crates/jsbindings/webgl.rs +++ b/crates/jsbindings/webgl.rs @@ -1,45 +1,394 @@ use glsl_lang::ast; -use glsl_lang::visitor::{HostMut, Visit, VisitorMut}; +use glsl_lang::visitor::{Host, HostMut, Visit, Visitor, VisitorMut}; +use std::collections::BTreeSet; use std::path::Path; use crate::glsl_transpiler; -struct MyGLSLPatcher {} +const FRAGMENT_OUTPUT_NAME: &str = "fragColor"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ShaderStage { + Vertex, + Fragment, +} + +#[derive(Default)] +struct StageDetector { + has_fragment_builtin: bool, +} + +impl StageDetector { + fn stage(&self) -> ShaderStage { + if self.has_fragment_builtin { + ShaderStage::Fragment + } else { + ShaderStage::Vertex + } + } +} + +impl Visitor for StageDetector { + fn visit_identifier(&mut self, ident: &ast::Identifier) -> Visit { + let name = ident.content.as_str(); + match name { + "gl_FragColor" | "gl_FragData" | "gl_FragDepth" | "gl_FragCoord" | "gl_SampleMask" => { + self.has_fragment_builtin = true; + } + _ => {} + } + Visit::Children + } +} + +struct MyGLSLPatcher { + stage: ShaderStage, + saw_gl_fragcolor: bool, + fragment_output_declared: bool, + fragdata_indices: BTreeSet, +} impl MyGLSLPatcher { + fn fragment_output_name(index: u32) -> String { + if index == 0 { + FRAGMENT_OUTPUT_NAME.to_string() + } else { + format!("{}{}", FRAGMENT_OUTPUT_NAME, index) + } + } + + fn fragment_output_exists(tu: &ast::TranslationUnit, name: &str) -> bool { + tu.0.iter().any(|decl| { + if let ast::ExternalDeclarationData::Declaration(decl_node) = &decl.content { + if let ast::DeclarationData::InitDeclaratorList(list) = &decl_node.content { + let head = &list.content.head.content; + if let Some(identifier) = &head.name { + return identifier.content.as_str() == name; + } + } + } + false + }) + } + + fn fragment_output_insertion_index(tu: &ast::TranslationUnit) -> usize { + let mut insertion_index = 0; + for (idx, decl) in tu.0.iter().enumerate() { + match &decl.content { + ast::ExternalDeclarationData::Preprocessor(_) => { + insertion_index = idx + 1; + } + ast::ExternalDeclarationData::Declaration(_) => { + insertion_index = idx + 1; + } + ast::ExternalDeclarationData::FunctionDefinition(_) => { + break; + } + } + } + insertion_index + } + + fn extract_constant_u32(expr: &ast::Expr) -> Option { + match &expr.content { + ast::ExprData::IntConst(value) => value.to_string().parse().ok(), + ast::ExprData::UIntConst(value) => value + .to_string() + .trim_end_matches('u') + .parse() + .ok(), + _ => None, + } + } + + fn ensure_additional_fragdata_declarations(&mut self, tu: &mut ast::TranslationUnit) { + let need_fragcolor = self.saw_gl_fragcolor || self.fragdata_indices.contains(&0); + if need_fragcolor && !self.fragment_output_declared { + self.ensure_fragment_output_declaration(tu); + } + + let extra_indices: Vec = self + .fragdata_indices + .iter() + .copied() + .filter(|&idx| idx != 0) + .collect(); + + for index in extra_indices { + let name = Self::fragment_output_name(index); + if Self::fragment_output_exists(tu, &name) { + continue; + } + + let insertion_index = Self::fragment_output_insertion_index(tu); + tu.0 + .insert(insertion_index, self.build_fragment_output_declaration(&name)); + } + } + + fn new(stage: ShaderStage) -> Self { + Self { + stage, + saw_gl_fragcolor: false, + fragment_output_declared: false, + fragdata_indices: BTreeSet::new(), + } + } + + fn apply(&mut self, tu: &mut ast::TranslationUnit) { + self.remove_precision_declarations(tu); + tu.visit_mut(self); + if self.stage == ShaderStage::Fragment { + if self.saw_gl_fragcolor { + self.ensure_fragment_output_declaration(tu); + } + self.ensure_additional_fragdata_declarations(tu); + } + self.ensure_version_and_extension_order(tu); + } + + fn remove_precision_declarations(&self, tu: &mut ast::TranslationUnit) { + tu.0.retain(|decl| { + !matches!( + &decl.content, + ast::ExternalDeclarationData::Declaration(node) + if matches!(node.content, ast::DeclarationData::Precision(_, _)) + ) + }); + } + + fn sanitize_type_qualifier(&self, qualifier: &mut ast::TypeQualifier) { + use ast::{StorageQualifierData, TypeQualifierSpecData}; + + let mut seen_storage: Option = None; + let mut sanitized = Vec::with_capacity(qualifier.content.qualifiers.len()); + + for mut spec in qualifier.content.qualifiers.drain(..) { + let keep = match &mut spec.content { + TypeQualifierSpecData::Storage(storage) => { + match storage.content { + StorageQualifierData::Attribute => { + storage.content = StorageQualifierData::In; + } + StorageQualifierData::Varying => { + storage.content = match self.stage { + ShaderStage::Vertex => StorageQualifierData::Out, + ShaderStage::Fragment => StorageQualifierData::In, + }; + } + _ => {} + } + + if let Some(existing) = &seen_storage { + if existing == &storage.content { + false + } else { + seen_storage = Some(storage.content.clone()); + true + } + } else { + seen_storage = Some(storage.content.clone()); + true + } + } + TypeQualifierSpecData::Precision(_) => false, + _ => true, + }; + + if keep { + sanitized.push(spec); + } + } + + qualifier.content.qualifiers = sanitized; + } + fn create_model_view_matrix_expr(&self) -> ast::Expr { let new_lhs: ast::Expr = - ast::ExprData::Variable(ast::IdentifierData(ast::SmolStr::new_inline("viewMatrix")).into()) - .into(); + ast::ExprData::Variable(ast::IdentifierData::from("viewMatrix").into()).into(); let new_rhs: ast::Expr = - ast::ExprData::Variable(ast::IdentifierData(ast::SmolStr::new_inline("modelMatrix")).into()) - .into(); - let new_binary_expr: ast::Expr = ast::ExprData::Binary( + ast::ExprData::Variable(ast::IdentifierData::from("modelMatrix").into()).into(); + ast::ExprData::Binary( ast::BinaryOpData::Mult.into(), Box::new(new_lhs), Box::new(new_rhs), ) + .into() + } + + fn ensure_fragment_output_declaration(&mut self, tu: &mut ast::TranslationUnit) { + if self.fragment_output_declared { + return; + } + + for decl in &mut tu.0 { + if let ast::ExternalDeclarationData::Declaration(decl_node) = &mut decl.content { + if let ast::DeclarationData::InitDeclaratorList(list) = &mut decl_node.content { + let head = &mut list.content.head.content; + if let Some(name) = &mut head.name { + let ident = name.content.as_str(); + if ident == FRAGMENT_OUTPUT_NAME || ident == "glFragColor" { + name.content = ast::IdentifierData::from(FRAGMENT_OUTPUT_NAME); + self.prepare_fragment_output_type(&mut head.ty); + list.content.tail.clear(); + self.fragment_output_declared = true; + return; + } + } + } + } + } + + let insertion_index = Self::fragment_output_insertion_index(tu); + tu.0 + .insert(insertion_index, self.build_fragment_output_declaration(FRAGMENT_OUTPUT_NAME)); + self.fragment_output_declared = true; + } + + fn prepare_fragment_output_type(&self, ty: &mut ast::FullySpecifiedType) { + ty.content.ty = ast::TypeSpecifierData { + ty: ast::TypeSpecifierNonArrayData::Vec4.into(), + array_specifier: None, + } + .into(); + + let qualifier = ty.content.qualifier.get_or_insert_with(|| { + ast::TypeQualifierData { + qualifiers: Vec::new(), + } + .into() + }); + + self.sanitize_type_qualifier(qualifier); + + let has_out = qualifier.content.qualifiers.iter().any(|spec| { + matches!( + &spec.content, + ast::TypeQualifierSpecData::Storage(storage) + if matches!(storage.content, ast::StorageQualifierData::Out) + ) + }); + + if !has_out { + qualifier + .content + .qualifiers + .push(ast::TypeQualifierSpecData::Storage(ast::StorageQualifierData::Out.into()).into()); + } + + if qualifier.content.qualifiers.is_empty() { + ty.content.qualifier = None; + } + } + + fn build_fragment_output_declaration(&self, name: &str) -> ast::ExternalDeclaration { + let qualifier: ast::TypeQualifier = ast::TypeQualifierData { + qualifiers: vec![ + ast::TypeQualifierSpecData::Storage(ast::StorageQualifierData::Out.into()).into(), + ], + } + .into(); + + let ty: ast::FullySpecifiedType = ast::FullySpecifiedTypeData { + qualifier: Some(qualifier), + ty: ast::TypeSpecifierData { + ty: ast::TypeSpecifierNonArrayData::Vec4.into(), + array_specifier: None, + } + .into(), + } + .into(); + + let single_decl: ast::SingleDeclaration = ast::SingleDeclarationData { + ty, + name: Some(ast::IdentifierData::from(name).into()), + array_specifier: None, + initializer: None, + } .into(); - new_binary_expr + + let init_list: ast::InitDeclaratorList = ast::InitDeclaratorListData { + head: single_decl, + tail: Vec::new(), + } + .into(); + + ast::ExternalDeclarationData::Declaration( + ast::DeclarationData::InitDeclaratorList(init_list).into(), + ) + .into() } - fn handle_expr(&self, expr: &mut ast::Expr) -> bool { + fn ensure_version_and_extension_order(&self, tu: &mut ast::TranslationUnit) { + let mut versions = Vec::new(); + let mut extensions = Vec::new(); + + tu.0.retain(|decl| { + if let ast::ExternalDeclarationData::Preprocessor(preprocessor) = &decl.content { + match &preprocessor.content { + ast::PreprocessorData::Version(_) => { + let mut updated = decl.clone(); + if let ast::ExternalDeclarationData::Preprocessor(pre) = &mut updated.content { + if let ast::PreprocessorData::Version(version) = &mut pre.content { + version.content.version = 300; + version.content.profile = Some(ast::PreprocessorVersionProfileData::Es.into()); + } + } + versions.push(updated); + return false; + } + ast::PreprocessorData::Extension(_) => { + extensions.push(decl.clone()); + return false; + } + _ => {} + } + } + true + }); + + if versions.is_empty() { + let version = ast::PreprocessorVersionData { + version: 300, + profile: Some(ast::PreprocessorVersionProfileData::Es.into()), + } + .into(); + versions.push( + ast::ExternalDeclarationData::Preprocessor(ast::PreprocessorData::Version(version).into()) + .into(), + ); + } + + tu.0.splice(0..0, extensions); + tu.0.splice(0..0, versions); + } + + fn handle_expr(&mut self, expr: &mut ast::Expr) -> bool { match &mut expr.content { ast::ExprData::Variable(identifier) => { - if identifier.content.0 == "modelViewMatrix" { + if identifier.content.as_str() == "modelViewMatrix" { *expr = self.create_model_view_matrix_expr(); - true - } else { - false + return true; + } + if self.stage == ShaderStage::Fragment && identifier.content.as_str() == "gl_FragColor" { + print!("Rewriting gl_FragColor to {}", FRAGMENT_OUTPUT_NAME); + identifier.content = ast::IdentifierData::from(FRAGMENT_OUTPUT_NAME); + self.saw_gl_fragcolor = true; + return true; } + false } ast::ExprData::Unary(_, operand) => self.handle_expr(operand), ast::ExprData::Binary(_, lhs, rhs) => { - let r1 = self.handle_expr(lhs); - let r2 = self.handle_expr(rhs); - r1 || r2 + let l = self.handle_expr(lhs); + let r = self.handle_expr(rhs); + l || r + } + ast::ExprData::Assignment(lhs, _, rhs) => { + let l = self.handle_expr(lhs); + let r = self.handle_expr(rhs); + l || r } - ast::ExprData::Assignment(_, _, rhs) => self.handle_expr(rhs), ast::ExprData::FunCall(_, args) => { let mut changed = false; for arg in args { @@ -47,6 +396,33 @@ impl MyGLSLPatcher { } changed } + ast::ExprData::Ternary(cond, then_branch, else_branch) => { + let r1 = self.handle_expr(cond); + let r2 = self.handle_expr(then_branch); + let r3 = self.handle_expr(else_branch); + r1 || r2 || r3 + } + ast::ExprData::Comma(lhs, rhs) => self.handle_expr(lhs) || self.handle_expr(rhs), + ast::ExprData::Bracket(inner, index) => { + if self.stage == ShaderStage::Fragment { + if let ast::ExprData::Variable(identifier) = &inner.content { + if identifier.content.as_str() == "gl_FragData" { + if let Some(idx) = Self::extract_constant_u32(index) { + let name = Self::fragment_output_name(idx); + *expr = ast::ExprData::Variable(ast::IdentifierData::from(name.as_str()).into()).into(); + self.fragdata_indices.insert(idx); + if idx == 0 { + self.saw_gl_fragcolor = true; + } + return true; + } + } + } + } + self.handle_expr(inner) || self.handle_expr(index) + } + ast::ExprData::Dot(inner, _) => self.handle_expr(inner), + ast::ExprData::PostInc(inner) | ast::ExprData::PostDec(inner) => self.handle_expr(inner), _ => false, } } @@ -60,48 +436,31 @@ impl VisitorMut for MyGLSLPatcher { Visit::Children } } -} -/// 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; + fn visit_full_specified_type(&mut self, ty: &mut ast::FullySpecifiedType) -> Visit { + if let Some(qualifier) = &mut ty.content.qualifier { + self.sanitize_type_qualifier(qualifier); + if qualifier.content.qualifiers.is_empty() { + ty.content.qualifier = None; + } + } + Visit::Children } - // 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; + fn visit_struct_field_specifier(&mut self, field: &mut ast::StructFieldSpecifier) -> Visit { + if let Some(qualifier) = &mut field.content.qualifier { + self.sanitize_type_qualifier(qualifier); + if qualifier.content.qualifiers.is_empty() { + field.content.qualifier = None; + } + } + Visit::Children } - // 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; + fn visit_type_qualifier(&mut self, qualifier: &mut ast::TypeQualifier) -> Visit { + self.sanitize_type_qualifier(qualifier); + Visit::Children } - - // Default to WebGL 1.0 for safety - false } fn patch_glsl_source_from_str(s: &str) -> String { @@ -109,55 +468,22 @@ fn patch_glsl_source_from_str(s: &str) -> String { 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(&source_to_parse, Path::new(".")) + .open_source(s, Path::new(".")) .builder() .parse() .map(|(mut tu, _, iter)| { iter.into_directives().inject(&mut tu); tu }) - .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); - - { - /* - * This reorders the preprocessor directives in the GLSL source code. - * - * 1. Move the #version directive to the top. - * 2. Move the #extension directives to the top after the #version directive if exists. - */ - let mut versions_list = Vec::new(); - let mut extensions_list = Vec::new(); - tu.0.retain(|decl| match &decl.content { - ast::ExternalDeclarationData::Preprocessor(processor) => match processor.content { - ast::PreprocessorData::Version(_) => { - versions_list.push(decl.clone()); - false - } - ast::PreprocessorData::Extension(_) => { - extensions_list.push(decl.clone()); - false - } - _ => true, - }, - _ => true, - }); - tu.0.splice(0..0, extensions_list); - tu.0.splice(0..0, versions_list); - } + .expect(format!("Failed to parse GLSL source: \n{}\n", s).as_str()); + + let mut detector = StageDetector::default(); + tu.visit(&mut detector); + let stage = detector.stage(); + let mut patcher = MyGLSLPatcher::new(stage); + patcher.apply(&mut tu); let mut s = String::new(); glsl_transpiler::glsl::show_translation_unit( @@ -180,39 +506,50 @@ mod ffi { #[cfg(test)] mod tests { use super::*; - use std::ffi::CString; #[test] - fn test_patch_glsl_source() { + fn test_fragment_shader_rewrites_gl_frag_color() { let source_str = r#" -#extension GL_OVR_multiview2 : enable -layout(num_views = 2) in; +precision mediump float; +varying highp vec2 vUv; -#version 300 es -precision highp float; -highp float a = 1.0; -layout (location = 0) in vec3 aPos; -layout (location = 1) in vec3 aNormal; -layout (location = 0) out highp vec4 glFragColor; -#extension GL_OES_standard_derivatives : enable - -void main() { - gl_FragColor = vec4(1, 1, 1, 1); -}"#; +void main() { + gl_FragColor = vec4(vUv, 0.0, 1.0); +} +"#; let patched_source_str = patch_glsl_source_from_str(source_str); assert_eq!( patched_source_str, r#"#version 300 es -#extension GL_OVR_multiview2 : enable -#extension GL_OES_standard_derivatives : enable -layout(num_views = 2) in; -precision highp float; -highp float a = 1.; -layout(location = 0) in vec3 aPos; -layout(location = 1) in vec3 aNormal; -layout(location = 0) out highp vec4 glFragColor; +in vec2 vUv; +out vec4 fragColor; +void main() { + fragColor = vec4(vUv, 0., 1.); +} +"# + ) + } + + #[test] + fn test_vertex_attribute_and_varying_transforms() { + let source_str = r#" +attribute vec3 position; +attribute vec3 normal; +varying mediump vec2 vUv; + +void main() { + gl_Position = vec4(position, 1.0); +} +"#; + let patched_source_str = patch_glsl_source_from_str(source_str); + assert_eq!( + patched_source_str, + r#"#version 300 es +in vec3 position; +in vec3 normal; +out vec2 vUv; void main() { - gl_FragColor = vec4(1, 1, 1, 1); + gl_Position = vec4(position, 1.); } "# ) From 31fa5e811b7fc545f24db5db3792a153ac98b5eb Mon Sep 17 00:00:00 2001 From: yjh Date: Thu, 9 Oct 2025 15:48:05 +0800 Subject: [PATCH 14/15] Refactor test-shader-version-fix.html for improved readability and structure --- .../test-shader-version-fix.html | 272 ++++++++++-------- 1 file changed, 152 insertions(+), 120 deletions(-) diff --git a/fixtures/html/webgl-conformance/test-shader-version-fix.html b/fixtures/html/webgl-conformance/test-shader-version-fix.html index 5a0f1ff3a..cb3464bc9 100644 --- a/fixtures/html/webgl-conformance/test-shader-version-fix.html +++ b/fixtures/html/webgl-conformance/test-shader-version-fix.html @@ -1,162 +1,194 @@ - - - WebGL Standards-Compliant GLSL Version Handling Test - + + + WebGL Standards-Compliant GLSL Version Handling Test + - -

WebGL Standards-Compliant GLSL Version Handling Test

-

This test validates WebGL standards-compliant GLSL version handling:

-
    -
  • WebGL 1.0: Version directives are optional (should default to GLSL ES 1.00)
  • -
  • WebGL 2.0: Version directives are required (#version 300 es)
  • -
- -
- - + // Run tests when page loads + document.addEventListener('DOMContentLoaded', runTests); + + \ No newline at end of file From 377515ca4d4ac0c9c91dcab8c1a2e412a7e1e0de Mon Sep 17 00:00:00 2001 From: yjh Date: Thu, 9 Oct 2025 19:21:41 +0800 Subject: [PATCH 15/15] Enhance fragment output handling in MyGLSLPatcher by ensuring proper declaration and updating existing variables --- crates/jsbindings/webgl.rs | 65 +++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/crates/jsbindings/webgl.rs b/crates/jsbindings/webgl.rs index 614c14db1..7367366db 100644 --- a/crates/jsbindings/webgl.rs +++ b/crates/jsbindings/webgl.rs @@ -216,32 +216,68 @@ impl MyGLSLPatcher { .into() } + /// Ensures that the fragment shader has a proper output declaration. + /// If an existing declaration is found (either "fragColor" or "glFragColor"), + /// it will be updated to the correct type and name. Otherwise, a new declaration is inserted. fn ensure_fragment_output_declaration(&mut self, tu: &mut ast::TranslationUnit) { if self.fragment_output_declared { return; } + // Try to find and update existing fragment output declaration + if self.try_update_existing_fragment_output(tu) { + return; + } + + // No existing declaration found, create a new one + self.insert_new_fragment_output_declaration(tu); + } + + /// Attempts to find and update an existing fragment output declaration. + /// Returns true if an existing declaration was found and updated. + fn try_update_existing_fragment_output(&mut self, tu: &mut ast::TranslationUnit) -> bool { for decl in &mut tu.0 { - if let ast::ExternalDeclarationData::Declaration(decl_node) = &mut decl.content { - if let ast::DeclarationData::InitDeclaratorList(list) = &mut decl_node.content { - let head = &mut list.content.head.content; - if let Some(name) = &mut head.name { - let ident = name.content.as_str(); - if ident == FRAGMENT_OUTPUT_NAME || ident == "glFragColor" { - name.content = ast::IdentifierData::from(FRAGMENT_OUTPUT_NAME); - self.prepare_fragment_output_type(&mut head.ty); - list.content.tail.clear(); - self.fragment_output_declared = true; - return; - } + if let Some(declaration_list) = self.extract_declaration_list(decl) { + let head = &mut declaration_list.content.head.content; + + if let Some(variable_name) = &mut head.name { + let identifier = variable_name.content.as_str(); + + if self.is_fragment_output_variable(identifier) { + // Update the existing declaration + variable_name.content = ast::IdentifierData::from(FRAGMENT_OUTPUT_NAME); + self.prepare_fragment_output_type(&mut head.ty); + declaration_list.content.tail.clear(); + self.fragment_output_declared = true; + return true; } } } } + false + } + + /// Extracts the InitDeclaratorList from a declaration if it exists. + fn extract_declaration_list<'a>(&self, decl: &'a mut ast::ExternalDeclaration) + -> Option<&'a mut ast::InitDeclaratorList> { + if let ast::ExternalDeclarationData::Declaration(decl_node) = &mut decl.content { + if let ast::DeclarationData::InitDeclaratorList(list) = &mut decl_node.content { + return Some(list); + } + } + None + } + + /// Checks if the given identifier represents a fragment output variable. + fn is_fragment_output_variable(&self, identifier: &str) -> bool { + identifier == FRAGMENT_OUTPUT_NAME || identifier == "glFragColor" + } + /// Inserts a new fragment output declaration at the appropriate position. + fn insert_new_fragment_output_declaration(&mut self, tu: &mut ast::TranslationUnit) { let insertion_index = Self::fragment_output_insertion_index(tu); - tu.0 - .insert(insertion_index, self.build_fragment_output_declaration(FRAGMENT_OUTPUT_NAME)); + let new_declaration = self.build_fragment_output_declaration(FRAGMENT_OUTPUT_NAME); + tu.0.insert(insertion_index, new_declaration); self.fragment_output_declared = true; } @@ -371,7 +407,6 @@ impl MyGLSLPatcher { return true; } if self.stage == ShaderStage::Fragment && identifier.content.as_str() == "gl_FragColor" { - print!("Rewriting gl_FragColor to {}", FRAGMENT_OUTPUT_NAME); identifier.content = ast::IdentifierData::from(FRAGMENT_OUTPUT_NAME); self.saw_gl_fragcolor = true; return true;