feat(render3d): read the SPIR-V manifest the way a Vulkan backend will
gpu_manifest.ludic loads shaders/spv/manifest.txt and answers what a backend asks while drawing: which variant a program built by r3d_program is, where a uniform lives in its stage's block, which binding a sampler has, what the vertex inputs are. examples/rendering/vk_manifest.ludic resolves every program in variants.list against it (45 of 45) and checks a few offsets and bindings. Nothing imports it yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
78c1f4f9ca
commit
f97d54aa2a
2 changed files with 164 additions and 0 deletions
49
examples/rendering/vk_manifest.ludic
Normal file
49
examples/rendering/vk_manifest.ludic
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
115
packages/ludic.render3d/gpu_manifest.ludic
Normal file
115
packages/ludic.render3d/gpu_manifest.ludic
Normal file
|
|
@ -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/<id>.vert.spv and .frag.spv, with shaders/spv/manifest.txt saying what a
|
||||||
|
# backend needs to draw with it:
|
||||||
|
# P <id> <vertex> <fragment> <defines> a program (defines with ';' for newlines)
|
||||||
|
# B <id> <stage> <binding> <size> a stage's uniform block
|
||||||
|
# U <id> <stage> <name> <offset> <type> <count> <stride>
|
||||||
|
# T <id> <name> <binding> a sampler, one binding across both stages
|
||||||
|
# I <id> <name> <location> <type> 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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue