Files
TeslaRel410/dpl3-revive/parser/bsl.py
T
CydandClaude Fable 5 afc3fd839e Vendor dpl3-revive: the Division/DPL3 renderer, now ours
Bring the graphics-dev collaborator's dpl3-revive into the repo as first-class
project code (they've handed it off; it's ours now). This is the proven
Division renderer that our in-process rt_draw has been trying to be.

What's here:
- parser/  B2Z/V2Z/SVT/SCN/SPL/BGF/BMF/BSL decoders (pure Python).
- spec/    reverse-engineered format + the definitive VelociRender wire
           protocol (from the original DIVISION source, matches our live
           VPX node/action tables exactly).
- source-ref/  read-only copies of the original DIVISION C (BIZREAD.C,
           DPLTYPES.H, DPL.H) that define the formats.
- patha/   the "virtual VelociRender board": vrboard.py (24-action protocol
           server), vrview.py (numpy software rasterizer, the reference),
           vrview_gl.py (moderngl GPU backend, 832x512@60Hz), plus the
           run/replay/regress tooling and evidence renders. Drives FLYK/BLADE/
           Star Trek demos AND our btl4opt/rpl4opt game binaries.
- viewer/  WebGL archive generators (.py); prebuilt HTML/data regeneratable.
- samples/ small test models/textures.
- bt*.raw.bin  real BTL4OPT arena wire captures (kept for offline renderer
           testing/regression against OUR game).

.gitignore keeps the multi-hundred-MB demo capture dumps + debug logs +
regeneratable viewer bundles out of history (they stay on disk).

Phase 0 of the integration is validated: their board decodes our bt8 capture
with zero errors (3748 nodes, 507 instances, 4 mechs) and renders our arena
(terrain/dome/sky, correct Division DAC gamma). Plan + status in memory;
integration continues in emulator/RENDERER-COLLAB.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 22:06:25 -05:00

116 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""
bsl.py -- reader for DIV-BSL2 ".BSL" bit-slice texture packs.
A BSL packs several 4-bit mono textures ("slices" A..F) into the nibbles of one
32-bit texel map -- the formalised successor of GLOMM's /m: packing (GLOMM.C).
Materials reference a slice via `MAP {"name"} + BITSLICE {k}` in .VMF/.BMF
libraries; the material's DIFFUSE colours the mono plane.
Layout (reversed from STEH.BSL / F15_GLOM.BSL):
"DIV-BSL2" 8-byte magic
u32 version, u32 ? (0x100, 1)
u32 bits_per_slice (4)
u32 dir_bytes directory size hint
u32 n_slices
n x { u32 type, u32 slice_index, cstr name }
... texel words follow; data size = edge*edge*4, edge
inferred from remaining bytes (64/128/256), same
convention as .SVT
Slice k occupies a nibble of each 32-bit word; the exact nibble order was
determined empirically (see spec): slice k = bits [28-4k .. 31-4k].
python bsl.py info <file.bsl>
python bsl.py png <file.bsl> <slice|nibble:N> [out.png]
python bsl.py probe <file.bsl> # dump ALL 8 nibble planes for eyeballing
"""
import struct
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import svt # PNG writer
SIZE_TO_EDGE = {16384 * 4: 256, 65536: 128, 16384: 64, 262144: 256}
def load(path):
with open(path, "rb") as fp:
data = fp.read()
if data[:8] != b"DIV-BSL2":
raise ValueError("Not a DIV-BSL2 file")
pos = 8
version, one, bits, dirbytes, nslices = struct.unpack_from("<5i", data, pos)
pos += 20
entries = []
for _ in range(nslices):
typ, idx = struct.unpack_from("<2i", data, pos)
pos += 8
end = data.index(b"\0", pos)
name = data[pos:end].decode("latin-1")
pos = end + 1
entries.append({"name": name, "type": typ, "slice": idx})
body = data[pos:]
edge = {16384: 64, 65536: 128, 262144: 256}.get(len(body))
if edge is None:
# tolerate padding: use largest fitting square
for sz, e in ((262144, 256), (65536, 128), (16384, 64)):
if len(body) >= sz:
body = body[:sz]
edge = e
break
words = struct.unpack("<%dI" % (edge * edge), body)
return {"entries": entries, "edge": edge, "words": words,
"bits": bits, "version": version}
def nibble_plane(b, nib):
"""Extract nibble `nib` (0 = bits 0-3 ... 7 = bits 28-31) as 8-bit luminance."""
edge, words = b["edge"], b["words"]
shift = nib * 4
out = bytearray(edge * edge * 4)
for i, w in enumerate(words):
v = (w >> shift) & 0xF
L = v * 17 # 0..15 -> 0..255
o = i * 4
out[o] = out[o + 1] = out[o + 2] = L
out[o + 3] = 255
return bytes(out)
def slice_nibble(slice_index):
"""slice k -> nibble (k+2)^1.
Slices occupy bits 8..31 in ascending order, pair-swapped within each byte by
the writer's 32-bit byte reversal (as in GLOMM's save_svt); the low byte
(nibbles 0-1) is reserved for an 8-bit colour plane. Verified by correlating
all 12 source TGAs (STEH1-6, KLNG1-6) against every nibble plane: r >= 0.98
for this mapping on every slice of both packs."""
return (slice_index + 2) ^ 1
def slice_plane(b, slice_index):
return nibble_plane(b, slice_nibble(slice_index))
if __name__ == "__main__":
cmd, path = sys.argv[1], sys.argv[2]
b = load(path)
if cmd == "info":
print("edge %dx%d bits/slice=%d slices:" % (b["edge"], b["edge"], b["bits"]))
for e in b["entries"]:
print(" slice %d type=%d %s" % (e["slice"], e["type"], e["name"]))
elif cmd == "probe":
base = path.rsplit(".", 1)[0]
for nib in range(8):
out = "%s_nib%d.png" % (base, nib)
svt.write_png(out, b["edge"], b["edge"], nibble_plane(b, nib))
print("wrote", out)
elif cmd == "png":
sel = sys.argv[3]
nib = int(sel.split(":")[1]) if sel.startswith("nibble:") else slice_nibble(int(sel))
out = sys.argv[4] if len(sys.argv) > 4 else path.rsplit(".", 1)[0] + "_s%s.png" % sel
svt.write_png(out, b["edge"], b["edge"], nibble_plane(b, nib))
print("wrote", out)