aboutsummaryrefslogtreecommitdiff
path: root/tools/compile-shaders.sh
blob: a5d786ed497336b01e6a451fb485ddc5ad55c7ad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env bash
#
# Compiles every assets/shaders/*.slang to all backend targets, into
# assets/shaders/generated/ (gitignored):
#
#   OpenGL : <name>.glsl        - combined "#type" GLSL 4.10 (Slang -> SPIR-V ->
#                                 SPIRV-Cross), macOS-4.1-core valid, loaded by
#                                 OpenGLShader.
#   Vulkan : <name>.<entry>.spv - SPIR-V per entry point.
#   Metal  : <name>.metal       - MSL, whole module.
#
# Entry points are auto-discovered from Slang [shader("stage")] attributes.
# Override tool paths with SLANGC / SPIRV_CROSS.
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# the vulkan sdk ships both tools (git bash on windows); tools/slang + brew otherwise.
if [ -z "${SLANGC:-}" ]; then
  if [ -n "${VULKAN_SDK:-}" ] && [ -x "$VULKAN_SDK/Bin/slangc.exe" ]; then SLANGC="$VULKAN_SDK/Bin/slangc.exe"
  else SLANGC="$ROOT/tools/slang/bin/slangc"; fi
fi
if [ -z "${SPIRV_CROSS:-}" ]; then
  if [ -n "${VULKAN_SDK:-}" ] && [ -x "$VULKAN_SDK/Bin/spirv-cross.exe" ]; then SPIRV_CROSS="$VULKAN_SDK/Bin/spirv-cross.exe"
  else SPIRV_CROSS="$(command -v spirv-cross || echo spirv-cross)"; fi
fi
SRC="$ROOT/assets/shaders"
OUT="$SRC/generated"

[ -x "$SLANGC" ] || { echo "compile-shaders: slangc not found at $SLANGC (run tools/fetch-slang.sh)"; exit 1; }
command -v "$SPIRV_CROSS" >/dev/null 2>&1 || { echo "compile-shaders: spirv-cross not found (brew install spirv-cross)"; exit 1; }

mkdir -p "$OUT"
shopt -s nullglob
shaders=("$SRC"/*.slang)
[ ${#shaders[@]} -gt 0 ] || { echo "compile-shaders: no .slang files in $SRC yet"; exit 0; }

# Prints "stage entryName" for each [shader("stage")] entry point in a file.
entry_points() {
  awk '
    /\[shader\(/ { if (match($0, /"[a-zA-Z]+"/)) { stage = substr($0, RSTART+1, RLENGTH-2); want = 1 } next }
    want && $0 ~ /^[ \t]*\[/ { next }              # skip further attributes
    want && /\(/ {
      sig = $0; sub(/\(.*/, "", sig)
      n = split(sig, t, /[ \t*&]+/); name = t[n]
      if (name != "") print stage, name
      want = 0
    }
  ' "$1"
}

fail=0
for f in "${shaders[@]}"; do
  name="$(basename "$f" .slang)"
  echo "== $name.slang =="

  # Metal: whole module.
  if "$SLANGC" "$f" -target metal -o "$OUT/$name.metal" 2>"$OUT/.err"; then
    echo "   metal  -> $name.metal"
  else echo "   metal  FAILED:"; sed 's/^/     /' "$OUT/.err"; fail=1; fi

  # OpenGL + Vulkan: per entry point. Shaders that declare a ConstantBuffer keep
  # real UBO blocks (the Engine binds them by name); the rest have Slang's
  # $Globals flattened to loose uniforms so per-draw glUniform* calls still work.
  if grep -q 'ConstantBuffer<' "$f"; then ubo=1; else ubo=0; fi
  combined="$OUT/$name.glsl"; : > "$combined"
  while read -r stage entry; do
    [ -z "${stage:-}" ] && continue
    spv="$OUT/$name.$entry.spv"

    if ! "$SLANGC" "$f" -target spirv -entry "$entry" -stage "$stage" -o "$spv" 2>"$OUT/.err"; then
      echo "   spirv  FAILED ($entry):"; sed 's/^/     /' "$OUT/.err"; fail=1; continue
    fi
    echo "   spirv  -> $name.$entry.spv ($stage)"

    # SPIR-V -> classic GLSL 4.10 (combined samplers, no binding layouts).
    xcopts="--version 410 --no-es --no-420pack-extension"
    [ "$ubo" = 0 ] && xcopts="$xcopts --glsl-emit-ubo-as-plain-uniforms"

    if "$SPIRV_CROSS" $xcopts "$spv" --output "$OUT/.stage.glsl" 2>"$OUT/.err"; then
      if [ "$ubo" = 1 ]; then
        # Real UBOs: strip SPIRV-Cross's "_std140" block-name suffix so the block
        # names match glGetUniformBlockIndex("Camera") etc. in the Engine.
        perl -i -pe 's/_std140//g' "$OUT/.stage.glsl"
      else
        # Flatten "struct S {..}; uniform S inst;" into loose "uniform TYPE NAME;"
        # and strip the "inst." prefix, so uniforms keep their original names and
        # the existing C++ SetMat4("u_X") / SetFloat("u_Y") calls work unchanged.
        perl -0777 -i -pe '
          while (/struct\s+(\w+)\s*\{(.*?)\}\s*;\s*uniform\s+\1\s+(\w+)\s*;/s) {
            my ($s,$body,$inst)=($1,$2,$3); my @u;
            while ($body =~ /([A-Za-z_]\w*)\s+([A-Za-z_]\w*(?:\[\d+\])?)\s*;/g) { push @u,"uniform $1 $2;"; }
            my $loose=join("\n",@u);
            s/struct\s+\Q$s\E\s*\{.*?\}\s*;\s*uniform\s+\Q$s\E\s+\Q$inst\E\s*;/$loose/s;
            s/\b\Q$inst\E\.//g;
          }
        ' "$OUT/.stage.glsl"
      fi
      # SPIRV-Cross names inter-stage varyings per stage and relies on explicit
      # locations, but macOS's GL linker matches varyings by NAME. Rename each
      # varying to a location-canonical name (_v<loc>) so stages agree: rename
      # `out` in vertex stages and `in` in fragment stages (never attributes).
      case "$stage" in vertex) vdir=out ;; fragment) vdir=in ;; *) vdir="" ;; esac
      if [ -n "$vdir" ]; then
        DIR="$vdir" perl -0777 -i -pe '
          my $d=$ENV{DIR}; my %m;
          while (/layout\(location = (\d+)\)\s+\Q$d\E\s+\w+\s+(\w+)\s*;/g) { $m{$2}="_v$1"; }
          for my $n (keys %m) { s/\b\Q$n\E\b/$m{$n}/g; }
        ' "$OUT/.stage.glsl"
      fi
      { echo "#type $stage"; cat "$OUT/.stage.glsl"; echo; } >> "$combined"
      echo "   glsl   -> $name.glsl (#type $stage)"
    else echo "   glsl   FAILED ($entry):"; sed 's/^/     /' "$OUT/.err"; fail=1; fi
  done < <(entry_points "$f")
done

rm -f "$OUT/.err" "$OUT/.stage.glsl"
exit $fail