Harbor

branch main
showing the latest snapshot on main
validate.sh 7.9 KB · Shell
shader/tests/validate.sh 0755 Raw
#!/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