diff --git a/examples/rendering/vk_manifest.ludic b/examples/rendering/vk_manifest.ludic new file mode 100644 index 00000000..441ba1ad --- /dev/null +++ b/examples/rendering/vk_manifest.ludic @@ -0,0 +1,49 @@ +# vk_manifest.ludic — reads render3d's SPIR-V manifest the way the Vulkan backend will, +# and checks every program in variants.list resolves, with its uniforms and samplers. +# bin/ludicc examples/rendering/vk_manifest.ludic --headless -o build/vk_manifest && build/vk_manifest +program VkManifest { + import "../../packages/ludic.render3d/gpu_manifest.ludic" + + property Marker { on: int = 1 } + model Anchor { Marker } + + handler Boot phase Start { + spawn Anchor {} + let n = gpu_manifest_load("packages/ludic.render3d/shaders/spv/manifest.txt") + print(`manifest: {n} programs`) + var ok = n > 0 + let list = Fs.read_text("packages/ludic.render3d/shaders/variants.list") + let lines = Text.split(list, "\n") + var listed = 0 + var found = 0 + for i in 0 .. len(lines) { + let ln = Text.trim(lines[i]) + let lp: pointer = ln + if len(lp) == 0 or lp[0] == '#' { continue } + let parts = Text.split(ln, "|") + var defs = "" + if len(parts) > 2 { defs = parts[2] } + listed += 1 + let v = gpu_variant_find_key(parts[0], parts[1], defs) + if v == null { print(` NOT FOUND: {ln}`); ok = false } else { found += 1 } + } + print(`variants.list: {listed} listed, {found} resolved`) + if found != listed { ok = false } + # spot checks against what the tool wrote: the lit foliage program as scatter builds it + let fol = gpu_variant_find("model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define ALPHA_TEST\n") + if fol == null { print(" the foliage program did not resolve"); ok = false } + else { + let uv = gpu_variant_uniform(fol, 0, "u_view") + let us = gpu_variant_uniform(fol, 1, "u_sun_dir") + print(`foliage {fol.id}: vertex block {fol.vblock} bytes, fragment block {fol.fblock} bytes, {len(fol.u_name)} uniforms, {len(fol.t_name)} samplers, {len(fol.i_name)} vertex inputs`) + if uv >= 0 { print(` u_view (vertex) at {fol.u_off[uv]}, {fol.u_type[uv]}`) } else { print(" u_view missing from the vertex stage"); ok = false } + if us >= 0 { print(` u_sun_dir (fragment) at {fol.u_off[us]}, {fol.u_type[us]}`) } else { print(" u_sun_dir missing from the fragment stage"); ok = false } + let bd = gpu_variant_sampler(fol, "u_diff") + let bs = gpu_variant_sampler(fol, "u_shadow") + print(` u_diff binding {bd}, u_shadow binding {bs}`) + if bd < 2 or bs < 2 or bd == bs { ok = false } + } + if ok { print("VKMANIFEST OK") } else { print("VKMANIFEST FAILED") } + quit() + } +} diff --git a/packages/ludic.render3d/gpu_manifest.ludic b/packages/ludic.render3d/gpu_manifest.ludic new file mode 100644 index 00000000..885c3b60 --- /dev/null +++ b/packages/ludic.render3d/gpu_manifest.ludic @@ -0,0 +1,115 @@ +# ============================================================================ +# gpu_manifest.ludic — the shader variants a pipeline API draws with. +# +# OpenGL compiles each program's GLSL when the game starts; Vulkan cannot, so every +# program render3d builds is compiled ahead of time by `ludic-dev shaders` into +# shaders/spv/.vert.spv and .frag.spv, with shaders/spv/manifest.txt saying what a +# backend needs to draw with it: +# P a program (defines with ';' for newlines) +# B a stage's uniform block +# U +# T a sampler, one binding across both stages +# I a vertex input +# This reads it, and answers the questions a backend asks while drawing: which variant a +# program is, where a uniform lives in its stage's block, which binding a sampler has. +# ============================================================================ + +property GpuVariant { + id: string, + vs: string, + fs: string, + defs: string, # the defines as variants.list writes them: ';' for newlines + vblock: int = -1, # the vertex stage's uniform block size in bytes (-1: none) + fblock: int = -1, + u_name: []string, + u_stage: []int, # 0 vertex, 1 fragment + u_off: []int, + u_type: []string, + u_count: []int, + u_stride: []int, + t_name: []string, + t_bind: []int, + i_name: []string, + i_loc: []int, + i_type: []string +} + +var gpu_variants: []GpuVariant = null + +function gpu_variant_new(id: string, vs: string, fs: string, defs: string) -> GpuVariant { + let v = new GpuVariant + v.id = id; v.vs = vs; v.fs = fs; v.defs = defs + v.u_name = new []string; v.u_stage = new []int; v.u_off = new []int; v.u_type = new []string + v.u_count = new []int; v.u_stride = new []int + v.t_name = new []string; v.t_bind = new []int + v.i_name = new []string; v.i_loc = new []int; v.i_type = new []string + return v +} + +# Read a manifest; returns how many programs it holds (0: none, or no file). +function gpu_manifest_load(path: string) -> int { + gpu_variants = new []GpuVariant + let text = Fs.read_text(path) + if text == null { return 0 } + let lines = Text.split(text, "\n") + var cur: GpuVariant = null + for li in 0 .. len(lines) { + let ln = lines[li] + let lp: pointer = ln + if len(lp) < 2 or lp[0] == '#' { continue } + let f = Text.split(ln, " ") + if lp[0] == 'P' and len(f) >= 4 { + # the defines are the rest of the line, spaces and all + var defs = "" + var k = 4 + while k < len(f) { + if k > 4 { defs = defs + " " } + defs = defs + f[k] + k += 1 + } + cur = gpu_variant_new(f[1], f[2], f[3], defs) + push(gpu_variants, cur) + continue + } + if cur == null { continue } + if lp[0] == 'B' and len(f) >= 5 { + if f[2] == "vert" { cur.vblock = Text.to_int(f[4]) } else { cur.fblock = Text.to_int(f[4]) } + } else if lp[0] == 'U' and len(f) >= 8 { + var stage = 1 + if f[2] == "vert" { stage = 0 } + push(cur.u_stage, stage); push(cur.u_name, f[3]); push(cur.u_off, Text.to_int(f[4])) + push(cur.u_type, f[5]); push(cur.u_count, Text.to_int(f[6])); push(cur.u_stride, Text.to_int(f[7])) + } else if lp[0] == 'T' and len(f) >= 4 { + push(cur.t_name, f[2]); push(cur.t_bind, Text.to_int(f[3])) + } else if lp[0] == 'I' and len(f) >= 5 { + push(cur.i_name, f[2]); push(cur.i_loc, Text.to_int(f[3])); push(cur.i_type, f[4]) + } + } + return len(gpu_variants) +} + +# the variant for a program as r3d_program names it (defines with real newlines) +function gpu_variant_find(vs: string, fs: string, defines: string) -> GpuVariant { + return gpu_variant_find_key(vs, fs, Text.replace(defines, "\n", ";")) +} +# ... or as variants.list writes it (';' for newlines) +function gpu_variant_find_key(vs: string, fs: string, defs: string) -> GpuVariant { + if gpu_variants == null { return null } + for i in 0 .. len(gpu_variants) { + let v = gpu_variants[i] + if v.vs == vs and v.fs == fs and v.defs == defs { return v } + } + return null +} + +# a uniform's index in the variant's table for a stage (0 vertex, 1 fragment), or -1; +# a program sets a uniform in whichever stages declare it, so a backend asks both +function gpu_variant_uniform(v: GpuVariant, stage: int, name: string) -> int { + for i in 0 .. len(v.u_name) { if v.u_stage[i] == stage and v.u_name[i] == name { return i } } + return -1 +} +# a sampler's binding, or -1 when neither stage declares it +function gpu_variant_sampler(v: GpuVariant, name: string) -> int { + for i in 0 .. len(v.t_name) { if v.t_name[i] == name { return v.t_bind[i] } } + return -1 +}