From 66940f9c0ac78955e1a7b3e5506e1316a9bf9c9a Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 15 Sep 2026 12:50:54 +0300 Subject: [PATCH] fix(shaders): varyings meet by name on Vulkan, so the meadow's flowers draw OpenGL links a vertex output to a fragment input by name; SPIR-V links them by location, and glslang's --auto-map-locations numbered each stage in its own declaration order. The foliage prepass's depth.frag declares v_wpos then v_uv where model.vert writes v_wpos, v_nrm, v_uv, so the prepass read a normal as its texture coordinate, its alpha test cut every flower head and leaf, and the lit pass (depth EQUAL) drew nothing over them. `ludic-dev shaders` now gives both stages explicit locations: the vertex stage's out order numbers them and the fragment stage looks each in up by name. All 45 variants checked: every fragment input sits on its vertex output. Also: - A clear still waiting for its pass when the framebuffer changes now runs on that framebuffer, instead of becoming the load op of whichever pass began next. - R3D_DUMP_ATLAS writes every impostor and card atlas a run bakes (build/atlas__*.ppm); the 40 baked on Vulkan match OpenGL's. The PC's Vulkan frame now shows the flowers as OpenGL does, validation-clean. ludic-dev test 140 passed; OpenGL frames byte-identical at the five viewpoints; 59 self-tests pass. Co-Authored-By: Claude Opus 5 --- packages/ludic.render3d/gpu.ludic | 2 +- packages/ludic.render3d/gpu_vk_draw.ludic | 8 +++ packages/ludic.render3d/scatter.ludic | 10 ++- .../shaders/spv/a64015f2.frag.spv | Bin 3280 -> 3280 bytes .../shaders/spv/b964507f.frag.spv | Bin 3280 -> 3280 bytes .../shaders/spv/bfa15301.frag.spv | Bin 3932 -> 3932 bytes .../shaders/spv/e9b6300c.frag.spv | Bin 3296 -> 3296 bytes tools/ludic-cli/shaders.ludic | 65 +++++++++++++++++- 8 files changed, 81 insertions(+), 4 deletions(-) diff --git a/packages/ludic.render3d/gpu.ludic b/packages/ludic.render3d/gpu.ludic index 75467354..8bbda7b1 100644 --- a/packages/ludic.render3d/gpu.ludic +++ b/packages/ludic.render3d/gpu.ludic @@ -738,7 +738,7 @@ function gpu_debug_check(tag: string) -> void { if gpu_kind == GPU_VK { return } function gpu_fb_new() -> int { if gpu_kind == GPU_VK { gvk_fb_counter += 1; return gvk_fb_counter }; return gl_framebuffer() } function gpu_fb_bind(fb: int) -> void { - if gpu_kind == GPU_VK { if fb != gvk_fb_cur { gvk_pass_end() }; gvk_fb_cur = fb } else { gl_bind_framebuffer(GL_FRAMEBUFFER, fb) } + if gpu_kind == GPU_VK { gvk_rebind(fb) } else { gl_bind_framebuffer(GL_FRAMEBUFFER, fb) } gpu_fb_cur = fb gpu_glcheck_after("fb bind") } diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index d1a25479..8c2a8351 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -960,6 +960,14 @@ function gvk_clear_color(r: int, g: int, b: int, a: int) -> void { gvk_clear_rgba[0] = r; gvk_clear_rgba[1] = g; gvk_clear_rgba[2] = b; gvk_clear_rgba[3] = a } function gvk_fb_colors(fb: int, n: int) -> void { if gvk_fb_ncolor != null and fb >= 0 and fb < 4096 { gvk_fb_ncolor[fb] = n } } +# The framebuffer changes: a clear still waiting for this one's pass runs now, on this target, +# rather than becoming the load op of whichever pass begins next. +function gvk_rebind(fb: int) -> void { + if fb == gvk_fb_cur { return } + if not gvk_in_pass and gvk_clear_bits != 0 { gvk_pass_begin(gpu_fb, gpu_fb_at(gvk_fb_cur)) } + gvk_pass_end() + gvk_fb_cur = fb +} function gvk_fb_forget(fb: int) -> void { if fb == gvk_fb_cur { gvk_pass_end() } gvk_fb_colors(fb, 1) diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index 1efb2687..4bf79fad 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -341,6 +341,8 @@ var sc_printed: bool = false var sc_a2c: bool = true function scatter_init() -> void { + # R3D_DUMP_ATLAS: every impostor and card atlas the run bakes, to build/atlas__{color,alpha}.ppm + sc_debug_dump = Os.has_env("R3D_DUMP_ATLAS") sc_prog = r3d_program("model.vert", "model.frag", "") sc_prog_fol = r3d_program("model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define ALPHA_TEST\n") sc_prog_fol_depth = r3d_program("model.vert", "depth.frag", "#define FOLIAGE\n#define WIND\n#define ALPHA_TEST\n") @@ -413,6 +415,7 @@ function layer_add(l: Layer, x: int, y: int, z: int, scale: int, yaw: int, seed: # ---- impostors --------------------------------------------------------------------- var sc_bake_flower: bool = false +var sc_dump_n: int = 0 function impostor_bake(model: Model, tiles: int, tw: int, th: int) -> Impostor { let im = new Impostor im.tiles = tiles @@ -475,7 +478,12 @@ function impostor_bake(model: Model, tiles: int, tw: int, th: int) -> Impostor { gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) gpu_tex_mips(GPU_TEX2D) gpu_check("impostor bake") - if sc_debug_dump { tex_dump_alpha = true; tex_dump(im.albedo, aw, th, "build/atlas_alpha.ppm"); tex_dump_alpha = false } + # numbered, so every bake of a run survives to be compared (a card layer per species bakes one) + if sc_debug_dump { + sc_dump_n += 1 + tex_dump_alpha = true; tex_dump(im.albedo, aw, th, `build/atlas_{sc_dump_n}_alpha.ppm`); tex_dump_alpha = false + tex_dump(im.albedo, aw, th, `build/atlas_{sc_dump_n}_color.ppm`) + } return im } diff --git a/packages/ludic.render3d/shaders/spv/a64015f2.frag.spv b/packages/ludic.render3d/shaders/spv/a64015f2.frag.spv index b68f8d24c6b9fc2aefbe642d7740561e07f3ea27..5439938cb1decb41666e2a0731dcec091074b70f 100644 GIT binary patch delta 14 Vcmca0c|meR1Pde6=17)u4ge>X1aSZW delta 14 Vcmca0c|meR1Pdd>=17)u4ge>L1aANU diff --git a/packages/ludic.render3d/shaders/spv/b964507f.frag.spv b/packages/ludic.render3d/shaders/spv/b964507f.frag.spv index b68f8d24c6b9fc2aefbe642d7740561e07f3ea27..5439938cb1decb41666e2a0731dcec091074b70f 100644 GIT binary patch delta 14 Vcmca0c|meR1Pde6=17)u4ge>X1aSZW delta 14 Vcmca0c|meR1Pdd>=17)u4ge>L1aANU diff --git a/packages/ludic.render3d/shaders/spv/bfa15301.frag.spv b/packages/ludic.render3d/shaders/spv/bfa15301.frag.spv index e6a2bd7df5d083af30d72f107b41f7b535eb24ef..bd809151e2d9ac6bd357f07e2d407d5a60e42604 100644 GIT binary patch delta 14 Vcmca3cSmkR0y`tq=0x`2>;Nj(1uOsn delta 14 Vcmca3cSmkR0y`t)=0x`2>;Njz1uFmm diff --git a/packages/ludic.render3d/shaders/spv/e9b6300c.frag.spv b/packages/ludic.render3d/shaders/spv/e9b6300c.frag.spv index 68786f25e53229464cfdaa75af0fbe0a10399c2e..1dbf38d99416b9a09e0e15bd29f1e86bfc9e9374 100644 GIT binary patch delta 14 VcmaDL`9N|*2@50B=2Dhy4ge~O1ib(N delta 14 VcmaDL`9N|*2@4~`=2Dhy4ge~C1iJtL diff --git a/tools/ludic-cli/shaders.ludic b/tools/ludic-cli/shaders.ludic index de75c8bf..e09cc847 100644 --- a/tools/ludic-cli/shaders.ludic +++ b/tools/ludic-cli/shaders.ludic @@ -93,6 +93,63 @@ function shd_bind_samplers(src: pointer, collect: bool) -> pointer { return sb_str(b) } +# Varyings: OpenGL links a vertex output to a fragment input by NAME; SPIR-V links by location, +# and glslang's --auto-map-locations numbers each stage in its own declaration order. A fragment +# shader that declares fewer inputs, or declares them in another order - the foliage prepass's +# depth.frag reads v_wpos then v_uv, where model.vert writes v_wpos, v_nrm, v_uv - read the wrong +# varying, and the prepass cut every alpha-tested texel. So both stages get explicit locations: +# the vertex stage's `out` order numbers them, and the fragment stage looks each `in` up by name. +var shd_vary: []pointer = null +function shd_vary_index(name: pointer) -> int { + var k = 0 + while k < len(shd_vary) { if shd_vary[k] == name { return k }; k += 1 } + return -1 +} +# `word` at the start of a line (spaces before it allowed), then whitespace, a type, a name, then +# ';' or '[' - a global declaration, never a function parameter. The rewritten text carries an +# explicit layout; with `collect`, a vertex stage's out names are recorded in order instead. +function shd_bind_varyings(src: pointer, word: pointer, collect: bool) -> pointer { + let b = sb_new() + let n = slen(src) + let wl = slen(word) + var i = 0 + var extra = 0 + while i < n { + let at = s_index(src, word, i) + if at < 0 { sb_puts(b, str_sub(src, i, n)); break } + sb_puts(b, str_sub(src, i, at)) + var q = at - 1 + while q >= 0 and (src[q] == ' ' or src[q] == '\t') { q -= 1 } + var ok = (q < 0 or src[q] == '\n') and at + wl < n and str_space(src[at + wl]) + var p = at + wl + while ok and p < n and str_space(src[p]) { p += 1 } + let ts = p + while ok and p < n and shd_is_ident(src[p]) { p += 1 } + let ty = str_sub(src, ts, p) + if ok { ok = slen(ty) > 0 } + while ok and p < n and str_space(src[p]) { p += 1 } + let ns = p + while ok and p < n and shd_is_ident(src[p]) { p += 1 } + let name = str_sub(src, ns, p) + if ok { ok = slen(name) > 0 } + var e = p + while ok and e < n and str_space(src[e]) { e += 1 } + if ok { ok = e < n and (src[e] == ';' or src[e] == '[') } + if not ok { sb_puts(b, word); i = at + wl; continue } + if collect { + if shd_vary_index(name) < 0 { push(shd_vary, name) } + sb_puts(b, str_sub(src, at, p)) + } else { + var loc = shd_vary_index(name) + # an input no vertex stage writes still needs a location of its own + if loc < 0 { loc = len(shd_vary) + extra; extra += 1 } + sb_puts(b, `layout(location = {string(loc)}) {word} {ty} {name}`) + } + i = p + } + return sb_str(b) +} + var shd_noise: pointer = null var shd_lighting: pointer = null function shd_file(name: pointer) -> pointer { @@ -177,8 +234,10 @@ function cmd_shaders() -> int { # and without it the compiler may compute their positions a hair apart and fail the test let vsrc = "#version 460\n" + defines + "invariant gl_Position;\n#define main ludic_gl_main\n" + shd_file(parts[0]) + depth_remap let fsrc = "#version 460\n" + defines + shd_noise + shd_lighting + shd_file(parts[1]) - # one binding table for both stages + # one binding table for both stages, and one location table for the varyings between them shd_names = new []pointer + shd_vary = new []pointer + shd_bind_varyings(vsrc, "out", true) shd_bind_samplers(vsrc, true) shd_bind_samplers(fsrc, true) # name order, so the table does not depend on which stage met a sampler first @@ -199,7 +258,9 @@ function cmd_shaders() -> int { var src = vsrc if stage == "frag" { src = fsrc } let glsl = `{tmp}/{id}.{stage}.glsl` - if not write_file(glsl, shd_bind_samplers(src, false)) { err(`shaders: cannot write {glsl}\n`); return 1 } + var staged = shd_bind_samplers(src, false) + if stage == "vert" { staged = shd_bind_varyings(staged, "out", false) } else { staged = shd_bind_varyings(staged, "in", false) } + if not write_file(glsl, staged) { err(`shaders: cannot write {glsl}\n`); return 1 } let spv = `{outdir}/{id}.{stage}.spv` var extra = "" if stage == "frag" { extra = " --shift-UBO-binding frag 1" }