From 967f15ded976c23bff680433c2211c24a7b8980c Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 17:30:57 +0300 Subject: [PATCH] render3d: baked textures before the PNG - a png_decode takes assets/baked/png, a cut-out load assets/baked/cutouts baked_tex.ludic reads ludic.base's baked form by hand (render3d uses no package): the LBAK header, the key ("png_sheets" / "cutouts", the Bakes rows) and the generator version, then the payload - w, h, channels, depth and the samples for a PNG (the caller frees them as it would a decode), a whole .dds for a cut-out (tex_load_dds_at: a .dds at an offset). Missing, or another key or version, and the PNG path runs as before. Compiles with Maroon Lake (game, lab, lab/bake/textures). Co-Authored-By: Claude Opus 5.5 --- changes/baked-textures.md | 8 +++ packages/ludic.render3d/baked_tex.ludic | 75 +++++++++++++++++++++++ packages/ludic.render3d/r3d.ludic | 1 + packages/ludic.render3d/texture.ludic | 9 +++ packages/ludic.render3d/texture_dds.ludic | 20 +++--- 5 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 changes/baked-textures.md create mode 100644 packages/ludic.render3d/baked_tex.ludic diff --git a/changes/baked-textures.md b/changes/baked-textures.md new file mode 100644 index 00000000..7f7fcb4b --- /dev/null +++ b/changes/baked-textures.md @@ -0,0 +1,8 @@ +bump: minor +type: feature +**Textures a game baked at build time are read before the PNG.** `png_decode` first takes +`assets/baked/png/.tex` (a game's `ludic bake` output: the samples, ready to upload), and a cut-out +load (`tex_load_ex` with dilate) first takes `assets/baked/cutouts/.bc7` (padded as `tex_dilate` +pads, then BC7 with its mips) - so a boot decodes, pads and converts nothing it can take ready-made. Both +are ludic.base's baked form, read by hand (baked_tex.ludic: the "LBAK" header, the key and version); a +missing or stale one falls back to the PNG as before. `tex_load_dds_at` reads a .dds at an offset. diff --git a/packages/ludic.render3d/baked_tex.ludic b/packages/ludic.render3d/baked_tex.ludic new file mode 100644 index 00000000..e009731c --- /dev/null +++ b/packages/ludic.render3d/baked_tex.ludic @@ -0,0 +1,75 @@ +# baked_tex.ludic - textures a game baked at build time (`ludic bake`, assets/baked/), read before the PNG +# they came from: a boot decodes, pads and converts nothing it can take ready-made. The file is ludic.base's +# baked form (a 32-byte "LBAK" header, then the payload), read by hand here since render3d uses no package. +# Missing, or of another key or version, and the PNG is decoded as it always was. +# assets/baked/png/.tex a PNG's samples: u32 w, h, channels, depth, then them +# assets/baked/cutouts/.bc7 a cut-out atlas, padded as tex_dilate pads it: a .dds +const BAKED_PNG_KEY: string = "png_sheets" # the Bakes row (Maroon Lake's bakes.lres) and the key in its header +const BAKED_PNG_VERSION: int = 1 +const BAKED_CUT_KEY: string = "cutouts" +const BAKED_CUT_VERSION: int = 1 + +function baked_u32(b: []byte, o: int) -> int { return b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24) } + +# where the bake of `path` (a PNG under assets/) lives, or "" for any other path +@alloc_ok("a load: the baked file's name") +function baked_tex_path(path: string, dir: string, ext: string) -> string { + let n = len(path) + if n < 12 or path[0 .. 7] != "assets/" or path[n - 4 .. n] != ".png" { return "" } + return `assets/baked/{dir}/{path[7 .. n - 4]}{ext}` +} + +# the payload's offset in a baked file of this key and version, or -1 (not one, or another's) +function baked_payload(b: []byte, key: string, version: int) -> int { + if b == null or len(b) < 40 { return -1 } + if b[0] != 76 or b[1] != 66 or b[2] != 65 or b[3] != 75 { return -1 } # "LBAK" + if baked_u32(b, 4) != 1 or baked_u32(b, 8) != version { return -1 } + let off = baked_u32(b, 12) + if off < 32 + len(key) or off > len(b) { return -1 } + for i in 0 .. len(key) { if b[32 + i] != key[i] { return -1 } } + if b[32 + len(key)] != 0 { return -1 } + return off +} + +# a PNG's samples as the bake left them (tex_w .. tex_depth set, the caller frees them), or null +@alloc_ok("a load: the baked file read, its samples kept by the caller") +function baked_png(render3d_st: mut Render3dState, path: pointer) -> pointer { + let bp = baked_tex_path(path, "png", ".tex") + if bp == "" or not Fs.exists(bp) { return null } + let b = Fs.read_bytes(bp) + let off = baked_payload(b, BAKED_PNG_KEY, BAKED_PNG_VERSION) + if off < 0 or off + 16 > len(b) { + if b != null { free(b) } + print(`r3d: {bp} is not this build's bake; {path} is decoded`) + return null + } + let w = baked_u32(b, off) + let h = baked_u32(b, off + 4) + let ch = baked_u32(b, off + 8) + let depth = baked_u32(b, off + 12) + let n = w * h * ch * (depth / 8) + if w < 1 or h < 1 or ch < 1 or ch > 4 or (depth != 8 and depth != 16) or off + 16 + n > len(b) { + free(b) + return null + } + let out = bytes(n + 8) + mem_copy(out, mem_off(data_of(b), off + 16), n) + free(b) + render3d_st.tex_w = w; render3d_st.tex_h = h; render3d_st.tex_channels = ch; render3d_st.tex_depth = depth + return out +} + +# a cut-out atlas as the bake padded and compressed it (a texture id), or 0 +@alloc_ok("a load: the baked file read and let go") +function baked_cutout(render3d_st: mut Render3dState, path: pointer, srgb: bool) -> int { + if not render3d_st.gvk_has_bc { return 0 } + let bp = baked_tex_path(path, "cutouts", ".bc7") + if bp == "" or not Fs.exists(bp) { return 0 } + let b = Fs.read_bytes(bp) + let off = baked_payload(b, BAKED_CUT_KEY, BAKED_CUT_VERSION) + var id = 0 + if off >= 0 { id = tex_load_dds_at(render3d_st, b, off, srgb) } + if b != null { free(b) } + if id == 0 { print(`r3d: {bp} is not this build's bake; {path} is padded here`) } + return id +} diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index 07d98a96..9e799552 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -20,6 +20,7 @@ import "prof.ludic" import "drawstats.ludic" import "programs.ludic" import "texture.ludic" +import "baked_tex.ludic" import "mesh.ludic" import "gpu_vk_draw.ludic" import "camera.ludic" diff --git a/packages/ludic.render3d/texture.ludic b/packages/ludic.render3d/texture.ludic index 3ec299fd..e9a35c04 100644 --- a/packages/ludic.render3d/texture.ludic +++ b/packages/ludic.render3d/texture.ludic @@ -115,6 +115,8 @@ function png_unfilter(raw: pointer, cur: int, prev: int, stride: int, fbpp: int, @alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)") function png_decode(render3d_st: mut Render3dState, path: pointer) -> pointer { + let bk = baked_png(render3d_st, path) # a build that baked it (baked_tex.ludic) + if bk != null { return bk } let d = r3d_read_file(render3d_st, path) if d == null { print(`png: cannot read {path}`); return null } let size = render3d_st.tex_file_len @@ -310,6 +312,13 @@ function tex_load_ex__t(render3d_st: mut Render3dState, path: pointer, srgb: boo return t } } + if dilate > 0 { + let cut = baked_cutout(render3d_st, path, srgb) # padded and compressed at build time + if cut != 0 { + tex_note_size(render3d_st, cut) + return cut + } + } let px = png_decode(render3d_st, path) if px == null { return 0 } if dilate > 0 { tex_dilate(render3d_st, px, 60, dilate) } diff --git a/packages/ludic.render3d/texture_dds.ludic b/packages/ludic.render3d/texture_dds.ludic index 58a4b024..6321e33d 100644 --- a/packages/ludic.render3d/texture_dds.ludic +++ b/packages/ludic.render3d/texture_dds.ludic @@ -44,14 +44,16 @@ function tex_load_dds_bytes(render3d_st: mut Render3dState, b: []byte, srgb: boo render3d_st.gvk_tag = was return r } -function tex_load_dds_bytes__t(render3d_st: mut Render3dState, b: []byte, srgb: bool) -> int { - if len(b) < 148 or b[0] != 68 or b[1] != 68 or b[2] != 83 or b[3] != 32 { return 0 } - if b[84] != 68 or b[85] != 88 or b[86] != 49 or b[87] != 48 { return 0 } # "DX10" - let h = dds_u32(b, 12) - let w = dds_u32(b, 16) - var levels = dds_u32(b, 28) +function tex_load_dds_bytes__t(render3d_st: mut Render3dState, b: []byte, srgb: bool) -> int { return tex_load_dds_at(render3d_st, b, 0, srgb) } +# a .dds that starts `at` bytes into b (a baked file's payload, baked_tex.ludic) +function tex_load_dds_at(render3d_st: mut Render3dState, b: []byte, at0: int, srgb: bool) -> int { + if len(b) < at0 + 148 or b[at0] != 68 or b[at0 + 1] != 68 or b[at0 + 2] != 83 or b[at0 + 3] != 32 { return 0 } + if b[at0 + 84] != 68 or b[at0 + 85] != 88 or b[at0 + 86] != 49 or b[at0 + 87] != 48 { return 0 } # "DX10" + let h = dds_u32(b, at0 + 12) + let w = dds_u32(b, at0 + 16) + var levels = dds_u32(b, at0 + 28) if levels < 1 { levels = 1 } - let dxgi = dds_u32(b, 128) + let dxgi = dds_u32(b, at0 + 128) var ifmt = 0 var block = 16 if dxgi == DDS_BC7 or dxgi == DDS_BC7_SRGB { ifmt = R3D_BC7; if srgb { ifmt = R3D_BC7_SRGB } } @@ -65,13 +67,13 @@ function tex_load_dds_bytes__t(render3d_st: mut Render3dState, b: []byte, srgb: at += max((max(w >> l, 1) + 3) / 4, 1) * max((max(h >> l, 1) + 3) / 4, 1) * block } offs[levels] = at - if 148 + at > len(b) { + if at0 + 148 + at > len(b) { free(offs) return 0 } let id = gpu_tex_new(render3d_st) gpu_tex_bind(render3d_st, GPU_TEX2D, id) - let ok = gpu_tex_compressed(render3d_st, ifmt, w, h, levels, mem_off(data_of(b), 148), offs) + let ok = gpu_tex_compressed(render3d_st, ifmt, w, h, levels, mem_off(data_of(b), at0 + 148), offs) free(offs) if not ok { gpu_tex_free(render3d_st, id)