#!/usr/bin/env bash # External shader validator — tests backend output against real compilers/validators # Compiles each entry point separately and validates with available tools. # # Usage: # ./tests/validate.sh # Run all validations # ./tests/validate.sh --verbose # Show each test as it runs set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" SHADER_DIR="$SCRIPT_DIR/shaders" LUMA="${TMPDIR:-/tmp}/gpu_shader_bin" VERBOSE=false if [[ "${1:-}" == "--verbose" ]]; then VERBOSE=true fi # Build if needed if [[ ! -f "$LUMA" ]] || [[ "$PROJECT_DIR/main.odin" -nt "$LUMA" ]] || \ find "$PROJECT_DIR" -name '*.odin' -newer "$LUMA" 2>/dev/null | grep -q .; then echo "Building shader compiler..." (cd "$PROJECT_DIR/.." && odin build tools/shader -out:"$LUMA") || { echo "FAIL: build failed"; exit 1; } fi # Detect available validators HAS_GLSLANG=false HAS_SPIRV_VAL=false HAS_NAGA=false HAS_DXC=false HAS_METAL=false command -v glslangValidator &>/dev/null && HAS_GLSLANG=true command -v spirv-val &>/dev/null && HAS_SPIRV_VAL=true command -v naga &>/dev/null && HAS_NAGA=true command -v dxc &>/dev/null && HAS_DXC=true command -v xcrun &>/dev/null && xcrun -sdk macosx metal --version &>/dev/null 2>&1 && HAS_METAL=true echo "Available validators:" $HAS_GLSLANG && echo " glslangValidator (GLSL)" || echo " glslangValidator (GLSL) — not found" $HAS_SPIRV_VAL && echo " spirv-val (SPIR-V)" || echo " spirv-val (SPIR-V) — not found" $HAS_NAGA && echo " naga (WGSL)" || echo " naga (WGSL) — not found, install with: cargo install naga-cli" $HAS_DXC && echo " dxc (HLSL)" || echo " dxc (HLSL) — not found" $HAS_METAL && echo " metal (MSL)" || echo " metal (MSL) — macOS only" echo "" PASS=0 FAIL=0 SKIP=0 ERRORS="" # Map Luma stage to glslangValidator file extension stage_to_ext() { case "$1" in vertex) echo ".vert" ;; fragment) echo ".frag" ;; compute) echo ".comp" ;; *) echo ".glsl" ;; esac } # Map Luma stage to HLSL profile for dxc stage_to_hlsl_profile() { case "$1" in vertex) echo "vs_6_0" ;; fragment) echo "ps_6_0" ;; compute) echo "cs_6_0" ;; *) echo "ps_6_0" ;; esac } for shader in "$SHADER_DIR"/*.luma; do name="$(basename "$shader" .luma)" # Extract entry point names and stages from source # @entry(stage) is on the line before "function name(...)" entries=() stages=() pending_stage="" while IFS= read -r line; do if [[ "$line" =~ @entry\(([a-z]+)\) ]]; then pending_stage="${BASH_REMATCH[1]}" # Check if function is on the same line if [[ "$line" =~ function[[:space:]]+([a-zA-Z_][a-zA-Z0-9_]*) ]]; then entries+=("${BASH_REMATCH[1]}") stages+=("$pending_stage") pending_stage="" fi elif [[ -n "$pending_stage" ]] && [[ "$line" =~ function[[:space:]]+([a-zA-Z_][a-zA-Z0-9_]*) ]]; then entries+=("${BASH_REMATCH[1]}") stages+=("$pending_stage") pending_stage="" fi done < "$shader" if [[ ${#entries[@]} -eq 0 ]]; then $VERBOSE && echo "SKIP: $name (no entry points found)" continue fi for idx in "${!entries[@]}"; do entry="${entries[$idx]}" stage="${stages[$idx]}" # --- GLSL validation --- if $HAS_GLSLANG; then ext=$(stage_to_ext "$stage") glsl_tmp=$(mktemp "/tmp/luma_val_XXXXXX$ext") if "$LUMA" compile "$shader" --target=glsl --entry="$entry" > "$glsl_tmp" 2>/dev/null; then if glslangValidator --target-env vulkan1.1 "$glsl_tmp" > /dev/null 2>&1; then PASS=$((PASS + 1)) $VERBOSE && echo " PASS: $name/$entry (GLSL)" else FAIL=$((FAIL + 1)) err=$(glslangValidator --target-env vulkan1.1 "$glsl_tmp" 2>&1 | head -5) ERRORS="${ERRORS}FAIL: ${name}/${entry} (GLSL)\n${err}\n\n" fi else FAIL=$((FAIL + 1)) ERRORS="${ERRORS}FAIL: ${name}/${entry} (GLSL compile error)\n" fi rm -f "$glsl_tmp" fi # --- OpenGL GLSL validation --- if $HAS_GLSLANG; then ext=$(stage_to_ext "$stage") glsl_tmp=$(mktemp "/tmp/luma_val_XXXXXX$ext") if "$LUMA" compile "$shader" --target=glsl-opengl --entry="$entry" > "$glsl_tmp" 2>/dev/null; then if glslangValidator "$glsl_tmp" > /dev/null 2>&1; then PASS=$((PASS + 1)) $VERBOSE && echo " PASS: $name/$entry (GLSL OpenGL)" else FAIL=$((FAIL + 1)) err=$(glslangValidator "$glsl_tmp" 2>&1 | head -5) ERRORS="${ERRORS}FAIL: ${name}/${entry} (GLSL OpenGL)\n${err}\n\n" fi else FAIL=$((FAIL + 1)) ERRORS="${ERRORS}FAIL: ${name}/${entry} (GLSL OpenGL compile error)\n" fi rm -f "$glsl_tmp" fi # --- SPIR-V validation --- if $HAS_SPIRV_VAL; then spv_tmp=$(mktemp /tmp/luma_val_XXXXXX.spv) if "$LUMA" compile "$shader" --target=spirv --entry="$entry" -o "$spv_tmp" 2>/dev/null; then if spirv-val "$spv_tmp" > /dev/null 2>&1; then PASS=$((PASS + 1)) $VERBOSE && echo " PASS: $name/$entry (SPIR-V)" else FAIL=$((FAIL + 1)) err=$(spirv-val "$spv_tmp" 2>&1 | head -5) ERRORS="${ERRORS}FAIL: ${name}/${entry} (SPIR-V)\n${err}\n\n" fi else FAIL=$((FAIL + 1)) ERRORS="${ERRORS}FAIL: ${name}/${entry} (SPIR-V compile error)\n" fi rm -f "$spv_tmp" fi # --- WGSL validation --- if $HAS_NAGA; then wgsl_tmp=$(mktemp /tmp/luma_val_XXXXXX.wgsl) if "$LUMA" compile "$shader" --target=wgsl --entry="$entry" > "$wgsl_tmp" 2>/dev/null; then if naga "$wgsl_tmp" > /dev/null 2>&1; then PASS=$((PASS + 1)) $VERBOSE && echo " PASS: $name/$entry (WGSL)" else FAIL=$((FAIL + 1)) err=$(naga "$wgsl_tmp" 2>&1 | head -5) ERRORS="${ERRORS}FAIL: ${name}/${entry} (WGSL)\n${err}\n\n" fi else FAIL=$((FAIL + 1)) ERRORS="${ERRORS}FAIL: ${name}/${entry} (WGSL compile error)\n" fi rm -f "$wgsl_tmp" fi # --- HLSL validation --- if $HAS_DXC; then hlsl_tmp=$(mktemp /tmp/luma_val_XXXXXX.hlsl) profile=$(stage_to_hlsl_profile "$stage") if "$LUMA" compile "$shader" --target=hlsl --entry="$entry" > "$hlsl_tmp" 2>/dev/null; then if dxc -T "$profile" -E "$entry" "$hlsl_tmp" > /dev/null 2>&1; then PASS=$((PASS + 1)) $VERBOSE && echo " PASS: $name/$entry (HLSL)" else FAIL=$((FAIL + 1)) err=$(dxc -T "$profile" -E "$entry" "$hlsl_tmp" 2>&1 | head -5) ERRORS="${ERRORS}FAIL: ${name}/${entry} (HLSL)\n${err}\n\n" fi else FAIL=$((FAIL + 1)) ERRORS="${ERRORS}FAIL: ${name}/${entry} (HLSL compile error)\n" fi rm -f "$hlsl_tmp" fi done done echo "" if [[ $FAIL -gt 0 ]]; then echo -e "$ERRORS" fi echo "Validation results: $PASS passed, $FAIL failed" $HAS_NAGA || echo "(WGSL validation skipped — install naga-cli)" $HAS_DXC || echo "(HLSL validation skipped — install dxc)" $HAS_METAL || echo "(MSL validation skipped — macOS only)" [[ $FAIL -eq 0 ]] || exit 1