render3d: heightgen's plain and SMOOTH variants compiled (61 programs) - a smoothed or noise-only terrain had no height field on Vulkan; run-time defines as pure functions (program_defs.ludic) and manifest_test holds every program the source can ask for, literal or computed, to the manifest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 22:30:35 +03:00
parent a8ccd34559
commit 9f07fc8a62
12 changed files with 254 additions and 11 deletions

View file

@ -0,0 +1,9 @@
bump: patch
type: fix
**Every program render3d can ask for is compiled.** heightgen with no survey and with SMOOTH were never in
variants.list, so a smoothed or noise-only terrain (examples/rendering/chunks, smooth, reload) had no
height field on Vulkan - silently until render3d's faults were made loud. Both are compiled now (61
programs). A program's run-time defines are pure functions in program_defs.ludic (the height field, the
ground's tiers, the blades), and tests/manifest_test reads every r3d_program / gpu_program / ac_prog_new call
in render3d's source, holds each literal one and every value of each computed one to the shipped manifest,
and fails a new computed call site until it is given its function.

View file

@ -96,9 +96,8 @@ function grass_init(render3d_st: mut Render3dState) -> void {
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
function grass_init__t(render3d_st: mut Render3dState) -> void {
render3d_st.grass_merge = gpu_has_mdi(render3d_st) and not r3d_env_has(render3d_st, "R3D_GRASS_TILES")
var defs = "#define FOLIAGE\n#define BLADE\n#define GBLADE\n"
let defs = grass_defs(render3d_st.grass_merge)
if render3d_st.grass_merge {
defs = defs + "#define TILES\n"
render3d_st.grass_rec = words(GRASS_RECS * 5); render3d_st.grass_tv = floats(GRASS_RECS * 4); render3d_st.grass_chunk_tv = floats(GRASS_CHUNK * 4)
render3d_st.grass_band_start = words(4); render3d_st.grass_band_cells = words(4)
render3d_st.grass_band_mesh = new []Mesh

View file

@ -0,0 +1,25 @@
# program_defs.ludic - the defines of every program whose defines are decided at run time, each a pure
# function of what decides it, so tests/manifest_test can ask for every one a run could and hold the
# SPIR-V manifest to them. A new program made with computed defines gets its function here, and a line there.
# the height field's generator: from a survey (DEM), smoothed (SMOOTH wins), or noise alone
function ter_heightgen_defs(dem: bool, smooth: bool) -> string {
if smooth { return "#define SMOOTH\n" }
if dem { return "#define DEM\n" }
return ""
}
# the ground's program: `debug` the dev switches' defines ("" in every shipped run), the cascades read
# inside it on Vulkan (SUN_INLINE; R3D_SUN_PASS keeps a pass), tier 0 both, 1 far only, 2 near only
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
function ter_prog_defs(debug: string, sun_inline: bool, tier: int) -> string {
var d = debug
if sun_inline { d = d + "#define SUN_INLINE\n" }
if tier == 1 { d = d + "#define FAR_ONLY\n" }
if tier == 2 { d = d + "#define NEAR_ONLY\n" }
return d
}
# the blades: merged into tiles where the device draws indirect (TILES), else a draw a chunk
function grass_defs(merge: bool) -> string {
if merge { return "#define FOLIAGE\n#define BLADE\n#define GBLADE\n#define TILES\n" }
return "#define FOLIAGE\n#define BLADE\n#define GBLADE\n"
}

View file

@ -21,6 +21,7 @@ import "prof.ludic"
import "drawstats.ludic"
import "r3d_root.ludic"
import "programs.ludic"
import "program_defs.ludic"
import "texture.ludic"
import "baked_tex.ludic"
import "mesh.ludic"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -199,6 +199,53 @@ T 5161e6ba u_src 7
T 5161e6ba u_tershadow 8
T 5161e6ba u_tershadow_aux 9
T 5161e6ba u_ts_height 10
P 5b8c3ae8 fullscreen.vert heightgen.frag
B 5b8c3ae8 frag 1 836
U 5b8c3ae8 frag u_cascade_vp 0 mat4 5 64
U 5b8c3ae8 frag u_cascade_split 320 float 5 16
U 5b8c3ae8 frag u_cascade_range 400 float 5 16
U 5b8c3ae8 frag u_cascade_texel 480 float 5 16
U 5b8c3ae8 frag u_sun_dir 560 vec3 1 0
U 5b8c3ae8 frag u_sun_color 576 vec3 1 0
U 5b8c3ae8 frag u_cam_pos 592 vec3 1 0
U 5b8c3ae8 frag u_prefilter_levels 604 float 1 0
U 5b8c3ae8 frag u_fog_density 608 float 1 0
U 5b8c3ae8 frag u_fog_height_falloff 612 float 1 0
U 5b8c3ae8 frag u_fog_base 616 float 1 0
U 5b8c3ae8 frag u_clip_y 620 float 1 0
U 5b8c3ae8 frag u_spec_scale 624 float 1 0
U 5b8c3ae8 frag u_sky_rot 632 vec2 1 0
U 5b8c3ae8 frag u_ground_alb 640 vec3 1 0
U 5b8c3ae8 frag u_ground_alb_hi 656 vec3 1 0
U 5b8c3ae8 frag u_ground_hi_y 668 float 1 0
U 5b8c3ae8 frag u_ground_hi_w 672 float 1 0
U 5b8c3ae8 frag u_ibl_scale 688 vec3 1 0
U 5b8c3ae8 frag u_daylight 700 float 1 0
U 5b8c3ae8 frag u_fire_pos 704 vec3 1 0
U 5b8c3ae8 frag u_fire_color 720 vec3 1 0
U 5b8c3ae8 frag u_hand_pos 736 vec3 1 0
U 5b8c3ae8 frag u_hand_color 752 vec3 1 0
U 5b8c3ae8 frag u_hand_dir 768 vec3 1 0
U 5b8c3ae8 frag u_hand_cone 780 float 1 0
U 5b8c3ae8 frag u_hand_reach 784 float 1 0
U 5b8c3ae8 frag u_ts_origin 792 vec2 1 0
U 5b8c3ae8 frag u_ts_half 800 float 1 0
U 5b8c3ae8 frag u_ts_on 804 float 1 0
U 5b8c3ae8 frag u_force_cascade 808 int 1 0
U 5b8c3ae8 frag u_cloud_shadow 812 float 1 0
U 5b8c3ae8 frag u_time 816 float 1 0
U 5b8c3ae8 frag u_fog_inscatter 820 float 1 0
U 5b8c3ae8 frag u_fog_desat 824 float 1 0
U 5b8c3ae8 frag u_fog_wall 828 float 1 0
U 5b8c3ae8 frag u_half 832 float 1 0
T 5b8c3ae8 u_brdf 2
T 5b8c3ae8 u_dem 3
T 5b8c3ae8 u_irradiance 4
T 5b8c3ae8 u_prefilter 5
T 5b8c3ae8 u_shadow 6
T 5b8c3ae8 u_tershadow 7
T 5b8c3ae8 u_tershadow_aux 8
T 5b8c3ae8 u_ts_height 9
P 4be7acd9 fullscreen.vert heightgen.frag #define DEM;
B 4be7acd9 frag 1 920
U 4be7acd9 frag u_cascade_vp 0 mat4 5 64
@ -257,6 +304,53 @@ T 4be7acd9 u_shadow 6
T 4be7acd9 u_tershadow 7
T 4be7acd9 u_tershadow_aux 8
T 4be7acd9 u_ts_height 9
P c60a9a25 fullscreen.vert heightgen.frag #define SMOOTH;
B c60a9a25 frag 1 836
U c60a9a25 frag u_cascade_vp 0 mat4 5 64
U c60a9a25 frag u_cascade_split 320 float 5 16
U c60a9a25 frag u_cascade_range 400 float 5 16
U c60a9a25 frag u_cascade_texel 480 float 5 16
U c60a9a25 frag u_sun_dir 560 vec3 1 0
U c60a9a25 frag u_sun_color 576 vec3 1 0
U c60a9a25 frag u_cam_pos 592 vec3 1 0
U c60a9a25 frag u_prefilter_levels 604 float 1 0
U c60a9a25 frag u_fog_density 608 float 1 0
U c60a9a25 frag u_fog_height_falloff 612 float 1 0
U c60a9a25 frag u_fog_base 616 float 1 0
U c60a9a25 frag u_clip_y 620 float 1 0
U c60a9a25 frag u_spec_scale 624 float 1 0
U c60a9a25 frag u_sky_rot 632 vec2 1 0
U c60a9a25 frag u_ground_alb 640 vec3 1 0
U c60a9a25 frag u_ground_alb_hi 656 vec3 1 0
U c60a9a25 frag u_ground_hi_y 668 float 1 0
U c60a9a25 frag u_ground_hi_w 672 float 1 0
U c60a9a25 frag u_ibl_scale 688 vec3 1 0
U c60a9a25 frag u_daylight 700 float 1 0
U c60a9a25 frag u_fire_pos 704 vec3 1 0
U c60a9a25 frag u_fire_color 720 vec3 1 0
U c60a9a25 frag u_hand_pos 736 vec3 1 0
U c60a9a25 frag u_hand_color 752 vec3 1 0
U c60a9a25 frag u_hand_dir 768 vec3 1 0
U c60a9a25 frag u_hand_cone 780 float 1 0
U c60a9a25 frag u_hand_reach 784 float 1 0
U c60a9a25 frag u_ts_origin 792 vec2 1 0
U c60a9a25 frag u_ts_half 800 float 1 0
U c60a9a25 frag u_ts_on 804 float 1 0
U c60a9a25 frag u_force_cascade 808 int 1 0
U c60a9a25 frag u_cloud_shadow 812 float 1 0
U c60a9a25 frag u_time 816 float 1 0
U c60a9a25 frag u_fog_inscatter 820 float 1 0
U c60a9a25 frag u_fog_desat 824 float 1 0
U c60a9a25 frag u_fog_wall 828 float 1 0
U c60a9a25 frag u_half 832 float 1 0
T c60a9a25 u_brdf 2
T c60a9a25 u_dem 3
T c60a9a25 u_irradiance 4
T c60a9a25 u_prefilter 5
T c60a9a25 u_shadow 6
T c60a9a25 u_tershadow 7
T c60a9a25 u_tershadow_aux 8
T c60a9a25 u_ts_height 9
P 858f5089 fullscreen.vert ibl_brdf.frag
B 858f5089 frag 0 832
U 858f5089 frag u_cascade_vp 0 mat4 5 64
@ -3995,4 +4089,4 @@ T b20fdcb5 u_shadow 9
T b20fdcb5 u_tershadow 10
T b20fdcb5 u_tershadow_aux 11
T b20fdcb5 u_ts_height 12
E 59
E 61

View file

@ -6,7 +6,9 @@ fullscreen.vert|adapt.frag|
fullscreen.vert|bloom_down.frag|
fullscreen.vert|bloom_up.frag|
fullscreen.vert|dof.frag|
fullscreen.vert|heightgen.frag|
fullscreen.vert|heightgen.frag|#define DEM;
fullscreen.vert|heightgen.frag|#define SMOOTH;
fullscreen.vert|ibl_brdf.frag|
fullscreen.vert|ibl_irradiance.frag|
fullscreen.vert|ibl_prefilter.frag|

View file

@ -191,10 +191,7 @@ function terrain_generate(render3d_st: mut Render3dState) -> void {
render3d_st.gvk_tag = was
}
function terrain_generate__t(render3d_st: mut Render3dState) -> void {
var defs = ""
if render3d_st.ter_dem_tex != 0 { defs = "#define DEM\n" }
if render3d_st.ter_smooth { defs = "#define SMOOTH\n" }
let p = r3d_program(render3d_st, "fullscreen.vert", "heightgen.frag", defs)
let p = r3d_program(render3d_st, "fullscreen.vert", "heightgen.frag", ter_heightgen_defs(render3d_st.ter_dem_tex != 0, render3d_st.ter_smooth))
# a map generated over another: its 256 MB height texture goes first
if render3d_st.ter_height_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_height_tex); render3d_st.ter_height_tex = 0 }
render3d_st.ter_height_tex = tex_target(render3d_st, TERRAIN_RES, TERRAIN_RES, GL_R32F, GL_RED, GL_FLOAT, GL_LINEAR)
@ -453,10 +450,9 @@ function terrain_init_finish__t(render3d_st: mut Render3dState) -> void {
# (terrain.frag, SUN_INLINE): one rasterisation of the patches instead of two, in the frame and in
# the reflection. R3D_SUN_PASS=1 keeps the pass there too, for comparing.
render3d_st.ter_sun_inline = render3d_st.gpu_kind == GPU_VK and not r3d_env_has(render3d_st, "R3D_SUN_PASS")
if render3d_st.ter_sun_inline { defs = defs + "#define SUN_INLINE\n" }
render3d_st.ter_prog = r3d_program(render3d_st, "terrain.vert", "terrain.frag", defs)
render3d_st.ter_prog_far = r3d_program(render3d_st, "terrain.vert", "terrain.frag", defs + "#define FAR_ONLY\n")
render3d_st.ter_prog_near = r3d_program(render3d_st, "terrain.vert", "terrain.frag", defs + "#define NEAR_ONLY\n")
render3d_st.ter_prog = r3d_program(render3d_st, "terrain.vert", "terrain.frag", ter_prog_defs(defs, render3d_st.ter_sun_inline, 0))
render3d_st.ter_prog_far = r3d_program(render3d_st, "terrain.vert", "terrain.frag", ter_prog_defs(defs, render3d_st.ter_sun_inline, 1))
render3d_st.ter_prog_near = r3d_program(render3d_st, "terrain.vert", "terrain.frag", ter_prog_defs(defs, render3d_st.ter_sun_inline, 2))
render3d_st.ter_sun_prog = r3d_program(render3d_st, "terrain.vert", "tersun.frag", "")
render3d_st.ter_far_split = 200.0
if r3d_env_has(render3d_st, "R3D_TFAR") { render3d_st.ter_far_split = float(Text.to_int(r3d_env(render3d_st, "R3D_TFAR"))) }

View file

@ -64,4 +64,121 @@ program ManifestTest {
expect_eq(r3d_tilde("~other/x", "/Users/me"), "~other/x")
expect_eq(r3d_tilde("~/x", null), "~/x")
}
# ---- every program render3d can ask for, not only what variants.list names ----
# A request is r3d_program / gpu_program / ac_prog_new(state, vertex, fragment, defines). With all three
# literal it is read straight out of the source; with computed defines its site is one of KNOWN, whose
# defines are program_defs.ludic's functions, asked below for every value. A new computed site fails
# here by name until it is given a function there and a line in `computed_all`.
function shipped() -> []GpuVariant { return gpu_manifest_parse(Fs.read_text("packages/ludic.render3d/shaders/spv/manifest.txt")) }
function known_computed(file: string) -> int {
if file == "programs.ludic" { return 1 } # r3d_program itself, handing on to gpu_program
if file == "actor.ludic" { return 1 } # ac_prog_new's own call: its callers are literal
if file == "terrain.ludic" { return 4 } # heightgen and the ground's three tiers
if file == "grass.ludic" { return 1 } # the blades
return 0
}
# the defines as the manifest keys them: the source's "\n" escapes as ';'
function key_defs(lit: string) -> string {
var out = ""
var i = 0
while i < len(lit) {
if lit[i] == 92 and i + 1 < len(lit) and lit[i + 1] == 110 {
out = out + ";"
i += 2
} else {
out = out + lit[i .. i + 1]
i += 1
}
}
return out
}
function is_lit(a: string) -> bool { return len(a) >= 2 and a[0] == 34 and a[len(a) - 1] == 34 }
function unq(a: string) -> string { return a[1 .. len(a) - 1] }
# the top-level arguments of the call whose '(' is at `open`, trimmed
function args_at(src: string, open: int) -> []string {
let out = new []string
var depth = 0
var inq = false
var start = open + 1
var i = open + 1
while i < len(src) {
let c = src[i]
if c == 34 { inq = not inq }
if not inq {
if c == 40 { depth += 1 }
if c == 41 and depth == 0 {
push(out, Text.trim(src[start .. i]))
return out
}
if c == 41 { depth -= 1 }
if c == 44 and depth == 0 {
push(out, Text.trim(src[start .. i]))
start = i + 1
}
}
i += 1
}
return out
}
# checks every request in one file against the manifest; returns how many had computed arguments
function file_requests(vs: []GpuVariant, file: string, missing: []string) -> int {
let src = Fs.read_text(`packages/ludic.render3d/{file}`)
var computed = 0
let names = ["r3d_program(", "gpu_program(", "ac_prog_new("]
for n in 0 .. len(names) {
let parts = Text.split(src, names[n])
var at = 0
for k in 0 .. len(parts) - 1 {
at += len(parts[k])
let before = parts[k]
let fn_decl = Text.ends_with(before, "function ")
let ident = len(before) > 0 and (char_word(before[len(before) - 1]))
if not fn_decl and not ident {
let a = args_at(src, at + len(names[n]) - 1)
if len(a) == 4 and is_lit(a[1]) and is_lit(a[2]) and is_lit(a[3]) {
if gpu_variant_in(vs, unq(a[1]), unq(a[2]), key_defs(unq(a[3]))) == null { push(missing, `{file}: {a[1]} {a[2]} {a[3]}`) }
} else {
computed += 1
}
}
at += len(names[n])
}
}
return computed
}
function char_word(c: int) -> bool { return (c >= 97 and c <= 122) or (c >= 65 and c <= 90) or (c >= 48 and c <= 57) or c == 95 or c == 46 }
# every value a computed site can take (dev switches' debug views left out: they are not shipped)
function computed_all(vs: []GpuVariant, missing: []string) -> void {
let flags = [false, true]
for a in 0 .. 2 {
for b in 0 .. 2 { need(vs, "fullscreen.vert", "heightgen.frag", ter_heightgen_defs(flags[a], flags[b]), missing) }
for t in 0 .. 3 { need(vs, "terrain.vert", "terrain.frag", ter_prog_defs("", flags[a], t), missing) }
need(vs, "grass.vert", "model.frag", grass_defs(flags[a]), missing)
}
}
function need(vs: []GpuVariant, v: string, f: string, defs: string, missing: []string) -> void {
let k = Text.replace(defs, "\n", ";")
if gpu_variant_in(vs, v, f, k) == null { push(missing, `{v}|{f}|{k}`) }
}
test "every program render3d can ask for has a variant in the shipped manifest" {
let vs = shipped()
expect(vs != null)
let missing = new []string
let files = Fs.list("packages/ludic.render3d")
var asked_files = 0
for i in 0 .. len(files) {
if not Text.ends_with(files[i], ".ludic") { continue }
let computed = file_requests(vs, files[i], missing)
if computed != known_computed(files[i]) { print(`{files[i]}: {computed} requests with computed arguments, {known_computed(files[i])} known - give the new one a function in program_defs.ludic`) }
expect_eq(computed, known_computed(files[i]))
asked_files += 1
}
expect(asked_files > 20)
computed_all(vs, missing)
for i in 0 .. len(missing) { print(`no SPIR-V variant for {missing[i]}`) }
expect_eq(len(missing), 0)
}
}