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>
116 lines
4.2 KiB
Python
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)
|