summaryrefslogtreecommitdiff
path: root/scripts/validate_shaders.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/validate_shaders.py')
-rwxr-xr-xscripts/validate_shaders.py169
1 files changed, 169 insertions, 0 deletions
diff --git a/scripts/validate_shaders.py b/scripts/validate_shaders.py
new file mode 100755
index 0000000..2aab4e5
--- /dev/null
+++ b/scripts/validate_shaders.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+# This script is part of the 64k demo project.
+# Offline WGSL validation: compose #includes then run naga-cli.
+# Usage: ./scripts/validate_shaders.py [--install]
+
+import os
+import sys
+import subprocess
+import tempfile
+
+REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Mirrors InitShaderComposer() in src/effects/shaders.cc
+# snippet_name -> path relative to REPO
+SNIPPETS = {
+ "common_uniforms": "src/shaders/common_uniforms.wgsl",
+ "sequence_uniforms": "src/shaders/sequence_uniforms.wgsl",
+ "postprocess_inline": "src/shaders/postprocess_inline.wgsl",
+ "camera_common": "src/shaders/camera_common.wgsl",
+ "math/sdf_shapes": "src/shaders/math/sdf_shapes.wgsl",
+ "math/sdf_utils": "src/shaders/math/sdf_utils.wgsl",
+ "math/common_utils": "src/shaders/math/common_utils.wgsl",
+ "math/noise": "src/shaders/math/noise.wgsl",
+ "math/color": "src/shaders/math/color.wgsl",
+ "math/utils": "src/shaders/math/utils.wgsl",
+ "lighting": "src/shaders/lighting.wgsl",
+ "ray_box": "src/shaders/ray_box.wgsl",
+ "ray_triangle": "src/shaders/ray_triangle.wgsl",
+ "render/shadows": "src/shaders/render/shadows.wgsl",
+ "render/scene_query_bvh": "src/shaders/render/scene_query_bvh.wgsl",
+ "render/scene_query_linear": "src/shaders/render/scene_query_linear.wgsl",
+ "render/lighting_utils": "src/shaders/render/lighting_utils.wgsl",
+ "render/scratch_lines": "src/shaders/render/scratch_lines.wgsl",
+ "render/fullscreen_vs": "src/shaders/render/fullscreen_vs.wgsl",
+ "render/fullscreen_uv_vs": "src/shaders/render/fullscreen_uv_vs.wgsl",
+ "render/raymarching": "src/shaders/render/raymarching.wgsl",
+ "render/raymarching_id": "src/shaders/render/raymarching_id.wgsl",
+ "render/mesh": "workspaces/main/shaders/mesh_render.wgsl",
+}
+
+# Shaders that use runtime ShaderComposer substitutions (e.g. scene_query_bvh
+# vs scene_query_linear) — they reference symbols injected at runtime and
+# cannot be validated standalone.
+SUBSTITUTION_SHADERS = {
+ "workspaces/main/shaders/renderer_3d.wgsl",
+ "workspaces/main/shaders/masked_cube.wgsl",
+ "workspaces/main/shaders/mesh_render.wgsl",
+}
+
+# All .wgsl files with entry points that are NOT snippets (i.e. complete shaders)
+MAIN_SHADERS = [
+ "src/effects/chroma_aberration.wgsl",
+ "src/effects/distort.wgsl",
+ "src/effects/ellipse.wgsl",
+ "src/effects/flash.wgsl",
+ "src/effects/gaussian_blur.wgsl",
+ "src/effects/ntsc.wgsl",
+ "src/effects/particle_compute.wgsl",
+ "src/effects/particle_render.wgsl",
+ "src/effects/particle_spray_compute.wgsl",
+ "src/effects/rotating_cube.wgsl",
+ "src/effects/scene1.wgsl",
+ "src/effects/scene2.wgsl",
+ "src/effects/scratch.wgsl",
+ "src/effects/solarize.wgsl",
+ "src/effects/vignette.wgsl",
+ "src/shaders/combined_postprocess.wgsl",
+ "src/shaders/gaussian_blur.wgsl",
+ "src/shaders/heptagon.wgsl",
+ "src/shaders/passthrough.wgsl",
+ "src/shaders/skybox.wgsl",
+ "src/shaders/compute/gen_blend.wgsl",
+ "src/shaders/compute/gen_grid.wgsl",
+ "src/shaders/compute/gen_mask.wgsl",
+ "src/shaders/compute/gen_noise.wgsl",
+ "src/shaders/compute/gen_perlin.wgsl",
+ "workspaces/main/shaders/circle_mask_compute.wgsl",
+ "workspaces/main/shaders/circle_mask_render.wgsl",
+ "workspaces/main/shaders/main_shader.wgsl",
+ "workspaces/main/shaders/masked_cube.wgsl",
+ "workspaces/main/shaders/mesh_render.wgsl",
+ "workspaces/main/shaders/renderer_3d.wgsl",
+ "workspaces/main/shaders/visual_debug.wgsl",
+]
+
+
+def compose(source, included=None):
+ """Recursively resolve #include directives using SNIPPETS map."""
+ if included is None:
+ included = set()
+ result = []
+ for line in source.splitlines():
+ if line.strip().startswith("#include "):
+ s = line.find('"')
+ e = line.find('"', s + 1)
+ if s == -1 or e == -1:
+ result.append(line)
+ continue
+ name = line[s + 1:e]
+ if name in included:
+ continue
+ included.add(name)
+ if name not in SNIPPETS:
+ result.append(f"// ERROR: unknown snippet '{name}'")
+ continue
+ snippet_path = os.path.join(REPO, SNIPPETS[name])
+ try:
+ snippet_src = open(snippet_path).read()
+ result.append(f"// --- {name} ---")
+ result.append(compose(snippet_src, included))
+ result.append(f"// --- end {name} ---")
+ except FileNotFoundError:
+ result.append(f"// ERROR: missing file for snippet '{name}'")
+ else:
+ result.append(line)
+ return "\n".join(result)
+
+
+def validate(rel_path):
+ path = os.path.join(REPO, rel_path)
+ if not os.path.exists(path):
+ return None, "file not found"
+ composed = compose(open(path).read())
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".wgsl", delete=False) as f:
+ f.write(composed)
+ tmp = f.name
+ try:
+ r = subprocess.run(["naga", tmp], capture_output=True, text=True)
+ if r.returncode == 0:
+ return True, None
+ return False, (r.stderr or r.stdout).strip()
+ finally:
+ os.unlink(tmp)
+
+
+def main():
+ if "--install" in sys.argv:
+ os.execvp("cargo", ["cargo", "install", "naga-cli"])
+
+ if subprocess.run(["which", "naga"], capture_output=True).returncode != 0:
+ print("naga not found. Run: cargo install naga-cli (or: ./scripts/validate_shaders.sh --install)")
+ sys.exit(1)
+
+ passed = failed = skipped = 0
+ for shader in MAIN_SHADERS:
+ label = shader.split("/", 1)[-1]
+ if shader in SUBSTITUTION_SHADERS:
+ print(f" skip {label} (runtime substitution)")
+ skipped += 1
+ continue
+ ok, err = validate(shader)
+ if ok is None:
+ print(f" SKIP {label}")
+ skipped += 1
+ elif ok:
+ print(f" ok {label}")
+ passed += 1
+ else:
+ print(f" FAIL {label}")
+ for line in err.splitlines()[:8]:
+ print(f" {line}")
+ failed += 1
+
+ print(f"\n{passed} passed, {failed} failed, {skipped} skipped")
+ sys.exit(1 if failed else 0)
+
+
+if __name__ == "__main__":
+ main()