render3d: the deterministic work as bytes a bake keeps, and the textures made from them

Each pair runs the work as the renderer always has and reads the result back, or makes the same
textures and fills them from bytes instead: impostor_bytes / impostor_from_bytes (and impostor_fill,
for a fog that opens past a layer's cards), terrain_shadow_bytes / terrain_shadow_from_bytes,
sky_ibl_bytes / sky_ibl_from_bytes (sky_precompute split into sky_ibl_make and the convolutions).
A bundle is a word count, a word length per part and the parts (bake_pack / bake_unpack), checked
against the textures' shapes before any byte is used. gpu_vk_readback.ludic reads a texture's level 0,
every layer, as stored; r3d_baked_read reads a bake's file (ludic.base's LBAK header: key and version
must match) since render3d cannot import ludic.base. Nothing calls them yet: behaviour unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 17:19:13 +03:00
parent 7ffa0ecf6d
commit 883ea8d48a
4 changed files with 211 additions and 12 deletions

View file

@ -0,0 +1,133 @@
# bake_parts.ludic — the renderer's deterministic work as bytes a build-time bake can keep, and the same
# textures made from those bytes at run time instead of being computed again. Each pair is: *_bytes
# (run the work as the renderer always has, read the result back) and *_from_bytes (make the textures
# as the work would have, then fill them). A bundle is a word count of parts, each a word length and
# its bytes, so a reader can check it before using it.
# the parts of a bundle packed after a header of their lengths
@alloc_ok("a bake: made once at build time, or read once at load")
function bake_pack(render3d_st: mut Render3dState, texs: words) -> []byte {
var total = 4 + len(texs) * 4
for i in 0 .. len(texs) { total += gpu_tex_byte_size(render3d_st, texs[i]) }
let out = buffer(total)
let p = data_of(out)
Vk.put_i32(p, 0, len(texs))
var at = 4 + len(texs) * 4
for i in 0 .. len(texs) {
let n = gpu_tex_byte_size(render3d_st, texs[i])
Vk.put_i32(p, 4 + i * 4, n)
gpu_tex_read_all(render3d_st, texs[i], mem_off(p, at))
at += n
}
return out
}
# the textures filled from a bundle bake_pack made of textures of the same shapes; false when it does not fit
function bake_unpack(render3d_st: mut Render3dState, data: []byte, texs: words) -> bool {
let p = data_of(data)
if len(data) < 4 or Vk.get_i32(p, 0) != len(texs) { return false }
var at = 4 + len(texs) * 4
for i in 0 .. len(texs) {
let n = Vk.get_i32(p, 4 + i * 4)
if n != gpu_tex_byte_size(render3d_st, texs[i]) or at + n > len(data) { return false }
if not gpu_tex_write_all(render3d_st, texs[i], mem_off(p, at)) { return false }
at += n
}
return true
}
@alloc_ok("a bake's list of textures, once")
function bake_list2(a: int, b: int) -> words {
let w = words(2)
w[0] = a; w[1] = b
return w
}
@alloc_ok("a bake's list of textures, once")
function bake_list3(a: int, b: int, c: int) -> words {
let w = words(3)
w[0] = a; w[1] = b; w[2] = c
return w
}
# ---- an impostor: its albedo and normal atlases (level 0; mips are made after) ----
function impostor_bytes(render3d_st: mut Render3dState, im: Impostor) -> []byte { return bake_pack(render3d_st, bake_list2(im.albedo, im.normal)) }
# an impostor with the bake's shape (model, tiles and tile size as impostor_bake takes them) from bytes
function impostor_from_bytes(render3d_st: mut Render3dState, model: Model, tiles: int, tw: int, th: int, data: []byte) -> Impostor {
let im = new Impostor
im.tiles = tiles; im.radius = model.radius * 1.02; im.height = model.height
im.model = model; im.tw = tw; im.th = th; im.flower = render3d_st.sc_bake_flower
impostor_fill(render3d_st, im, data)
return im
}
# an impostor's atlases made and filled from bytes (at a load, or when a fog opens past its cards)
function impostor_fill(render3d_st: mut Render3dState, im: Impostor, data: []byte) -> bool {
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_IMPOSTOR
let aw = im.tiles * im.tw
im.albedo = tex_target(render3d_st, aw, im.th, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
im.normal = tex_target(render3d_st, aw, im.th, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
let ok = bake_unpack(render3d_st, data, bake_list2(im.albedo, im.normal))
for t in 0 .. 2 {
var tex = im.albedo
if t == 1 { tex = im.normal }
gpu_tex_bind(render3d_st, GPU_TEX2D, tex)
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
gpu_tex_mips(render3d_st, GPU_TEX2D)
}
render3d_st.gvk_tag = was
return ok
}
# ---- the terrain's height-field sun shadow: R32F lit height, RG16F distance and cloud mask ----
function terrain_shadow_bytes(render3d_st: mut Render3dState) -> []byte {
terrain_bake_shadow(render3d_st)
return bake_pack(render3d_st, bake_list2(render3d_st.ter_shadow_tex, render3d_st.ter_shadow_aux))
}
function terrain_shadow_from_bytes(render3d_st: mut Render3dState, data: []byte) -> bool {
if render3d_st.ter_shadow_tex == 0 { render3d_st.ter_shadow_tex = tex_target(render3d_st, TERRAIN_SHADOW_RES, TERRAIN_SHADOW_RES, GL_R32F, GL_RED, GL_FLOAT, GL_LINEAR) }
if render3d_st.ter_shadow_aux == 0 { render3d_st.ter_shadow_aux = tex_target(render3d_st, TERRAIN_SHADOW_RES, TERRAIN_SHADOW_RES, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR) }
let ok = bake_unpack(render3d_st, data, bake_list2(render3d_st.ter_shadow_tex, render3d_st.ter_shadow_aux))
if ok { render3d_st.ter_shadow_yaw = render3d_st.sky_yaw }
return ok
}
# ---- the sky's image-based lighting: irradiance, the prefiltered levels, the BRDF table ----
# for the HDRI already loaded and the yaw already set (the yaw is part of what a bake is keyed on)
function sky_ibl_bytes(render3d_st: mut Render3dState) -> []byte {
sky_precompute(render3d_st)
return bake_pack(render3d_st, bake_list3(render3d_st.sky_irradiance, render3d_st.sky_prefilter, render3d_st.sky_brdf))
}
# the IBL set made from a bake of the same HDRI, yaw and prefilter width; false (and nothing kept) when
# it does not fit, and the caller computes it
function sky_ibl_from_bytes(render3d_st: mut Render3dState, data: []byte) -> bool {
sky_ibl_make(render3d_st)
if not bake_unpack(render3d_st, data, bake_list3(render3d_st.sky_irradiance, render3d_st.sky_prefilter, render3d_st.sky_brdf)) { return false }
render3d_st.sky_baked = true
render3d_st.sky_baked_yaw = render3d_st.sky_yaw
return true
}
# ---- reading a bake's file (ludic.base's format; render3d cannot import ludic.base) ----
# "LBAK" | u32 format 1 | u32 version | u32 payload offset | u64 inputs hash | u64 payload length | key
const BAKE_MAGIC: int = 1262567756 # "LBAK", little-endian
# the payload of the bake at `path` when its key and version are these, else null (missing, stale, torn)
@alloc_ok("a bake read once at load")
function r3d_baked_read(path: string, key: string, version: int) -> []byte {
if not Fs.exists(path) { return null }
let all = Fs.read_bytes(path)
if all == null or len(all) < 32 { return null }
let p = data_of(all)
if Vk.get_i32(p, 0) != BAKE_MAGIC or Vk.get_i32(p, 4) != 1 or Vk.get_i32(p, 8) != version { return null }
let off = Vk.get_i32(p, 12)
let n = Vk.get_i32(p, 24)
if off < 32 or off + n > len(all) { return null }
for i in 0 .. len(key) { if off <= 32 + i or all[32 + i] != key[i] { return null } }
let out = buffer(n)
mem_copy(data_of(out), mem_off(p, off), n)
free(all)
return out
}

View file

@ -0,0 +1,61 @@
# gpu_vk_readback.ludic — a texture's level 0, every layer, read back as the bytes it stores, and put back
# from them: what a build-time bake keeps of a texture the renderer made (bake_parts.ludic).
# the bytes a texture's level 0 takes, every layer, in the format it is stored in
function gpu_tex_byte_size(render3d_st: Render3dState, tex: int) -> int {
let ifmt = render3d_st.gvk_tex_glfmt[tex]
return render3d_st.gvk_tex_dims_w[tex] * render3d_st.gvk_tex_dims_h[tex] * render3d_st.gvk_tex_layers[tex] * gvk_channels(ifmt) * gvk_channel_bytes(ifmt)
}
# level 0 of layer `layer` into `out` (w * h * channels * bytes), as stored
function gvk_tex_read_layer(render3d_st: mut Render3dState, tex: int, layer: int, out: pointer) -> bool {
let ifmt = render3d_st.gvk_tex_glfmt[tex]
let w = render3d_st.gvk_tex_dims_w[tex]
let h = render3d_st.gvk_tex_dims_h[tex]
let layers = render3d_st.gvk_tex_layers[tex]
let n = w * h * gvk_channels(ifmt) * gvk_channel_bytes(ifmt)
let src = gvk_staging(render3d_st, n, VK_BUFFER_USAGE_TRANSFER_DST_BIT)
if src == null { return false }
let image = render3d_st.gvk_tex_image[tex]
let levels = render3d_st.gvk_tex_levels[tex]
let cb = gvk_once_begin(render3d_st)
gvk_barrier(render3d_st, cb, image, false, 0, levels, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
let bic = gvk_tmp(render3d_st, VkBufferImageCopy_sizeof)
Vk.zero(bic, VkBufferImageCopy_sizeof)
Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_baseArrayLayer, layer)
Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_layerCount, 1)
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_width, w)
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_height, h)
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_depth, 1)
Vk.cmd_copy_image_to_buffer(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_st_buf, 1, bic)
gvk_barrier(render3d_st, cb, image, false, 0, levels, layers, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
let ok = gvk_once_end(render3d_st, cb)
if ok { mem_copy(out, src, n) }
gvk_staging_free(render3d_st)
return ok
}
# every layer's level 0 into `out` (gpu_tex_byte_size bytes), layer after layer
function gpu_tex_read_all(render3d_st: mut Render3dState, tex: int, out: pointer) -> bool {
let layers = render3d_st.gvk_tex_layers[tex]
let per = gpu_tex_byte_size(render3d_st, tex) / max(layers, 1)
for l in 0 .. layers { if not gvk_tex_read_layer(render3d_st, tex, l, mem_off(out, l * per)) { return false } }
return true
}
# level 0 of every layer written from bytes read back by gpu_tex_read_all; the texture's storage must
# already be what it was (same format, size and layers). Mips, where it has them, are the caller's
function gpu_tex_write_all(render3d_st: mut Render3dState, tex: int, data: pointer) -> bool {
let ifmt = render3d_st.gvk_tex_glfmt[tex]
let c = gvk_channels(ifmt)
let b = gvk_channel_bytes(ifmt)
var fmt = GL_RGBA
if c == 1 { fmt = GL_RED }
if c == 2 { fmt = GL_RG }
if c == 3 { fmt = GL_RGB }
var ty = GL_UNSIGNED_BYTE
if b == 2 { ty = GL_HALF_FLOAT }
if b == 4 { ty = GL_FLOAT }
return gvk_tex_upload(render3d_st, tex, ifmt, render3d_st.gvk_tex_dims_w[tex], render3d_st.gvk_tex_dims_h[tex], render3d_st.gvk_tex_layers[tex], fmt, ty, data)
}

View file

@ -14,6 +14,7 @@ import "vkmem_report.ludic"
import "gpu_vk_res.ludic" import "gpu_vk_res.ludic"
import "gpu_vk_layers.ludic" import "gpu_vk_layers.ludic"
import "gpu_vk_bc.ludic" import "gpu_vk_bc.ludic"
import "gpu_vk_readback.ludic"
import "texture_dds.ludic" import "texture_dds.ludic"
import "prof.ludic" import "prof.ludic"
import "drawstats.ludic" import "drawstats.ludic"
@ -51,4 +52,5 @@ import "grass_gpu.ludic"
import "water.ludic" import "water.ludic"
import "streamline.ludic" import "streamline.ludic"
import "render.ludic" import "render.ludic"
import "bake_parts.ludic"
import "safe_api.ludic" import "safe_api.ludic"

View file

@ -92,20 +92,31 @@ function sky_set_quality(render3d_st: mut Render3dState, w: int) -> void {
function sky_precompute(render3d_st: mut Render3dState) -> void { function sky_precompute(render3d_st: mut Render3dState) -> void {
gpu_depth_test(render3d_st, false) gpu_depth_test(render3d_st, false)
sky_ibl_make(render3d_st)
if render3d_st.sky_p_irr == 0 { render3d_st.sky_p_irr = r3d_program(render3d_st, "fullscreen.vert", "ibl_irradiance.frag", ""); render3d_st.sky_p_pre = r3d_program(render3d_st, "fullscreen.vert", "ibl_prefilter.frag", ""); render3d_st.sky_p_brdf = r3d_program(render3d_st, "fullscreen.vert", "ibl_brdf.frag", "") }
sky_convolve(render3d_st, render3d_st.sky_p_irr, render3d_st.sky_irradiance, -1, 128, 64, 0.0)
for l in 0 .. SKY_PREFILTER_LEVELS {
sky_convolve(render3d_st, render3d_st.sky_p_pre, render3d_st.sky_prefilter, l, render3d_st.sky_prefilter_w, render3d_st.sky_prefilter_w / 2, float(l) / float(SKY_PREFILTER_LEVELS - 1))
}
sky_convolve(render3d_st, render3d_st.sky_p_brdf, render3d_st.sky_brdf, -1, 256, 256, 0.0)
render3d_st.sky_baked = true
render3d_st.sky_baked_yaw = render3d_st.sky_yaw
gpu_check(render3d_st, "sky precompute")
}
# the IBL set's textures, made empty (the last ones let go): what sky_precompute convolves into, and
# what sky_ibl_from_bytes fills from a bake
function sky_ibl_make(render3d_st: mut Render3dState) -> void {
if render3d_st.sky_irradiance != 0 { if render3d_st.sky_irradiance != 0 {
gpu_tex_free(render3d_st, render3d_st.sky_irradiance) gpu_tex_free(render3d_st, render3d_st.sky_irradiance)
gpu_tex_free(render3d_st, render3d_st.sky_prefilter) gpu_tex_free(render3d_st, render3d_st.sky_prefilter)
gpu_tex_free(render3d_st, render3d_st.sky_brdf) gpu_tex_free(render3d_st, render3d_st.sky_brdf)
} }
# irradiance: 128 x 64 equirect # irradiance: 128 x 64 equirect
if render3d_st.sky_p_irr == 0 { render3d_st.sky_p_irr = r3d_program(render3d_st, "fullscreen.vert", "ibl_irradiance.frag", ""); render3d_st.sky_p_pre = r3d_program(render3d_st, "fullscreen.vert", "ibl_prefilter.frag", ""); render3d_st.sky_p_brdf = r3d_program(render3d_st, "fullscreen.vert", "ibl_brdf.frag", "") }
let p_irr = render3d_st.sky_p_irr
render3d_st.sky_irradiance = tex_target(render3d_st, 128, 64, GL_RGB16F, GL_RGB, GL_FLOAT, GL_LINEAR) render3d_st.sky_irradiance = tex_target(render3d_st, 128, 64, GL_RGB16F, GL_RGB, GL_FLOAT, GL_LINEAR)
gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.sky_irradiance) gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.sky_irradiance)
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT) gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
sky_convolve(render3d_st, p_irr, render3d_st.sky_irradiance, -1, 128, 64, 0.0)
# prefiltered specular: 6 roughness levels, sky_prefilter_w x half each, as a 2D array # prefiltered specular: 6 roughness levels, sky_prefilter_w x half each, as a 2D array
let p_pre = render3d_st.sky_p_pre
render3d_st.sky_prefilter = gpu_tex_new(render3d_st) render3d_st.sky_prefilter = gpu_tex_new(render3d_st)
gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, render3d_st.sky_prefilter) gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, render3d_st.sky_prefilter)
gpu_tex_image3d(render3d_st, GL_RGB16F, render3d_st.sky_prefilter_w, render3d_st.sky_prefilter_w / 2, SKY_PREFILTER_LEVELS, GL_RGB, GL_FLOAT, null) gpu_tex_image3d(render3d_st, GL_RGB16F, render3d_st.sky_prefilter_w, render3d_st.sky_prefilter_w / 2, SKY_PREFILTER_LEVELS, GL_RGB, GL_FLOAT, null)
@ -113,16 +124,8 @@ function sky_precompute(render3d_st: mut Render3dState) -> void {
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR) gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR) gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
for l in 0 .. SKY_PREFILTER_LEVELS {
sky_convolve(render3d_st, p_pre, render3d_st.sky_prefilter, l, render3d_st.sky_prefilter_w, render3d_st.sky_prefilter_w / 2, float(l) / float(SKY_PREFILTER_LEVELS - 1))
}
# BRDF LUT # BRDF LUT
let p_brdf = render3d_st.sky_p_brdf
render3d_st.sky_brdf = tex_target(render3d_st, 256, 256, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR) render3d_st.sky_brdf = tex_target(render3d_st, 256, 256, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR)
sky_convolve(render3d_st, p_brdf, render3d_st.sky_brdf, -1, 256, 256, 0.0)
render3d_st.sky_baked = true
render3d_st.sky_baked_yaw = render3d_st.sky_yaw
gpu_check(render3d_st, "sky precompute")
} }
# bind the IBL set + sun for a lit program (units 12..14) # bind the IBL set + sun for a lit program (units 12..14)