The first cut (LGD1) stored every tile of every layer raw, scale channel and all: 288 MB a map, which made Maroon's pack 803 MB. LGD2 keeps a tile index per layer (8 bytes a tile, read whole at the map's load): 0 all zero, 1 one value in the word, else an offset and a length, the top bit set when the bake deflated it (inflated into the tile cache with the runtime's z_inflate, a reused buffer; nothing per frame beyond the cache). A layer keeps its scale only where it varies, the density 6 bits and the scale 4 (the same integer arithmetic as the bake). The bake is maroon-lake's tools/bake/ground_density.py; the dev copy here writes the same format stored. Maroon 23.2 MB, Lamar 17.2 MB. The deflated flag is tested as a negative length: `b & 0x80000000` compiled to broken IR (an i32 `and` with a bare `-`), a compiler fault to fix separately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
4.9 KiB
Text
117 lines
4.9 KiB
Text
# ground_density_write.ludic — a map's painted ground densities (one PNG per layer: R the chance a cell keeps
|
|
# one, G an optional scale) cut into the terrain's 64 m tiles as LGD2, the format the build-time bake writes
|
|
# (maroon-lake's tools/bake/ground_density.py): an all-zero tile is nothing and a one-value tile a word, the
|
|
# scale is kept only where it varies, and the density is held to 6 bits and the scale to 4. This is a dev
|
|
# build's own copy, made when the bake is missing: the same bytes the bake decodes to, never deflated.
|
|
|
|
const GD_MAGIC: int = 843335500 # "LGD2" read as a little-endian u32: 0x3244474C
|
|
const GD_VERSION: int = 2
|
|
const GD_HEADW: int = 136 # words: magic, version, n, side, layers, 3 spare, (channels, index) x 64
|
|
const GD_LAYERS: int = 64
|
|
const GD_R_BITS: int = 6
|
|
const GD_G_BITS: int = 4
|
|
|
|
# v (0..255) held to `bits`, a byte again - the integer arithmetic the bake uses, so both agree to the bit
|
|
function gd_q(v: int, bits: int) -> int {
|
|
let top = (1 << bits) - 1
|
|
let q = (v * top + 127) / 255
|
|
return (q * 255 + top / 2) / top
|
|
}
|
|
|
|
# the layers' PNGs written to f from `base` (n tiles a side of `side` texels); the payload's length, or -1
|
|
@alloc_ok("a dev build's map load: the densities cut once")
|
|
function gd_write_payload(render3d_st: mut Render3dState, f: pointer, base: int, pngs: []string, n: int, side: int) -> int {
|
|
let layers = min(len(pngs), GD_LAYERS)
|
|
let head = words(GD_HEADW)
|
|
for k in 0 .. GD_HEADW { head[k] = 0 }
|
|
let idx = words(max(layers * n * n * 2, 1))
|
|
for k in 0 .. len(idx) { idx[k] = 0 }
|
|
let ib = GD_HEADW * 4
|
|
file_write(f, data_of(head), ib)
|
|
file_write(f, data_of(idx), layers * n * n * 8)
|
|
var at = ib + layers * n * n * 8
|
|
let tile = buffer(side * side * 2)
|
|
for l in 0 .. layers {
|
|
let px = png_decode_bytes(render3d_st, pngs[l], null)
|
|
if px == null or render3d_st.tex_w != n * side or render3d_st.tex_h != n * side { gd_say_bad(pngs[l], n * side); return -1 }
|
|
let c = render3d_st.tex_channels
|
|
var ch = 1
|
|
if c >= 2 and gd_g_varies(px, c, n * side) { ch = 2 }
|
|
head[8 + l * 2] = ch; head[9 + l * 2] = ib + l * n * n * 8
|
|
at = gd_write_layer(f, px, c, ch, n, side, tile, idx, l * n * n * 2, at)
|
|
free(px)
|
|
}
|
|
free(tile)
|
|
head[0] = GD_MAGIC; head[1] = GD_VERSION; head[2] = n; head[3] = side; head[4] = layers
|
|
file_seek(f, base, 0)
|
|
file_write(f, data_of(head), ib)
|
|
file_write(f, data_of(idx), layers * n * n * 8)
|
|
file_seek(f, base + at, 0)
|
|
free(head); free(idx)
|
|
return at
|
|
}
|
|
|
|
# does the PNG's G vary anywhere (else the layer keeps R alone)
|
|
function gd_g_varies(px: []byte, c: int, w: int) -> bool {
|
|
let g0 = px[1]
|
|
for i in 0 .. w * w { if px[i * c + 1] != g0 { return true } }
|
|
return false
|
|
}
|
|
|
|
# one layer's tiles: each quantized, then nothing (all zero), a word (one value) or its bytes; the next offset
|
|
function gd_write_layer(f: pointer, px: []byte, c: int, ch: int, n: int, side: int, tile: []byte, idx: words, i0: int, at0: int) -> int {
|
|
let w = n * side
|
|
var at = at0
|
|
for j in 0 .. n {
|
|
for i in 0 .. n {
|
|
var same = true
|
|
for r in 0 .. side {
|
|
for k in 0 .. side {
|
|
let o = ((j * side + r) * w + i * side + k) * c
|
|
let p = (r * side + k) * ch
|
|
tile[p] = gd_q(px[o], GD_R_BITS)
|
|
if ch == 2 { tile[p + 1] = gd_q(px[o + 1], GD_G_BITS) }
|
|
if tile[p] != tile[0] or (ch == 2 and tile[p + 1] != tile[1]) { same = false }
|
|
}
|
|
}
|
|
let e = i0 + (j * n + i) * 2
|
|
if same {
|
|
var g = 0
|
|
if ch == 2 { g = tile[1] }
|
|
if tile[0] != 0 or g != 0 { idx[e] = 1; idx[e + 1] = tile[0] | (g << 8) }
|
|
} else {
|
|
file_write(f, data_of(tile), side * side * ch)
|
|
idx[e] = at; idx[e + 1] = side * side * ch
|
|
at += side * side * ch
|
|
}
|
|
}
|
|
}
|
|
return at
|
|
}
|
|
|
|
# The bake's own file from a build without Python (ludic.base's LBAK header, as terrain_tiles_bake writes it):
|
|
# stored tiles, which the reader takes as it takes the deflated ones
|
|
@alloc_ok("a bake: once, at build time")
|
|
export function ground_density_bake(render3d_st: mut Render3dState, path: string, key: string, version: int, inputs_hash: long, pngs: []string) -> bool {
|
|
Fs.mkdir(Path.dir(path))
|
|
let f = file_open(path, "wb")
|
|
if f == null { return false }
|
|
let base = 32 + (len(key) + 1 + 7) / 8 * 8
|
|
let head = buffer(base)
|
|
for i in 0 .. base { head[i] = 0 }
|
|
file_write(f, data_of(head), base)
|
|
let n = gd_write_payload(render3d_st, f, base, pngs, TERRAIN_RES / TT_TEX, GD_SIDE)
|
|
let p = data_of(head)
|
|
Vk.put_i32(p, 0, BAKE_MAGIC); Vk.put_i32(p, 4, 1); Vk.put_i32(p, 8, version); Vk.put_i32(p, 12, base)
|
|
Vk.put_i64(p, 16, inputs_hash)
|
|
let nl: long = max(n, 0)
|
|
Vk.put_i64(p, 24, nl)
|
|
for i in 0 .. len(key) { head[32 + i] = key[i] }
|
|
file_seek(f, 0, 0)
|
|
file_write(f, data_of(head), base)
|
|
file_close(f)
|
|
free(head)
|
|
return n >= 0
|
|
}
|
|
|
|
function gd_say_bad(path: string, want: int) -> void { print(`r3d: ground: {path} is missing or not {want} x {want}; the densities are not written`) }
|