diff --git a/dpl3-revive/.gitignore b/dpl3-revive/.gitignore new file mode 100644 index 0000000..63c4ad5 --- /dev/null +++ b/dpl3-revive/.gitignore @@ -0,0 +1,23 @@ +# dpl3-revive is now part of this repo (the friend's Division renderer, ours). +# Vendor the CODE + specs + parsers + source-ref + samples + Path-A board and +# tools + evidence PNGs. Keep the huge regeneratable debug bulk out of git +# (it stays on disk, but multi-hundred-MB wire dumps don't belong in history). + +__pycache__/ +*.pyc + +# Wire-capture debug logs (regeneratable text output) — do not track. +*.log + +# Wire captures: large demo/other-product dumps stay untracked; OUR BattleTech +# (game) captures ARE tracked for offline renderer testing / regression. +*.raw.bin +!bt*.raw.bin +!btnub*.raw.bin + +# Prebuilt / regeneratable viewer bulk. Rebuild with viewer/archive.py (data/) +# and viewer/bundle.py (dpl3-viewer.html). Keep the small archive.html shell + +# the .py generators tracked. +viewer/data/ +viewer/dpl3-viewer.html +viewer/vwe-archive.html diff --git a/dpl3-revive/README.md b/dpl3-revive/README.md new file mode 100644 index 0000000..d072f88 --- /dev/null +++ b/dpl3-revive/README.md @@ -0,0 +1,135 @@ +# dpl3-revive + +Porting the Virtual World Entertainment **DPL3** renderer (BattleTech / Red Planet +arcade pods, c. 1994–97) to modern hardware. The original ran on an Intel **i860** +graphics coprocessor driven by an x86 DOS host; the i860 and its PixelPlanes-5-style +board are long gone, so the plan is a **reimplementation** on a modern GPU, not a +recompile. + +The source drive is treated as a **read-only archive**. Nothing here writes back to +it; the reference source and sample models below were *copied out* of it. + +## Status + +**Milestones 1 & 2a — asset formats decoded.** The `.B2Z` / `.V2Z` binary geometry +format and the `.SVT` texture format are both fully reverse-engineered, with +working, validated parsers. Models export to OBJ; textures export to PNG. The art +is now recoverable independent of the dead engine. + +Roadmap: +1. ✅ **B2Z/V2Z geometry format** — spec + parser + OBJ export. +2a. ✅ **SVT texture format** — spec + parser + PNG export (`0BGR`, size-inferred dims). +3. ✅ **GPU viewer** — self-contained WebGL page renders textured models on modern + hardware (`viewer/`). Proves the whole art pipeline end-to-end. +2b. ✅ **SCN scene format** — plain-text scenes parsed + assembled into world space + (models placed via the exact DPL transform), textured, fogged, and rendered. + `CANAL.SCN` (47 objects) explorable in the viewer. +4. ✅ **SPL camera splines** — cubic-Hermite fly-through paths evaluated and animated. + `RAPTOR.SCN` + `CAMERA.SPL` play back the original 1994 flyover in the viewer. +4b. ✅ **Scene gallery** — the viewer now hosts several recovered worlds (Raptor + flyover, Red Planet canal, underwater reef). Fish are textured by the + **model-name** convention (`shark` → `SHARK.SVT`) when the material carries none. +4c. ✅ **Historical archive** — every resolvable scene (~60) **and every binary model + (~264)** batch-built and browsable in a searchable, lazy-loading viewer + (`archive.html` + `serve.py`) with Scenes/Models tabs. Camera has mouse orbit + + **WASD fly movement** (Q/E for down/up). A large array to judge render/colour. +5. ✅ **Material libraries + output gamma** — authoritative per-material colour+texture + from each model's `.V2Z` (`DIFFUSE × texture`), detail textures as luminance, and + the Division card's 10-bit-DAC **gamma-1.7** curve (`GAMMA.C`) for the authentic + pastel look. Red rock / grey concrete columns / red pipes now match the original. +5b. ✅ **Texture V orientation** — the original renderer samples v=0 at SVT row 0 + (no flip). Fixed a wrong global V-flip that inverted mech cockpits/pods and fish + shading. Also decoded GLOMM bit-plane texture packs (`*ALL.SVT`) — see + `spec/SVT_FORMAT.md`. +5d. ✅ **Game maps: MAP / MOD** — the real playable worlds (BattleTech arenas, Red + Planet tracks) from the game trees' `MAPS/` dirs: INI-style maps placing `.MOD` + game objects (→ BGF video geometry) with nesting, includes, and pod dropzones + (drawn as red markers). Also fixed the 1996 **LOD-container** BGF layout, which + unlocked ~850 game models that previously parsed empty. Archive: **142 scenes + + 41 maps + 1,462 models**, with a Maps tab in the viewer. +5c. ✅ **Game-side formats: BGF / BMF / VGF / VMF / BSL** — the game pipeline turns + out to be the same BIZ2/VIZ2 family: `.BGF` geometry, `.BMF`/`.VMF` material + libraries (namespaced refs like `stships:eblu4_mtl`), plus `DIV-BSL2` bit-slice + texture packs (texture tag `0x018` = BITSLICE). This unlocked **Star Trek** (the + "Tesla" Galaxy-class starship + Klingon cruiser, hull textures from `STEH.BSL`) + and the BattleTech / Red Planet / Hull Pressure game content. Archive now holds + **142 scenes + 608 models** across all product lines. +6. ⬜ Port the DPL3 geometry pipeline (transform / light / cull / LOD) from the C. +7. ⬜ Replace the PXPL5 rasterizer backend with the GPU. + +### Path A — run the original binaries unmodified (alternative track) + +The full original DPL3 engine source is on the archive drive (`sda4/DPL3/`), both the +DOS host side and the i860 board side. That means the **VelociRender wire protocol** the +host uses to drive the (dead) i860 board is fully documented in C — no disassembly. Path +A stands up a *virtual VelociRender board*: emulate the INMOS C012 link adapter (six I/O +ports at `0x150`) as a **DOSBox-X device**, answer the 24-command protocol, and use the +renderer this project already built as the board's guts — so `FLYK.EXE` and the other +HPDAVE/DPL3 binaries run **unmodified**, renderer transparently swapped underneath. +Spec + architecture: `spec/VELOCIRENDER_PROTOCOL.md`. + +## Layout + +``` +spec/ format documentation + B2Z_FORMAT.md the reverse-engineered DIV-BIZ2 geometry spec + SVT_FORMAT.md the reverse-engineered SVT texture spec + SCN_FORMAT.md the reverse-engineered SCN scene-format spec + SPL_FORMAT.md the reverse-engineered SPL camera-spline spec +parser/ the decoders (pure Python 3, no deps) + b2z.py geometry reader + triangulating OBJ exporter + svt.py texture reader + PNG exporter + scn.py scene-file reader + the DPL instance-transform math + spl.py camera-spline evaluator (cubic Hermite) -> camera frames +samples/ small test models/textures from the archive (+ exported .obj/.png) +source-ref/ read-only copies of the original DIVISION source that define the format +viewer/ the WebGL viewers + bundle.py packs a few curated scenes into one self-contained HTML + dpl3-viewer.html the curated gallery (open in any browser; shareable) + archive.py batch-builds EVERY resolvable scene -> data/*.json + archive.html the historical archive viewer (searchable, lazy-loads scenes) + serve.py local launcher for the archive (needed for fetch()) + data/ one JSON per scene + manifest.json (~60 scenes) + render_preview.py CPU rasterizer used to verify the pipeline + preview_*.png verification renders +``` + +## Usage + +```sh +python parser/b2z.py dump samples/TESTBOX.B2Z # structural dump +python parser/b2z.py obj samples/BALL.B2Z out.obj # export triangulated OBJ +python parser/svt.py info samples/SHARK.SVT # texture dimensions/stats +python parser/svt.py png samples/SHARK.SVT out.png # export texture to PNG +python parser/scn.py "/DPL3RLS/SCENES/CANAL.SCN" # dump a scene's contents +python viewer/bundle.py # (re)build viewer/dpl3-viewer.html +``` + +Then open `viewer/dpl3-viewer.html` in any browser — drag to orbit, scroll to zoom, +switch models. It's fully self-contained (geometry inlined, textures as base64). + +### The historical archive (all ~60 scenes) + +```sh +python viewer/archive.py # (re)build data/*.json for every resolvable scene +python viewer/serve.py # serve locally + open the archive viewer +``` + +`serve.py` opens `http://localhost:8765/archive.html` — **Scenes** and **Models** +tabs, each a searchable list **grouped by product** (Red Planet, BattleTech, Hull +Pressure, Star Trek, Milestone, Engine/Demo). Camera: **drag** to look, **scroll** +to zoom, **WASD** to fly, **Q/E** down/up. A live **gamma slider** (default 1.25) +tunes the Division-DAC brightness. A local server is required because browsers +block `fetch()` of local files over `file://`. + +Rebuild subsets with `python viewer/archive.py --scenes-only` / `--models-only`; +re-tag products (no data rebuild) with `--reclassify`. + +The exported `.obj` files open in any modern 3D tool (Blender, MeshLab, etc.); +the `.png` textures open anywhere. + +## Provenance + +Format decoded from `source-ref/BIZREAD.C` — "Copyright DIVISION Limited (c) 1994, +author PJA" (Phil Atkin). Data model in `source-ref/DPLTYPES.H`. See +`spec/B2Z_FORMAT.md` §10 for the validation evidence. diff --git a/dpl3-revive/parser/b2z.py b/dpl3-revive/parser/b2z.py new file mode 100644 index 0000000..651519f --- /dev/null +++ b/dpl3-revive/parser/b2z.py @@ -0,0 +1,523 @@ +#!/usr/bin/env python3 +""" +b2z.py -- reader for DIVISION / Virtual World Entertainment "DIV-BIZ2" (.B2Z / .V2Z) +binary geometry objects, used by the DPL3 renderer (c. 1994). + +Faithful transcription of DPL3/BIZREAD.C (Copyright DIVISION Ltd 1994, author PJA). +The original read the file in 8 KB blocks with endian correction; here we load the +whole file into memory and walk an absolute cursor -- identical results, less code. + +Little-endian throughout (the files were authored on x86 / i860 hosts). + +Usage: + python b2z.py dump # structural dump + python b2z.py obj [out.obj] # export triangulated Wavefront OBJ +""" +import struct +import sys +from dataclasses import dataclass, field + + +# ---------------------------------------------------------------------------- +# low-level stream (mirrors get_char / get_int* / get_float32 / get_struct32) +# ---------------------------------------------------------------------------- +class Stream: + def __init__(self, data: bytes): + self.d = data + self.pos = 0 + + def eof(self) -> bool: + return self.pos >= len(self.d) + + def u8(self) -> int: + v = self.d[self.pos] + self.pos += 1 + return v + + def i8(self) -> int: + v = self.u8() + return v - 256 if v >= 128 else v + + def u16(self) -> int: + v = struct.unpack_from(" int: + v = struct.unpack_from(" float: + v = struct.unpack_from(" str: + """null-terminated string (the `while (c=get_char())` idiom).""" + start = self.pos + while self.d[self.pos] != 0: + self.pos += 1 + s = self.d[start:self.pos].decode("latin-1") + self.pos += 1 # skip the NUL + return s + + def fixed_str(self, n: int) -> str: + s = self.d[self.pos:self.pos + n].decode("latin-1") + self.pos += n + return s + + def skip(self, n: int): + self.pos += n + + +# ---------------------------------------------------------------------------- +# block header: 16-bit type; top nibble selects the width of the length field. +# (hdr >> 12) & 0xc == 0x8 -> int32 len, 0x4 -> int16 len, 0x0 -> int8 len +# The record TYPE is (hdr & 0xfff); the top nibble is only a length-encoding flag, +# which is why every record type appears as 0x0NNN / 0x4NNN / 0x8NNN in the source. +# ---------------------------------------------------------------------------- +def read_block(s: Stream): + hdr = s.u16() + sel = (hdr >> 12) & 0xc + if sel == 0x8: + length = s.i32() & 0xffffffff + elif sel == 0x4: + length = s.u16() + elif sel == 0x0: + length = s.u8() + else: + raise ValueError("Unrecognised block header 0x%04x" % hdr) + return hdr, length + + +# ---------------------------------------------------------------------------- +# data model (subset of dpltypes.h that a .b2z can express) +# ---------------------------------------------------------------------------- +@dataclass +class Vertex: + pos: tuple # (x, y, z) + normal: tuple = None # (nx, ny, nz) + rgba: tuple = None # (r, g, b, a) + lum: tuple = None # (l, a) + uv: tuple = None # (u, v) or (u, v, w) + + +# geometry primitive types (dpl_geo_type) +TRISTRIP, POLYSTRIP, PMESH, POLYGON = "tristrip", "polystrip", "pmesh", "polygon" + + +@dataclass +class Geometry: + geotype: str + verts: list = field(default_factory=list) + conns: list = field(default_factory=list) # explicit faces (pmesh); each = list[int] + + +@dataclass +class Geogroup: + name: str = "" + draw_mode: int = 0 + f_material: str = None + b_material: str = None + geoms: list = field(default_factory=list) + + +@dataclass +class Material: + name: str + ambient: tuple = (1, 1, 1) + diffuse: tuple = (1, 1, 1) + specular: tuple = (0, 0, 0, 0) + emissive: tuple = (1, 1, 1) + opacity: tuple = (1, 1, 1) + texture: str = None + ramp: str = None + + +@dataclass +class Texture: + name: str + mapfile: str = None + minify: int = 0 + magnify: int = 0 + alpha: int = 0 + wrap_u: int = 0 + wrap_v: int = 0 + bitslice: int = 0 # tag 0x018 (1996+): 4-bit plane index into a .BSL pack + special: str = None # tag 0x037: free-text hook, e.g. " SCROLL u0 v0 du dv" + + +@dataclass +class Model: + object_name: str = "" + scale: float = 1.0 + units: float = 1.0 + geogroups: list = field(default_factory=list) + materials: dict = field(default_factory=dict) + textures: dict = field(default_factory=dict) + ramps: dict = field(default_factory=dict) + + +# vertex-format table: header type (12-bit) -> (floats_per_vertex, offsets dict) +# offsets are indices into the per-vertex float array. Mirrors parse_vertices(). +VFMT = { + 0x080: (3, {}), # flat x y z + 0x081: (6, {"n": 3}), # + normal + 0x082: (7, {"c": 3}), # + rgba (cooked) + 0x083: (10, {"n": 3, "c": 6}), # normal + rgba + 0x084: (5, {"l": 3}), # + luminance/alpha + 0x085: (8, {"n": 3, "l": 6}), # normal + la + 0x088: (5, {"t2": 3}), # + 2d texture uv + 0x089: (8, {"n": 3, "t2": 6}), # normal + uv + 0x08a: (9, {"c": 3, "t2": 7}), # rgba + uv + 0x08c: (7, {"l": 3, "t2": 5}), # la + uv + 0x090: (6, {"t3": 3}), # + 3d texture uvw + 0x091: (9, {"n": 3, "t3": 6}), # normal + uvw + 0x092: (10, {"c": 3, "t3": 7}), # rgba + uvw + 0x094: (8, {"l": 3, "t3": 5}), # la + uvw +} + +PRIM = {0x043: POLYGON, 0x044: TRISTRIP, 0x045: POLYSTRIP, 0x046: PMESH} + + +class Reader: + def __init__(self, data: bytes): + self.s = Stream(data) + self.m = Model() + + # -- header ------------------------------------------------------------ + def parse_header(self, length): + end = self.s.pos + length + while self.s.pos < end: + hdr, ln = read_block(self.s) + body = self.s.pos + t = hdr & 0xfff + if t == 0x005: # scale (0x2005) + self.m.scale = self.s.f32() + elif t == 0x006: # precision (0x2006): 0 = single precision + if self.s.u8() != 0: + raise ValueError("double-precision files unsupported (as in original)") + elif t == 0x009: # unit (0x2009) + u = self.s.u8() + self.m.units = 1.0 if u == 0 else (1.0 / 25.4) + # version/date/time/filetype -> skipped + self.s.pos = body + ln + + # -- vertices ---------------------------------------------------------- + def read_vertices(self, fpv, offs, n): + out = [] + for _ in range(n): + f = self.s.floats(fpv) + v = Vertex(pos=(f[0], f[1], f[2])) + if "n" in offs: + o = offs["n"]; v.normal = (f[o], f[o+1], f[o+2]) + if "c" in offs: + o = offs["c"]; v.rgba = (f[o], f[o+1], f[o+2], f[o+3]) + if "l" in offs: + o = offs["l"]; v.lum = (f[o], f[o+1]) + if "t2" in offs: + o = offs["t2"]; v.uv = (f[o], f[o+1]) + if "t3" in offs: + o = offs["t3"]; v.uv = (f[o], f[o+1], f[o+2]) + out.append(v) + return out + + def parse_vertices(self, length, geotype, group): + end = self.s.pos + length + geom = Geometry(geotype) + pre_conns = [] # pmesh connectivity accumulates across sub-blocks + while self.s.pos < end: + hdr, ln = read_block(self.s) + body = self.s.pos + t = hdr & 0xfff + nfloats = ln >> 2 + if t == 0x047: # pmesh triangle connectivity + for _ in range(ln // 12): + pre_conns.append([self.s.i32(), self.s.i32(), self.s.i32()]) + elif t == 0x04d: # pmesh polygon connectivity + vpp = self.s.u8() + for _ in range((ln - 1) // (vpp * 4)): + pre_conns.append([self.s.i32() for _ in range(vpp)]) + elif t in VFMT: # a vertex block + fpv, offs = VFMT[t] + geom.verts = self.read_vertices(fpv, offs, nfloats // fpv) + # sphere/line/text (0x048/0x04a/0x04b) not implemented in original + self.s.pos = body + ln + if geotype == PMESH: + geom.conns = pre_conns + group.geoms.append(geom) + + # -- patch (geogroup) -------------------------------------------------- + def parse_patch(self, length, draw_mode): + end = self.s.pos + length + g = Geogroup(draw_mode=draw_mode) + while self.s.pos < end: + hdr, ln = read_block(self.s) + body = self.s.pos + t = hdr & 0xfff + if t == 0x008: # name (0x2008) + g.name = self.s.cstr() + elif t == 0x030: # front material (0x2030) + g.f_material = self._material_ref() + elif t == 0x031: # back material (0x2031) + g.b_material = self._material_ref(back=True, front=g.f_material) + elif t == 0x036: # vertex format / draw mode + g.draw_mode = self.s.u8() + elif t in PRIM: # a geometry primitive + self.s.pos = body # parse_vertices re-reads from body + # length already known (ln); rewind not needed -- body is content start + self.parse_vertices(ln, PRIM[t], g) + # plane/decal/facet/voodoo/sphere/line/text -> skipped + self.s.pos = body + ln + self.m.geogroups.append(g) + + def _material_ref(self, back=False, front=None): + typ = self.s.u8() + if typ == 0: + return None + if typ == 2: + return "DEFAULT" + if typ == 3 and back: + return front + if typ == 1: + return self.s.cstr() + return None + + # -- object ------------------------------------------------------------ + def parse_object(self, length): + self._lod_taken = False + self._object_body(self.s.pos + length, 0) + + def _object_body(self, end, draw_mode, in_lod=False): + while self.s.pos < end: + hdr, ln = read_block(self.s) + body = self.s.pos + t = hdr & 0xfff + if t == 0x008: # object name + self.m.object_name = self.s.cstr() + elif t == 0x036: # vertex format -> draw_mode + draw_mode = self.s.u8() + elif t == 0x042: # patch + self.s.pos = body + self.parse_patch(ln, draw_mode) + elif t == 0x041 and not in_lod: + # LOD. In the 1994 format its payload was empty (a marker the + # original loader skipped); the 1996 game format nests the + # patches INSIDE it, with 0x046(len 8) = switch in/out floats. + # Parse only the FIRST (highest-detail) LOD -- later ones are + # lower-poly alternates and would double-draw. + if not self._lod_taken: + self._lod_taken = True + self._object_body(body + ln, draw_mode, in_lod=True) + # materials-on-object / comments -> skipped + self.s.pos = body + ln + + # -- texture / material / ramp ---------------------------------------- + def parse_texture(self, length): + end = self.s.pos + length + tx = Texture(name="") + while self.s.pos < end: + hdr, ln = read_block(self.s) + body = self.s.pos + t = hdr & 0xfff + if t == 0x008: + tx.name = self.s.cstr() + elif t == 0x011: + tx.mapfile = self.s.cstr() + elif t == 0x012: + tx.minify = self.s.u8() + elif t == 0x013: + tx.magnify = self.s.u8() + elif t == 0x014: + tx.alpha = self.s.u8() + elif t == 0x015: + tx.wrap_u = self.s.u8() + elif t == 0x016: + tx.wrap_v = self.s.u8() + elif t == 0x018: + tx.bitslice = self.s.u8() + elif t == 0x037: + tx.special = self.s.cstr() + self.s.pos = body + ln + self.m.textures[tx.name] = tx + + def parse_material(self, length): + end = self.s.pos + length + mt = Material(name="") + while self.s.pos < end: + hdr, ln = read_block(self.s) + body = self.s.pos + t = hdr & 0xfff + if t == 0x008: + mt.name = self.s.cstr() + elif t == 0x021: # texture ref + mode = self.s.u8() + if mode == 2: + mt.texture = self.s.cstr() + elif t == 0x023: + mt.ambient = tuple(self.s.floats(3)) + elif t == 0x024: + mt.diffuse = tuple(self.s.floats(3)) + elif t == 0x025: + mt.specular = tuple(self.s.floats(4)) + elif t == 0x026: + mt.emissive = tuple(self.s.floats(3)) + elif t == 0x027: + mt.opacity = tuple(self.s.floats(3)) + elif t == 0x028: + mt.ramp = self.s.cstr() + self.s.pos = body + ln + self.m.materials[mt.name] = mt + + def parse_ramp(self, length): + end = self.s.pos + length + name, c0, c1 = "", (0, 0, 0), (0, 0, 0) + while self.s.pos < end: + hdr, ln = read_block(self.s) + body = self.s.pos + t = hdr & 0xfff + if t == 0x008: + name = self.s.cstr() + elif t == 0x031: + c0 = tuple(self.s.floats(3)) + c1 = tuple(self.s.floats(3)) + self.s.pos = body + ln + self.m.ramps[name] = (c0, c1) + + # -- top level --------------------------------------------------------- + def read(self): + magic = self.s.fixed_str(8) + if magic != "DIV-BIZ2": + raise ValueError("Not a DIV-BIZ2 file (magic=%r)" % magic) + while not self.s.eof(): + hdr, length = read_block(self.s) + t = hdr & 0xfff + if t == 0x005: # trailer + break + elif t == 0x003: # header + self.parse_header(length) + elif t == 0x010: # texture + self.parse_texture(length) + elif t == 0x020: # material + self.parse_material(length) + elif t == 0x030: # ramp + self.parse_ramp(length) + elif t == 0x040: # object + self.parse_object(length) + elif t == 0x004: # comment + self.s.skip(length) + else: + self.s.skip(length) + return self.m + + +def load(path: str) -> Model: + with open(path, "rb") as fp: + return Reader(fp.read()).read() + + +# ---------------------------------------------------------------------------- +# triangulation for export +# ---------------------------------------------------------------------------- +def triangles(geom: Geometry): + """Yield (i0, i1, i2) index triples in the geometry's own vertex space.""" + n = len(geom.verts) + if geom.geotype == TRISTRIP: + for i in range(n - 2): + yield (i, i + 1, i + 2) if i % 2 == 0 else (i + 1, i, i + 2) + elif geom.geotype == PMESH: + for face in geom.conns: + for k in range(1, len(face) - 1): + yield (face[0], face[k], face[k + 1]) + elif geom.geotype in (POLYGON, POLYSTRIP): + # a strip of polygons stored as a fan of the block's vertices + for k in range(1, n - 1): + yield (0, k, k + 1) + + +def export_obj(model: Model, out) -> tuple: + base = 1 # OBJ indices are 1-based + nv = nf = 0 + out.write("# exported from DIV-BIZ2 by b2z.py\n") + out.write("# object: %s\n" % model.object_name) + for gi, g in enumerate(model.geogroups): + out.write("g %s\n" % (g.name or ("geogroup_%d" % gi))) + for geom in g.geoms: + for v in geom.verts: + out.write("v %.6g %.6g %.6g\n" % v.pos) + has_uv = any(v.uv for v in geom.verts) + if has_uv: + for v in geom.verts: + u, w = (v.uv[0], v.uv[1]) if v.uv else (0.0, 0.0) + out.write("vt %.6g %.6g\n" % (u, w)) + for (a, b, c) in triangles(geom): + if has_uv: + out.write("f %d/%d %d/%d %d/%d\n" % + (base+a, base+a, base+b, base+b, base+c, base+c)) + else: + out.write("f %d %d %d\n" % (base+a, base+b, base+c)) + nf += 1 + base += len(geom.verts) + nv += len(geom.verts) + return nv, nf + + +# ---------------------------------------------------------------------------- +# CLI +# ---------------------------------------------------------------------------- +def cmd_dump(path): + m = load(path) + print("file :", path) + print("object :", m.object_name or "(unnamed)") + print("scale/units: %g / %g" % (m.scale, m.units)) + print("materials : %d %s" % (len(m.materials), list(m.materials))) + print("textures : %d %s" % (len(m.textures), list(m.textures))) + print("ramps : %d %s" % (len(m.ramps), list(m.ramps))) + print("geogroups : %d" % len(m.geogroups)) + tot_v = tot_t = 0 + for gi, g in enumerate(m.geogroups): + print(" [%d] name=%r draw_mode=0x%x f_mtl=%r b_mtl=%r geoms=%d" + % (gi, g.name, g.draw_mode, g.f_material, g.b_material, len(g.geoms))) + for geom in g.geoms: + tris = list(triangles(geom)) + tot_v += len(geom.verts); tot_t += len(tris) + attrs = [] + if geom.verts: + v0 = geom.verts[0] + for a in ("normal", "rgba", "lum", "uv"): + if getattr(v0, a) is not None: + attrs.append(a) + print(" - %-9s verts=%-4d faces=%-4d tris=%-4d attrs=%s" + % (geom.geotype, len(geom.verts), len(geom.conns), len(tris), + ",".join(attrs) or "pos")) + print("TOTAL : %d verts, %d triangles" % (tot_v, tot_t)) + for name, mt in m.materials.items(): + print(" material %-16s diffuse=%s tex=%s" % + (name, tuple(round(x, 3) for x in mt.diffuse), mt.texture)) + + +def cmd_obj(path, outpath=None): + m = load(path) + outpath = outpath or (path.rsplit(".", 1)[0] + ".obj") + with open(outpath, "w") as fp: + nv, nf = export_obj(m, fp) + print("wrote %s : %d vertices, %d triangles" % (outpath, nv, nf)) + + +if __name__ == "__main__": + if len(sys.argv) < 3: + print(__doc__) + sys.exit(1) + cmd, path = sys.argv[1], sys.argv[2] + if cmd == "dump": + cmd_dump(path) + elif cmd == "obj": + cmd_obj(path, sys.argv[3] if len(sys.argv) > 3 else None) + else: + print("unknown command", cmd); sys.exit(1) diff --git a/dpl3-revive/parser/bsl.py b/dpl3-revive/parser/bsl.py new file mode 100644 index 0000000..210845a --- /dev/null +++ b/dpl3-revive/parser/bsl.py @@ -0,0 +1,115 @@ +#!/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 + python bsl.py png [out.png] + python bsl.py probe # 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) diff --git a/dpl3-revive/parser/gamemap.py b/dpl3-revive/parser/gamemap.py new file mode 100644 index 0000000..c58010a --- /dev/null +++ b/dpl3-revive/parser/gamemap.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +""" +gamemap.py -- reader for the game-side ".MAP" arena/track format and its ".MOD" +game-object definitions (BTDAVE / BTLIVE / BTRAVINE / RPDAVE / RPLIVE MAPS dirs). + +A .MAP is INI-style text: + + [hall2] ; declare an asset section + type=model + modelfile=hall2.mod + [one] + type=dropzone ; pod spawn ring + dropzone= x y z rx ry rz ; rotations in RADIANS + [arena1] ; the map itself + type=map + instance=hall2 x y z rx ry rz + instancedropzone=one x y z rx ry rz + [inc] + type=include ; pull in another .MAP's sections + file=other.map + +A .MOD is the game object: `[video] object=X.bgf` (render geometry, DIV-BIZ2), +`[collision] name=X.sld`, `[gamedata]` physics/damage/animations. We only need +the video geometry. + +expand(path) resolves everything to a flat list of world-space placements: + [(bgf_name, matrix4x4), ...], plus dropzone positions. +Maps may instance other maps; expansion is recursive with matrix composition +(row-vector convention, as everywhere in DPL -- see SCN_FORMAT.md). +""" +import math +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import scn + + +def _parse_ini(path): + """-> ordered {section: {'type':..,'modelfile':..,'file':.., + 'instances':[(name,[6f])], 'dropzones':[[6f]], + 'instancedropzones':[(name,[6f])]}}""" + sections = {} + cur = None + try: + lines = open(path, encoding="latin-1").read().splitlines() + except OSError: + return sections + for raw in lines: + line = raw.split("//")[0].strip() + if not line: + continue + if line.startswith("[") and line.endswith("]"): + name = line[1:-1].strip().lower() + cur = sections.setdefault(name, {"type": None, "instances": [], + "dropzones": [], "instancedropzones": []}) + continue + if cur is None or "=" not in line: + continue + key, _, val = line.partition("=") + key = key.strip().lower() + val = val.strip() + if key == "type": + cur["type"] = val.lower() + elif key == "instance": + p = val.split() + cur["instances"].append((p[0].lower(), [float(x) for x in p[1:7]])) + elif key == "instancedropzone": + p = val.split() + cur["instancedropzones"].append((p[0].lower(), [float(x) for x in p[1:7]])) + elif key == "dropzone": + cur["dropzones"].append([float(x) for x in val.split()[:6]]) + else: + cur[key] = val.lower() # generic key (modelfile, MotionExtent, ...) + return sections + + +def load_sections(path, _seen=None): + """Parse a .MAP plus its type=include files (same directory).""" + _seen = _seen or set() + key = os.path.normcase(os.path.abspath(path)) + if key in _seen: + return {} + _seen.add(key) + sections = _parse_ini(path) + merged = dict(sections) + for name, sec in sections.items(): + if sec.get("type") == "include" and sec.get("file"): + inc = os.path.join(os.path.dirname(path), sec["file"]) + for n, s in load_sections(inc, _seen).items(): + merged.setdefault(n, s) + return merged + + +def _matrix(vals): + """instance 6-tuple: x y z rx ry rz (RADIANS) -> row-vector matrix, + same composition as the SCN STATIC transform (unit scale).""" + x, y, z, rx, ry, rz = (vals + [0.0] * 6)[:6] + return scn.instance_matrix(1.0, x, y, z, + math.degrees(rx), math.degrees(ry), math.degrees(rz)) + + +def pick_root(sections, path): + """The map section to expand: prefer one named like the file, else the + map-typed section with the most instances.""" + stem = os.path.splitext(os.path.basename(path))[0].lower() + maps = {n: s for n, s in sections.items() if s.get("type") in ("map",)} + if stem in maps: + return stem + if maps: + return max(maps, key=lambda n: len(maps[n]["instances"])) + return None + + +def expand(path, mod_resolver, max_depth=6): + """Flatten a .MAP to world space. + mod_resolver(modfile_basename) -> bgf_name or None (looks up the .MOD's + [video] object=). Returns (placements, dropzones): + placements = [(bgf_name, matrix)] + dropzones = [[x,y,z,rx,ry,rz]] (world space)""" + sections = load_sections(path) + root = pick_root(sections, path) + placements, dropzones = [], [] + if root is None: + return placements, dropzones + + def emit(sec_name, parent_mtx, depth): + sec = sections.get(sec_name) + if sec is None or depth > max_depth: + return + t = sec.get("type") + if t == "model": + # mod_resolver -> list of (bgf_name, anim_or_None); a MOD may carry + # several video objects (door frame + sliding leaves, laser drill...) + for item in (mod_resolver(sec.get("modelfile", "")) or []): + placements.append((item, parent_mtx)) + elif t == "map": + for name, vals in sec["instances"]: + emit(name, scn.matmul(_matrix(vals), parent_mtx), depth + 1) + for name, vals in sec["instancedropzones"]: + dz = sections.get(name) + if dz: + m = scn.matmul(_matrix(vals), parent_mtx) + for d in dz["dropzones"]: + wx, wy, wz = scn.xform_point(m, d[0], d[1], d[2]) + dropzones.append([wx, wy, wz, d[3], d[4], d[5]]) + elif t == "dropzone": + for d in sec["dropzones"]: + wx, wy, wz = scn.xform_point(parent_mtx, d[0], d[1], d[2]) + dropzones.append([wx, wy, wz, d[3], d[4], d[5]]) + + emit(root, scn.ident(), 0) + return placements, dropzones + + +def parse_mod_objects(path): + """.MOD -> list of [video] object= geometry basenames (no extension). + A MOD may carry several objects ("number and order is significant!"), e.g. + doors: frame + leaves (+ low-poly LOD twins, which we drop: any name equal to + an earlier name + 'l'). Also returns the [gamedata] class and Subsystems file. + -> (objects, gameclass, subsystems_file)""" + sec = _parse_ini(path) + vid = sec.get("video") or {} + raw = vid.get("object") or vid.get("name") or "" + if not raw: + for s in sec.values(): + if s.get("object"): + raw = s["object"] + break + names = [] + for tok in raw.split(): + base = os.path.splitext(os.path.basename(tok))[0].lower() + if not tok.lower().endswith((".bgf", ".b2z", ".biz")): + continue # skip node-name tokens + if base[:-1] in names and base.endswith("l"): + continue # low-poly LOD twin of an earlier object + names.append(base) + game = sec.get("gamedata") or {} + return names, (game.get("class") or "").lower(), game.get("subsystems") + + +def parse_mod(path): + """Back-compat: the first video object basename, or None.""" + names, _, _ = parse_mod_objects(path) + return names[0] if names else None + + +def parse_sub(path): + """.SUB subsystems -> {collision_stem: {'extent':[x,y,z], 'travel':s, 'dead':s}} + for DoorClassID sections. The collision stem (dr1_cv -> dr1) links a subsystem + to its video object by name.""" + out = {} + for name, sec in _parse_ini(path).items(): + if "motionextent" not in sec: + continue + ext = [float(x) for x in sec["motionextent"].split()[:3]] + coll = sec.get("collision", name) + stem = os.path.splitext(os.path.basename(coll))[0].lower() + if stem.endswith("_cv"): + stem = stem[:-3] + out[stem] = {"extent": ext, + "travel": float(sec.get("traveltime", 10.0)), + "dead": float(sec.get("deadtime", 3.0))} + return out + + +if __name__ == "__main__": + path = sys.argv[1] + secs = load_sections(path) + types = {} + for n, s in secs.items(): + types.setdefault(s.get("type"), []).append(n) + print("root:", pick_root(secs, path)) + for t, names in types.items(): + print(" %-10s x%-3d %s" % (t, len(names), names[:8])) diff --git a/dpl3-revive/parser/scn.py b/dpl3-revive/parser/scn.py new file mode 100644 index 0000000..bc4022b --- /dev/null +++ b/dpl3-revive/parser/scn.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +scn.py -- reader for DPL3 ".SCN" scene files (the FLYK "cheesy scene format"). + +.SCN is plain text, one command per line. This module implements the core subset +that FLYK.C parses -- environment + object placement -- which covers the vast +majority of every scene on the disk (STATIC alone is ~5000 of ~9000 lines): + + FOG z0 z1 r g b fog start/end distance + colour + BACKGND r g b background clear colour + AMBIENT r g b ambient light + LIGHT r g b ax ay az directional light: colour + orientation angles + STATIC name scale x y z ax ay az place a model instance + DYNAMIC name scale splinefile t0 dt model that follows a camera spline + SCROLL texname u0 v0 du dv scroll rate for a TEXTURE entity ("lib:name" + or a model-own name); overrides the SPECIAL + " SCROLL ..." baked into the material source + +Comments start with // or #. Other keywords seen in game-tool scenes (SPECIALFX, +MORPH, TEXTURE/GEOMETRY/MATERIAL, ZONE/START/END, CLIP, VIEWANGLE, ...) are +recognised and skipped by this loader -- see spec/SCN_FORMAT.md. + +Object placement transform (from FLYK.C + MATRIX.C), row-vector convention +(a point is a row, p' = p . M), angles in DEGREES: + + M = Rz(az) . Rx(ax) . Ry(ay) . Scale(s) . Translate(x,y,z) +""" +import math +import sys + + +# ---- row-vector 4x4 matrices (DPL convention: translation in the bottom row) -- +def ident(): + return [[1.0 if i == j else 0.0 for j in range(4)] for i in range(4)] + + +def matmul(a, b): + return [[sum(a[i][k] * b[k][j] for k in range(4)) for j in range(4)] for i in range(4)] + + +def m_translate(x, y, z): + m = ident(); m[3][0], m[3][1], m[3][2] = x, y, z; return m + + +def m_scale(s): + m = ident(); m[0][0] = m[1][1] = m[2][2] = s; return m + + +def m_rotZ(deg): + c, s = math.cos(math.radians(deg)), math.sin(math.radians(deg)) + return [[c, s, 0, 0], [-s, c, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] + + +def m_rotX(deg): + c, s = math.cos(math.radians(deg)), math.sin(math.radians(deg)) + return [[1, 0, 0, 0], [0, c, s, 0], [0, -s, c, 0], [0, 0, 0, 1]] + + +def m_rotY(deg): + c, s = math.cos(math.radians(deg)), math.sin(math.radians(deg)) + return [[c, 0, -s, 0], [0, 1, 0, 0], [s, 0, c, 0], [0, 0, 0, 1]] + + +def instance_matrix(scale, tx, ty, tz, ax, ay, az): + """Exactly the sequence FLYK applies: Rz . Rx . Ry . Scale . Translate.""" + m = m_rotZ(az) + m = matmul(m, m_rotX(ax)) + m = matmul(m, m_rotY(ay)) + m = matmul(m, m_scale(scale)) + m = matmul(m, m_translate(tx, ty, tz)) + return m + + +def xform_point(m, x, y, z): + return (x*m[0][0] + y*m[1][0] + z*m[2][0] + m[3][0], + x*m[0][1] + y*m[1][1] + z*m[2][1] + m[3][1], + x*m[0][2] + y*m[1][2] + z*m[2][2] + m[3][2]) + + +def xform_dir(m, x, y, z): + """rotate a direction/normal by the upper-left 3x3 (no translation).""" + return (x*m[0][0] + y*m[1][0] + z*m[2][0], + x*m[0][1] + y*m[1][1] + z*m[2][1], + x*m[0][2] + y*m[1][2] + z*m[2][2]) + + +# ---- scene model ----------------------------------------------------------- +class Instance: + __slots__ = ("name", "scale", "pos", "rot", "dynamic", "spline", "matrix") + + def __init__(self, name, scale, pos, rot, dynamic=False, spline=None): + self.name = name + self.scale = scale + self.pos = pos + self.rot = rot + self.dynamic = dynamic + self.spline = spline + self.matrix = instance_matrix(scale, pos[0], pos[1], pos[2], rot[0], rot[1], rot[2]) + + +class Scene: + def __init__(self): + self.fog = None # (z0, z1, r, g, b) + self.background = (0, 0, 0) + self.ambient = (0, 0, 0) + self.lights = [] # list of (r,g,b, ax,ay,az) (ambient excluded) + self.instances = [] + self.start = None # START x y z ax ay az -- player/camera spawn + self.scrolls = {} # texture entity name -> [u0, v0, du, dv] + self.unhandled = {} # keyword -> count (recognised but skipped) + + +def _nums(parts, n): + return [float(x) for x in parts[:n]] + + +def load(path) -> Scene: + sc = Scene() + with open(path, "r", encoding="latin-1") as fp: + for raw in fp: + line = raw.strip() + if not line or line.startswith("//") or line.startswith("#"): + continue + p = line.split() + kw = p[0].upper() + a = p[1:] + try: + if kw == "FOG": + z0, z1, r, g, b = _nums(a, 5); sc.fog = (z0, z1, r, g, b) + elif kw == "BACKGND": + sc.background = tuple(_nums(a, 3)) + elif kw == "AMBIENT": + sc.ambient = tuple(_nums(a, 3)) + elif kw == "LIGHT": + r, g, b, ax, ay, az = _nums(a, 6) + sc.lights.append((r, g, b, ax, ay, az)) + elif kw in ("STATIC", "SSTATIC"): + name = a[0]; s, x, y, z, ax, ay, az = _nums(a[1:], 7) + sc.instances.append(Instance(name, s, (x, y, z), (ax, ay, az))) + elif kw == "DYNAMIC": + name = a[0]; s = float(a[1]); spline = a[2] + sc.instances.append(Instance(name, s, (0, 0, 0), (0, 0, 0), + dynamic=True, spline=spline)) + elif kw == "START": + sc.start = _nums(a, 6) # x y z ax ay az + elif kw == "SCROLL": + sc.scrolls[a[0].lower()] = _nums(a[1:], 4) + elif kw == "SCHILD": + pass # LOD child of the preceding STATIC -- skip, don't double-draw + elif kw in ("GEOMETRY", "MATERIAL", "TEXTURE"): + pass # search-path hints; the archive-wide index covers these + else: + sc.unhandled[kw] = sc.unhandled.get(kw, 0) + 1 + except (ValueError, IndexError): + sc.unhandled[kw + "?"] = sc.unhandled.get(kw + "?", 0) + 1 + return sc + + +if __name__ == "__main__": + sc = load(sys.argv[1]) + print("fog :", sc.fog) + print("background:", sc.background) + print("ambient :", sc.ambient) + print("lights :", len(sc.lights), sc.lights[:2]) + print("instances :", len(sc.instances)) + from collections import Counter + c = Counter(i.name for i in sc.instances) + for name, n in c.most_common(): + print(" %-14s x%d" % (name, n)) + if sc.unhandled: + print("skipped :", dict(sc.unhandled)) diff --git a/dpl3-revive/parser/spl.py b/dpl3-revive/parser/spl.py new file mode 100644 index 0000000..481588a --- /dev/null +++ b/dpl3-revive/parser/spl.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +""" +spl.py -- reader/evaluator for DPL3 ".SPL" camera-path splines. + +Faithful port of DPL3/EXAMPLES/SPLINE.C. A .SPL is plain text: + + N number of control points + x y z ax ay az x N -- position + euler angles (degrees) + +The path is a CLOSED LOOP of cubic-Hermite segments with Catmull-Rom tangents +(vel = (next - prev) / 2). Position and the three euler angles are each splined. + +We sample the loop densely and bake, per sample, a camera basis (eye / center / +up) so downstream code just needs lookAt(). Camera looks down local -Z (proved by +CAMERA.SPL starting at ay=180 and facing into the +Z scene). +""" +import math +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import scn # reuse the DPL row-vector rotation matrices + + +def _solve_cubic(v0, v1, d0, d1): + # v = a t^3 + b t^2 + c t + d ; endpoints v0,v1 and tangents d0,d1 + a = d1 + d0 - 2.0 * v1 + 2.0 * v0 + b = v1 - v0 - d0 - a + return (a, b, d0, v0) # (c0=a, c1=b, c2=c, c3=d) matching eval order + + +def _solve_rot_cubic(v0, v1, d0, d1): + if d0 > 360.0: + d0 -= 360.0 + if d1 > 360.0: + d1 -= 360.0 + a = d1 + d0 - 2.0 * v1 + 2.0 * v0 + b = v1 - v0 - d0 - a + return (a, b, d0, v0) + + +def _eval(c, t): + return c[0]*t*t*t + c[1]*t*t + c[2]*t + c[3] + + +def load_points(path): + with open(path, "r", encoding="latin-1") as fp: + toks = fp.read().split() + n = int(toks[0]) + vals = [float(x) for x in toks[1:]] + pts = [] + for i in range(n): + base = i * 6 + if base + 6 > len(vals): + break + pts.append({"pos": vals[base:base+3], "ang": vals[base+3:base+6]}) + return pts + + +def build_segments(pts): + """Compute per-knot tangents (closed loop) and per-segment cubic coeffs.""" + n = len(pts) + for i in range(n): + p, c, nx = pts[(i-1) % n], pts[i], pts[(i+1) % n] + c["vel"] = [(nx["pos"][k] - p["pos"][k]) / 2.0 for k in range(3)] + rot = [] + for k in range(3): + delta = nx["ang"][k] - c["ang"][k] + while delta > 180: + delta -= 180 + while delta < -180: + delta += 180 + rot.append(delta / 2.0) + while c["ang"][k] > 360.0: + c["ang"][k] -= 360.0 + c["rot"] = rot + segs = [] + for i in range(n): + a, b = pts[i], pts[(i+1) % n] + seg = {"pos": [], "rot": []} + for k in range(3): + seg["pos"].append(_solve_cubic(a["pos"][k], b["pos"][k], a["vel"][k], b["vel"][k])) + seg["rot"].append(_solve_rot_cubic(a["ang"][k], b["ang"][k], a["rot"][k], b["rot"][k])) + segs.append(seg) + return segs + + +def _basis(pos, ang): + """eye/center/up from position + euler (ax,ay,az) via DPL rotation order.""" + R = scn.matmul(scn.matmul(scn.m_rotZ(ang[2]), scn.m_rotX(ang[0])), scn.m_rotY(ang[1])) + fwd = scn.xform_dir(R, 0.0, 0.0, -1.0) # camera looks down local -Z + up = scn.xform_dir(R, 0.0, 1.0, 0.0) + return {"eye": list(pos), + "center": [pos[i] + fwd[i] for i in range(3)], + "up": list(up)} + + +def sample_flythrough(path, per_seg=12): + """Return a list of camera frames {eye,center,up} around the closed loop.""" + pts = load_points(path) + segs = build_segments(pts) + frames = [] + for seg in segs: + for s in range(per_seg): + t = s / float(per_seg) + pos = [_eval(seg["pos"][k], t) for k in range(3)] + ang = [_eval(seg["rot"][k], t) for k in range(3)] + frames.append(_basis(pos, ang)) + return frames + + +if __name__ == "__main__": + pts = load_points(sys.argv[1]) + frames = sample_flythrough(sys.argv[1]) + print("%d control points -> %d camera frames" % (len(pts), len(frames))) + xs = [f["eye"][0] for f in frames]; ys = [f["eye"][1] for f in frames]; zs = [f["eye"][2] for f in frames] + print("eye path X[%.0f,%.0f] Y[%.0f,%.0f] Z[%.0f,%.0f]" % + (min(xs), max(xs), min(ys), max(ys), min(zs), max(zs))) + print("frame0:", {k: [round(x, 1) for x in v] for k, v in frames[0].items()}) diff --git a/dpl3-revive/parser/svt.py b/dpl3-revive/parser/svt.py new file mode 100644 index 0000000..e49e8ea --- /dev/null +++ b/dpl3-revive/parser/svt.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +svt.py -- reader for DIVISION / VWE ".SVT" textures used by the DPL3 renderer. + +Format (from DPL3/DPL_LOAD.C : load_svt): there is NO header. The file is a raw +array of 32-bit texels, always square. The edge length is inferred from the file +size alone: + + 16384 bytes -> 64 x 64 + 65536 bytes -> 128 x 128 + 262144 bytes -> 256 x 256 + +Each texel is 4 bytes. The channel order within the 32-bit word is not stated in +the loader (the hardware consumed it directly), so this tool lets you pick it and +writes a standard PNG for viewing. Determined empirically: see spec/SVT_FORMAT.md. + +Usage: + python svt.py info + python svt.py png [out.png] [order] # order default = rgba +""" +import struct +import sys +import zlib + +SIZE_TO_EDGE = {16384: 64, 65536: 128, 262144: 256} + + +def load(path): + with open(path, "rb") as fp: + data = fp.read() + edge = SIZE_TO_EDGE.get(len(data)) + if edge is None: + raise ValueError("Bad texture file size %d (not 64/128/256 square RGBA32)" % len(data)) + return data, edge + + +def _png_chunk(tag, payload): + return (struct.pack(">I", len(payload)) + tag + payload + + struct.pack(">I", zlib.crc32(tag + payload) & 0xffffffff)) + + +def write_png(path, width, height, rgba: bytes): + """rgba = width*height*4 bytes, row-major, top-to-bottom.""" + raw = bytearray() + stride = width * 4 + for y in range(height): + raw.append(0) # filter: none + raw += rgba[y * stride:(y + 1) * stride] + out = b"\x89PNG\r\n\x1a\n" + out += _png_chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)) + out += _png_chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + out += _png_chunk(b"IEND", b"") + with open(path, "wb") as fp: + fp.write(out) + + +def to_rgba(data: bytes, order: str) -> bytes: + """Reorder 4-byte texels (file byte positions) into R,G,B,A for PNG. + `order` names which file byte maps to each channel, e.g. 'rgba','bgra'. + Use 'x' for an unused/pad byte; alpha is then forced opaque (255). + """ + idx = {c: i for i, c in enumerate(order)} + r, g, b = idx["r"], idx["g"], idx["b"] + a = idx.get("a") # may be None when a pad byte ('x') is present + mv = memoryview(data) + out = bytearray(len(data)) + for p in range(0, len(data), 4): + out[p + 0] = mv[p + r] + out[p + 1] = mv[p + g] + out[p + 2] = mv[p + b] + out[p + 3] = mv[p + a] if a is not None else 255 + return bytes(out) + + +def cmd_info(path): + data, edge = load(path) + print("file :", path) + print("size :", len(data), "bytes") + print("dims : %d x %d, 32bpp (%d texels)" % (edge, edge, edge * edge)) + # quick stats on the raw bytes to hint at channel usage + b = memoryview(data) + for ch in range(4): + vals = b[ch::4] + lo = min(vals); hi = max(vals) + print(" byte[%d]: min=%3d max=%3d" % (ch, lo, hi)) + + +def cmd_png(path, out=None, order="rgba"): + data, edge = load(path) + out = out or (path.rsplit(".", 1)[0] + ".png") + write_png(out, edge, edge, to_rgba(data, order)) + print("wrote %s : %dx%d order=%s" % (out, edge, edge, order)) + + +if __name__ == "__main__": + if len(sys.argv) < 3: + print(__doc__); sys.exit(1) + cmd = sys.argv[1] + if cmd == "info": + cmd_info(sys.argv[2]) + elif cmd == "png": + cmd_png(sys.argv[2], + sys.argv[3] if len(sys.argv) > 3 else None, + sys.argv[4] if len(sys.argv) > 4 else "xbgr") + else: + print("unknown command", cmd); sys.exit(1) diff --git a/dpl3-revive/patha/CAPTURE.md b/dpl3-revive/patha/CAPTURE.md new file mode 100644 index 0000000..7213a4f --- /dev/null +++ b/dpl3-revive/patha/CAPTURE.md @@ -0,0 +1,123 @@ +# Capturing real FLYK.EXE link traffic (Path A validation) + +Goal: run the genuine `FLYK.EXE` under DOSBox-X, have it talk to our Python +`vrboard` in place of the dead i860 board, and log every byte — to validate the +message framing against the *shipped* binary and pin the two open items +(`vr_readpixels` reply layout, and the bit30 `0x40000000` receive-side meaning). + +## Why a responder, not a passive log + +`start_velocirender()` streams the transputer `.BTL`, then blocks in +`startup_handshake()` waiting for **3 iserver transactions from the board** +(`VR_COMMS.C`). No framed `vr_*` message is sent until that handshake completes. So a +passive port logger stalls at boot and never sees the interesting scene traffic. The +link must *respond*. We keep all protocol logic in the tested Python `vrboard` and make +the emulator side a dumb byte-pipe. + +## Architecture — socket bridge + +``` + FLYK.EXE (unmodified, in DOSBox-X) + │ IN/OUT ports 0x150-0x161 (polled C012, LINKIO.C) + ▼ + DOSBox-X I/O handler (host-level C++, ~40 lines) ── TCP 127.0.0.1:8620 ──► vrserver.py + ▲ │ + └─────────────────────── board→host reply bytes ◄─────────────────────────┘ + (VirtualBoard + full logging → capture.log) +``` + +The I/O handler runs at **host** level (it's DOSBox-X code, not guest code), so it can +open a normal host socket. No guest-side driver, no changes to `FLYK.EXE`. + +## Port map (mirror of LINKIO.C `setLA`) + +| Port | Dir | Handler action | +|------|-----|----------------| +| 0x150 | R (Input Data) | return next board→host byte (from socket recv buffer) | +| 0x151 | W (Output Data) | send byte to socket (host→board) | +| 0x152 | R (Input Status) | pump socket; bit0=1 if a byte is available | +| 0x153 | R (Output Status)| bit0=1 (always ready to accept) | +| 0x160 | W reset / R fifo | on write send the RESET marker; on read return 1 | +| 0x161 | W (Analyse) | ignore | + +## Drop-in DOSBox-X I/O handler (sketch) + +Add to a new `src/hardware/vrlink.cpp` and call `VRLINK_Init()` from hardware init. +Adapt the handler signatures to your DOSBox-X version; init Winsock on Windows. + +```cpp +// vrlink.cpp -- bridge the C012 link ports (0x150-0x161) to vrserver.py +#include "inout.h" +#include // Windows; use on *nix +static SOCKET s = INVALID_SOCKET; +static unsigned char rxbuf[4096]; static int rxlen=0, rxpos=0; + +static void connect_board() { + WSADATA w; WSAStartup(MAKEWORD(2,2), &w); + s = socket(AF_INET, SOCK_STREAM, 0); + int one=1; setsockopt(s, IPPROTO_TCP, TCP_NODELAY,(char*)&one,sizeof one); + sockaddr_in a{}; a.sin_family=AF_INET; a.sin_port=htons(8620); + a.sin_addr.s_addr=inet_addr("127.0.0.1"); + connect(s,(sockaddr*)&a,sizeof a); + u_long nb=1; ioctlsocket(s, FIONBIO, &nb); // non-blocking reads +} +static void pump() { // fill rxbuf if empty + if (rxpos0){ rxlen=n; rxpos=0; } else { rxlen=rxpos=0; } +} +static Bitu rd(Bitu port, Bitu){ + switch(port){ + case 0x150: pump(); return rxpos REM (see HFLY/HLOOP for the scene/run pair) + ``` +3. Watch `capture.log` — it annotates every reassembled message and reply. Boot should + walk: RESET → 3 iserver txns → `hspcode`/`860code`/`860data`/`860bss`/`860args` + (discarded) → `init` → scene `create`/`flush`/`dcs_*`/`set_geom_verts`/ + `set_texmap_texels` → `draw_scene` → `readpixels`. + +## What the capture proves / pins + +- **Assembler validation:** every message reassembles with the source-derived framing + (incl. the mirrored bit30). Any mismatch shows up immediately as a bad length/action. +- **`vr_readpixels` layout (open item):** the request args and the reply chunking/pixel + word order the shipped lib expects — the one payload the source snapshot stubs. +- **bit30 semantics (open item):** whether FLYK's receive path requires `0x40000000` on + replies (we already set it) or ignores it. + +Once the capture is clean, swap the stub responder for the real render path +(`vrboard` → dpl3-revive pipeline) and `vr_readpixels` returns actual frames. + +## No build environment? + +If DOSBox-X + a C++ toolchain isn't set up, the alternative is to compile the original +host comms (`VR_COMMS.C` + `LINKIO.C` + `DPL_HOST.C`, port I/O teed to a file) natively +and drive it with a `STARTDPL.C`-style scene — same framed output, no emulator. Heavier +(needs the DPL types to build); the socket bridge above is the lighter path. diff --git a/dpl3-revive/patha/analyze_scene.py b/dpl3-revive/patha/analyze_scene.py new file mode 100644 index 0000000..30f2c66 --- /dev/null +++ b/dpl3-revive/patha/analyze_scene.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +analyze_scene.py -- decode the live scene graph FLYK streams to the board. + +Parses the framed region of capture.raw.bin (skipping the raw .BTL boot), tracks +create/flush per handle, and dumps the DPL node structs (VIEW camera, DCS matrices, +INSTANCE placements, MATERIAL colours) plus the mystery 0x1c/0x1d ops -- so we can +feed the dpl3-revive renderer. + + python analyze_scene.py [capture.raw.bin] +""" +import sys, struct +from vrboard import Assembler, A + +TYPE = {2:'zone',3:'view',4:'instance',5:'dcs',6:'lmodel',7:'light',8:'object', + 9:'lod',10:'geogroup',11:'geometry',12:'material',13:'texmap',14:'texture',15:'ramp'} + +def floats(b, n=None): + n = n if n else len(b)//4 + return list(struct.unpack_from('<%df' % n, b, 0)) + +def main(): + path = sys.argv[1] if len(sys.argv) > 1 else "capture.raw.bin" + data = open(path, "rb").read() + # find framed start: first 0x40ff00NN length word past the BTL + start = None + for i in range(80000, len(data)-8): + w = struct.unpack_from(' {'type':.., 'body':..} + creates = [] + op1c, op1d = [], [] + for m in asm: + if m.iserver: continue + a, p = m.action, m.payload + if a == A.create: + typ = struct.unpack_from('= 8 else 0 + nodes.setdefault(h, {})['type'] = typ + creates.append((h, typ)) + elif a == A.flush: + h = struct.unpack_from('= 8: + rem, tc = struct.unpack_from('= 68: # node(4) + matrix(64) + mat = floats(rest[4:68], 16) + for r in range(4): + print(" [" + " ".join(f"{mat[r*4+c]:8.3f}" for c in range(4)) + "]") + elif t == 'view' and len(rest) >= 64: + mat = floats(rest[0:64], 16) + print(" camera matrix:") + for r in range(4): + print(" [" + " ".join(f"{mat[r*4+c]:8.3f}" for c in range(4)) + "]") + tail = floats(rest[64:64+13*4]) if len(rest) >= 64+52 else [] + print(" view params (enable,x0,y0,x1,y1,zeye,xs,ys,hither,yon,bg3...):") + print(" ", [round(v,3) for v in tail]) + else: + print(" floats:", [round(v,3) for v in floats(rest[:min(64,len(rest))])]) + + for t in ('view', 'dcs', 'instance', 'material'): + hs = by_type.get(t, []) + if hs: + show(hs[0]) + + if op1c: + print(f"\n0x1c sample ({len(op1c[0])}B):", op1c[0].hex()) + if op1d: + p = op1d[0]; print(f"\n0x1d sample ({len(p)}B) as floats:", [round(v,3) for v in floats(p)]) + +if __name__ == '__main__': + main() diff --git a/dpl3-revive/patha/blade_render.png b/dpl3-revive/patha/blade_render.png new file mode 100644 index 0000000..352e48a Binary files /dev/null and b/dpl3-revive/patha/blade_render.png differ diff --git a/dpl3-revive/patha/bsl_pair.py b/dpl3-revive/patha/bsl_pair.py new file mode 100644 index 0000000..8ea8d19 --- /dev/null +++ b/dpl3-revive/patha/bsl_pair.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +bsl_pair.py -- decode the wire BSL slice-selector encoding by pairing: + wire side: mode-0 texmap pages + the texture nodes' selector words + file side: which .BSL file each page is (byte compare) + the bitslice values + (b2z tag 0x018) of every texture entry that references that .BSL + in the scene's .BGF/.BMF models. + + python bsl_pair.py sdemo2.raw.bin c:\\temp\\flykc\\HPDAVE\\VIDEO +""" +import os, sys, struct, glob +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'parser')) +from vrboard import VirtualBoard, Assembler +from decode_anim import find_framed_start +import b2z + +cap = sys.argv[1] if len(sys.argv) > 1 else 'sdemo2.raw.bin' +vid = sys.argv[2] if len(sys.argv) > 2 else r'c:\temp\flykc\HPDAVE\VIDEO' + +data = open(cap, 'rb').read() +board = VirtualBoard(); asm = Assembler(); asm.feed(data[find_framed_start(data):]) +for m in asm: + try: board.handle(m) + except Exception: pass + if board.frames >= 50: break + +types = {h: nd.get('type') for h, nd in board.nodes.items()} + +# wire: selectors per texmap +sel_by_texmap = {} +for h, nd in board.nodes.items(): + if types.get(h) != 0xc: + continue + b = nd.get('body') or b'' + if len(b) < 56: + continue + texm = struct.unpack_from(' file {match}") + print(f" wire selectors: {sorted(set(s for _, s in sels))} " + f"({[hex(s) for _, s in sorted(sels)]})") + if match: + keys = [k for k in filetex if match.split('.')[0] in k] + for k in keys: + for name, bs, src in sorted(set(filetex[k])): + print(f" file: {name:24} bitslice={bs:3} (in {src}, map {k})") diff --git a/dpl3-revive/patha/bt1.raw.bin b/dpl3-revive/patha/bt1.raw.bin new file mode 100644 index 0000000..01caeef Binary files /dev/null and b/dpl3-revive/patha/bt1.raw.bin differ diff --git a/dpl3-revive/patha/bt2.raw.bin b/dpl3-revive/patha/bt2.raw.bin new file mode 100644 index 0000000..e69de29 diff --git a/dpl3-revive/patha/bt3.raw.bin b/dpl3-revive/patha/bt3.raw.bin new file mode 100644 index 0000000..18ba6f6 Binary files /dev/null and b/dpl3-revive/patha/bt3.raw.bin differ diff --git a/dpl3-revive/patha/bt3_render.png b/dpl3-revive/patha/bt3_render.png new file mode 100644 index 0000000..c38ae7d Binary files /dev/null and b/dpl3-revive/patha/bt3_render.png differ diff --git a/dpl3-revive/patha/bt4.raw.bin b/dpl3-revive/patha/bt4.raw.bin new file mode 100644 index 0000000..1372550 Binary files /dev/null and b/dpl3-revive/patha/bt4.raw.bin differ diff --git a/dpl3-revive/patha/bt4_alpha.png b/dpl3-revive/patha/bt4_alpha.png new file mode 100644 index 0000000..42dcc28 Binary files /dev/null and b/dpl3-revive/patha/bt4_alpha.png differ diff --git a/dpl3-revive/patha/bt4_f120.png b/dpl3-revive/patha/bt4_f120.png new file mode 100644 index 0000000..f1a57f5 Binary files /dev/null and b/dpl3-revive/patha/bt4_f120.png differ diff --git a/dpl3-revive/patha/bt4_f1200.png b/dpl3-revive/patha/bt4_f1200.png new file mode 100644 index 0000000..6ef6da8 Binary files /dev/null and b/dpl3-revive/patha/bt4_f1200.png differ diff --git a/dpl3-revive/patha/bt4_f30.png b/dpl3-revive/patha/bt4_f30.png new file mode 100644 index 0000000..f1a57f5 Binary files /dev/null and b/dpl3-revive/patha/bt4_f30.png differ diff --git a/dpl3-revive/patha/bt4_f400.png b/dpl3-revive/patha/bt4_f400.png new file mode 100644 index 0000000..32526c2 Binary files /dev/null and b/dpl3-revive/patha/bt4_f400.png differ diff --git a/dpl3-revive/patha/bt4_nofog.png b/dpl3-revive/patha/bt4_nofog.png new file mode 100644 index 0000000..b976302 Binary files /dev/null and b/dpl3-revive/patha/bt4_nofog.png differ diff --git a/dpl3-revive/patha/bt4_texfix.png b/dpl3-revive/patha/bt4_texfix.png new file mode 100644 index 0000000..13b526f Binary files /dev/null and b/dpl3-revive/patha/bt4_texfix.png differ diff --git a/dpl3-revive/patha/bt5.raw.bin b/dpl3-revive/patha/bt5.raw.bin new file mode 100644 index 0000000..8bad6b0 Binary files /dev/null and b/dpl3-revive/patha/bt5.raw.bin differ diff --git a/dpl3-revive/patha/bt5_aerial.png b/dpl3-revive/patha/bt5_aerial.png new file mode 100644 index 0000000..8afb41d Binary files /dev/null and b/dpl3-revive/patha/bt5_aerial.png differ diff --git a/dpl3-revive/patha/bt5_gl.png b/dpl3-revive/patha/bt5_gl.png new file mode 100644 index 0000000..d4ed83b Binary files /dev/null and b/dpl3-revive/patha/bt5_gl.png differ diff --git a/dpl3-revive/patha/bt5_soft.png b/dpl3-revive/patha/bt5_soft.png new file mode 100644 index 0000000..aff419e Binary files /dev/null and b/dpl3-revive/patha/bt5_soft.png differ diff --git a/dpl3-revive/patha/bt6.raw.bin b/dpl3-revive/patha/bt6.raw.bin new file mode 100644 index 0000000..728a305 Binary files /dev/null and b/dpl3-revive/patha/bt6.raw.bin differ diff --git a/dpl3-revive/patha/bt7.raw.bin b/dpl3-revive/patha/bt7.raw.bin new file mode 100644 index 0000000..456ab83 Binary files /dev/null and b/dpl3-revive/patha/bt7.raw.bin differ diff --git a/dpl3-revive/patha/bt7_chase.png b/dpl3-revive/patha/bt7_chase.png new file mode 100644 index 0000000..953090b Binary files /dev/null and b/dpl3-revive/patha/bt7_chase.png differ diff --git a/dpl3-revive/patha/bt8.raw.bin b/dpl3-revive/patha/bt8.raw.bin new file mode 100644 index 0000000..530fcf0 Binary files /dev/null and b/dpl3-revive/patha/bt8.raw.bin differ diff --git a/dpl3-revive/patha/bt8_mech.png b/dpl3-revive/patha/bt8_mech.png new file mode 100644 index 0000000..c488f3d Binary files /dev/null and b/dpl3-revive/patha/bt8_mech.png differ diff --git a/dpl3-revive/patha/bt9.raw.bin b/dpl3-revive/patha/bt9.raw.bin new file mode 100644 index 0000000..e6fc601 Binary files /dev/null and b/dpl3-revive/patha/bt9.raw.bin differ diff --git a/dpl3-revive/patha/bt_paneltex.png b/dpl3-revive/patha/bt_paneltex.png new file mode 100644 index 0000000..84b335a Binary files /dev/null and b/dpl3-revive/patha/bt_paneltex.png differ diff --git a/dpl3-revive/patha/bt_staging.png b/dpl3-revive/patha/bt_staging.png new file mode 100644 index 0000000..4f36a63 Binary files /dev/null and b/dpl3-revive/patha/bt_staging.png differ diff --git a/dpl3-revive/patha/bt_tex_3c4.png b/dpl3-revive/patha/bt_tex_3c4.png new file mode 100644 index 0000000..84b335a Binary files /dev/null and b/dpl3-revive/patha/bt_tex_3c4.png differ diff --git a/dpl3-revive/patha/bt_tex_6f0.png b/dpl3-revive/patha/bt_tex_6f0.png new file mode 100644 index 0000000..84b335a Binary files /dev/null and b/dpl3-revive/patha/bt_tex_6f0.png differ diff --git a/dpl3-revive/patha/btnub1.raw.bin b/dpl3-revive/patha/btnub1.raw.bin new file mode 100644 index 0000000..e69de29 diff --git a/dpl3-revive/patha/btnub2.raw.bin b/dpl3-revive/patha/btnub2.raw.bin new file mode 100644 index 0000000..e69de29 diff --git a/dpl3-revive/patha/btnub3.raw.bin b/dpl3-revive/patha/btnub3.raw.bin new file mode 100644 index 0000000..e69de29 diff --git a/dpl3-revive/patha/btnub4.raw.bin b/dpl3-revive/patha/btnub4.raw.bin new file mode 100644 index 0000000..e69de29 diff --git a/dpl3-revive/patha/btnub5.raw.bin b/dpl3-revive/patha/btnub5.raw.bin new file mode 100644 index 0000000..8ccfc59 Binary files /dev/null and b/dpl3-revive/patha/btnub5.raw.bin differ diff --git a/dpl3-revive/patha/camfix_a.png b/dpl3-revive/patha/camfix_a.png new file mode 100644 index 0000000..d303155 Binary files /dev/null and b/dpl3-revive/patha/camfix_a.png differ diff --git a/dpl3-revive/patha/camfix_b.png b/dpl3-revive/patha/camfix_b.png new file mode 100644 index 0000000..19e5a1f Binary files /dev/null and b/dpl3-revive/patha/camfix_b.png differ diff --git a/dpl3-revive/patha/census.py b/dpl3-revive/patha/census.py new file mode 100644 index 0000000..986e220 --- /dev/null +++ b/dpl3-revive/patha/census.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +""" +census.py -- full inventory of a staged-run capture: create types, list_add tree, +instance/geogroup/material flush refs. Establishes the shipped node-type ids and +the object->lod->geogroup->geometry linkage the renderer walks. + + python census.py cap5.raw.bin +""" +import sys, struct +from collections import defaultdict +sys.path.insert(0, '.') +from vrboard import Assembler, A +from decode_anim import find_framed_start + +# shipped type ids (established cap3-cap5; 1994 ids differed from 7 up) +TYPE = {2:'zone',3:'view',4:'instance',5:'dcs',6:'lmodel',7:'object',8:'lod', + 9:'geogroup',0xa:'geometry',0xb:'material',0xc:'texmap',0xd:'texture', + 0xe:'light',0xf:'ramp'} + +def main(): + path = sys.argv[1] if len(sys.argv) > 1 else 'cap5.raw.bin' + data = open(path, 'rb').read() + asm = Assembler(); asm.feed(data[find_framed_start(data):]) + types, flushes, edges = {}, defaultdict(list), [] + extra = defaultdict(int) + for m in asm: + if m.iserver: continue + a, p = m.action, m.payload + if a == A.create and len(p) >= 8: + t, h = struct.unpack_from(' 1e-6 and abs(f) < 1e6: out.append(f'[{i}]={f:.3f}') + print(f' {t:9} {h:#04x} ({len(body)}B x{len(flushes[h])}): ' + ' '.join(out)) + +if __name__ == '__main__': + main() diff --git a/dpl3-revive/patha/debug_ident.py b/dpl3-revive/patha/debug_ident.py new file mode 100644 index 0000000..5fd3b07 --- /dev/null +++ b/dpl3-revive/patha/debug_ident.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""debug_ident.py -- render a capture frame with each instance in a unique flat +color (no materials/textures) to identify what occupies the screen.""" +import os, sys, struct +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +os.environ.setdefault('SDL_VIDEODRIVER', 'dummy') +import numpy as np +from vrboard import VirtualBoard, Assembler +from decode_anim import find_framed_start +import vrview + +path = sys.argv[1] if len(sys.argv) > 1 else 'klng.raw.bin' +upto = int(sys.argv[2]) if len(sys.argv) > 2 else 900 +out = sys.argv[3] if len(sys.argv) > 3 else 'ident.png' + +data = open(path, 'rb').read() +board = VirtualBoard(); asm = Assembler(); asm.feed(data[find_framed_start(data):]) +for m in asm: + try: board.handle(m) + except Exception: pass + if board.frames >= upto: break + +r = vrview.Renderer() +c = r.cache; c.maybe_rebuild(board) +V = np.linalg.inv(r.chain_matrix(board, c.cam_chain)) +W, H = r.w, r.h +img = np.zeros((H, W, 3), np.float32) +zbuf = np.full((H, W), np.inf, np.float32) +vp = {'x0': -1, 'y0': -0.625, 'x1': 1, 'y1': 0.625, 'zeye': 1.0, 'hither': 1.0} +import colorsys +legend = [] +for i, inst in enumerate(c.instances): + M = r.chain_matrix(board, inst['chain']) @ V + R, T = M[:3, :3], M[3, :3] + col3 = np.array(colorsys.hsv_to_rgb((i * 0.37) % 1.0, 0.9, 1.0)) * 255 + drew = False + for gh in inst['geoms']: + mesh = c._mesh(board, gh) + if mesh is None: continue + pe = mesh['pos'] @ R + T + z = -pe[:, 2] + ok = z > vp['hither'] + if not ok.any(): continue + zs = np.where(ok, z, 1.0) + px = (pe[:, 0] * vp['zeye'] / zs - vp['x0']) / 2 * W + py = (1 - (pe[:, 1] * vp['zeye'] / zs - vp['y0']) / 1.25) * H + col = np.repeat(col3[None, :], len(pe), 0) + r._raster(mesh['tri'], px, py, z, ok, col, None, None, None, None, + np.zeros(3), img, zbuf, W, H) + drew = True + if drew: + legend.append((i, inst['chain'][0], [int(x) for x in col3])) + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'parser')) +import svt +rgba = np.zeros((H, W, 4), np.uint8); rgba[..., 3] = 255 +rgba[..., :3] = np.clip(img, 0, 255).astype(np.uint8) +svt.write_png(out, W, H, rgba.tobytes()) +print('wrote', out) + +# which instances actually own visible pixels? +vis = {} +flat = rgba[..., :3].reshape(-1, 3) +for i, dcs, col in legend: + n = int(np.all(np.abs(flat.astype(int) - col) < 12, axis=1).sum()) + if n > 200: + vis[i] = (hex(dcs), col, n) +for i, (dcs, col, n) in sorted(vis.items(), key=lambda kv: -kv[1][2]): + print(f'instance {i:3} dcs {dcs:>6} color={col} pixels={n}') diff --git a/dpl3-revive/patha/debug_view.py b/dpl3-revive/patha/debug_view.py new file mode 100644 index 0000000..795bcb1 --- /dev/null +++ b/dpl3-revive/patha/debug_view.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""debug_view.py -- numeric check of vrview transforms against a capture replay.""" +import os, sys, struct +import numpy as np +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +os.environ.setdefault('SDL_VIDEODRIVER', 'dummy') +from vrboard import VirtualBoard, Assembler +from decode_anim import find_framed_start +from vrview import Renderer, _mat_from_dcs + +path = sys.argv[1] if len(sys.argv) > 1 else 'cap5.raw.bin' +data = open(path, 'rb').read() +board = VirtualBoard() +asm = Assembler(); asm.feed(data[find_framed_start(data):]) +for m in asm: + try: board.handle(m) + except Exception: pass + if board.frames >= 600: break + +r = Renderer(); r.cache.maybe_rebuild(board) +c = r.cache +print('cam chain', [hex(h) for h in c.cam_chain]) +for h in (0x1, 0x2, 0x3): + b = board.nodes.get(h, {}).get('body') or b'' + if len(b) >= 76: + M = _mat_from_dcs(b) + print(f'dcs {h:#x} diag={np.diag(M)[:3]} row3={M[3,:3]} anim={h in board.anim}') +V = np.linalg.inv(r.chain_matrix(board, c.cam_chain)) +print('V row3 (eye offset):', V[3, :3]) + +for inst in c.instances: + M = r.chain_matrix(board, inst['chain']) + for gh in inst['geoms']: + mesh = c.meshes[gh] + pw = mesh['pos'] @ M[:3, :3] + M[3, :3] + pe = pw @ V[:3, :3] + V[3, :3] + z = -pe[:, 2] + vis = z > 8 + if vis.any(): + xp = pe[vis, 0] * 1.3 / z[vis]; yp = pe[vis, 1] * 1.3 / z[vis] + scr = f"proj x[{xp.min():7.2f},{xp.max():7.2f}] y[{yp.min():7.2f},{yp.max():7.2f}]" + else: + scr = "behind camera" + print(f"dcs={inst['chain'][0]:#04x} geom={gh:#04x} v={len(pw):4d} " + f"world y[{pw[:,1].min():8.1f},{pw[:,1].max():8.1f}] " + f"size={np.ptp(pw,0).round(0)} {scr}") diff --git a/dpl3-revive/patha/decode_anim.py b/dpl3-revive/patha/decode_anim.py new file mode 100644 index 0000000..ee2b1b5 --- /dev/null +++ b/dpl3-revive/patha/decode_anim.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +decode_anim.py -- full decode of the live animation stream (action 0x1d) plus the +scene-graph edges, from a framed capture of a FLYK.EXE run. + +Answers, from capture.raw.bin: + 1. exact 0x1d record layout + which handles it animates + 2. the scene-graph edges (dcs_link/list_add/dcs_nest) -> who is the VIEW's DCS + 3. creates in wire order (to map handles -> SHARKS.SCN entities by order) + 4. per-frame trajectories of the animated handles (draw_scene = frame boundary) + + python decode_anim.py [capture.raw.bin] +""" +import sys, struct +from collections import Counter, defaultdict +from vrboard import Assembler, A + +TYPE = {2:'zone',3:'view',4:'instance',5:'dcs',6:'lmodel',7:'light',8:'object', + 9:'lod',10:'geogroup',11:'geometry',12:'material',13:'texmap',14:'texture',15:'ramp'} + +def find_framed_start(data): + for i in range(80000, len(data)-8): + w = struct.unpack_from(' 1 else "capture.raw.bin" + data = open(path, "rb").read() + start = find_framed_start(data) + asm = Assembler(); asm.feed(data[start:]) + + creates = [] # (handle, type) in wire order + flushes = defaultdict(list) # handle -> [body bytes] + edges = [] # (op, a, b) in wire order + anim = [] # (frame, handle, w1, 12 floats) for 0x1d + op1c = [] + frame = 0 + for m in asm: + if m.iserver: continue + a, p = m.action, m.payload + if a == A.create and len(p) >= 8: + typ, h = struct.unpack_from('= 56: + h, w1 = struct.unpack_from(' 1: + print(f" handle {h:#06x} ({tname(h)}): {len(lst)} flushes") + +if __name__ == '__main__': + main() diff --git a/dpl3-revive/patha/decode_anim2.py b/dpl3-revive/patha/decode_anim2.py new file mode 100644 index 0000000..83a7d1a --- /dev/null +++ b/dpl3-revive/patha/decode_anim2.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +""" +decode_anim2.py -- second-pass decode: 0x1d split by word1 (the real handle), +full DCS body dump (matrix + parent/sibling/child tree), zone/view bodies, +init argv, 0x1c hex, and ASCII strings on the wire. +""" +import sys, struct +from collections import defaultdict +from vrboard import Assembler, A +from decode_anim import find_framed_start, TYPE + +def main(): + path = sys.argv[1] if len(sys.argv) > 1 else "capture.raw.bin" + data = open(path, "rb").read() + asm = Assembler(); asm.feed(data[find_framed_start(data):]) + + types = {} + flushes = defaultdict(list) + anim = defaultdict(list) # w1 -> [(frame, w0, floats)] + op1c = [] + init_argv = None + frame = 0 + for m in asm: + if m.iserver: continue + a, p = m.action, m.payload + if a == A.init: + init_argv = p + elif a == A.create and len(p) >= 8: + typ, h = struct.unpack_from('= 56: + w0, w1 = struct.unpack_from('=6 chars) in framed region ===") + import re + for mt in re.finditer(rb'[ -~]{6,}', data[find_framed_start(data):]): + print(" ", mt.group().decode('latin1')) + +if __name__ == '__main__': + main() diff --git a/dpl3-revive/patha/flyk_heading_check.png b/dpl3-revive/patha/flyk_heading_check.png new file mode 100644 index 0000000..3e53245 Binary files /dev/null and b/dpl3-revive/patha/flyk_heading_check.png differ diff --git a/dpl3-revive/patha/flyk_live_render.png b/dpl3-revive/patha/flyk_live_render.png new file mode 100644 index 0000000..88bd8ae Binary files /dev/null and b/dpl3-revive/patha/flyk_live_render.png differ diff --git a/dpl3-revive/patha/flyk_live_render2.png b/dpl3-revive/patha/flyk_live_render2.png new file mode 100644 index 0000000..a6a13ed Binary files /dev/null and b/dpl3-revive/patha/flyk_live_render2.png differ diff --git a/dpl3-revive/patha/flyk_render.png b/dpl3-revive/patha/flyk_render.png new file mode 100644 index 0000000..864c3e9 Binary files /dev/null and b/dpl3-revive/patha/flyk_render.png differ diff --git a/dpl3-revive/patha/flyk_vr.conf b/dpl3-revive/patha/flyk_vr.conf new file mode 100644 index 0000000..6a0e86f --- /dev/null +++ b/dpl3-revive/patha/flyk_vr.conf @@ -0,0 +1,48 @@ +# flyk_vr.conf -- run the original FLYK.EXE against the virtual VelociRender board. +# +# 1. python patha\vrrun.py [prefix] (board, 127.0.0.1:8620) +# 2. $env:VRLINK='1'; G:\DOSBox-X\dosbox-x-vrlink.exe -conf patha\flyk_vr.conf +# +# C: is the staged asset tree (patha notes / stage step): +# c:\temp\flykc\HPDAVE = sda4\HPDAVE (FLYK.EXE, SHARKS.SCN, BTL, MNG) +# c:\temp\flykc\GEOMETRY = sda4\DPL3\GEOMETRY (satisfies SCN "GEOMETRY ..\geometry") +# c:\temp\flykc\TEXTURE = sda4\DPL3\TEXTURE (satisfies SCN "TEXTURE ..\texture") +# c:\temp\flykc\SHARKS = empty (no source dir survives on the drive) +# Staging exists because sda4 has no GEOMETRY/SHARKS siblings for HPDAVE: FLYK ran +# with dead search paths and built an empty world (object=NULL on every instance). + +[sdl] +autolock=false +# PHYSICAL STICK MODE (default): native joystick mapping, as validated on +# BLADE. For KEYBOARD flight uncomment the two blocks below -- arrows = stick +# X/Y, A/Z = throttle, Space = trigger (the shipped RPL4.RES has ONLY a +# Thrustmaster VTV mapper; KEYBOARD-only L4CONTROLS crashes the game, so +# keyboard flight = DOSBox synthesizing the game-port stick from keys). +# NOTE: joysticktype=fcs remaps a physical stick's axes (resting throttle +# reads as constant deflection = steady rotation) -- don't mix modes, and +# delete the staged JOYSTICK.CAL after switching. +#mapperfile=c:\temp\flykc\keyjoy.map + +#[joystick] +#joysticktype=fcs + +[dosbox] +memsize=32 + +# pod network: NETNUB (wattcp over a packet driver) hard-requires a NIC -- +# NE2000 emulation + the drive's ODI stack (LSL+NE2000+ODIPKT, loaded by +# run_game --netnub) recreate the 1996 BattleTech-center LAN inside DOSBox +[ne2000] +ne2000=true +nicbase=300 +nicirq=3 +backend=auto + +[cpu] +core=dynamic +cycles=max + +[autoexec] +mount c c:\temp\flykc +c: +call C:\RUNSCN.BAT diff --git a/dpl3-revive/patha/fxident.png b/dpl3-revive/patha/fxident.png new file mode 100644 index 0000000..1aa087f Binary files /dev/null and b/dpl3-revive/patha/fxident.png differ diff --git a/dpl3-revive/patha/fxtest_render.png b/dpl3-revive/patha/fxtest_render.png new file mode 100644 index 0000000..2adb6cf Binary files /dev/null and b/dpl3-revive/patha/fxtest_render.png differ diff --git a/dpl3-revive/patha/gen_launchers.py b/dpl3-revive/patha/gen_launchers.py new file mode 100644 index 0000000..4380794 --- /dev/null +++ b/dpl3-revive/patha/gen_launchers.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +gen_launchers.py -- generate one Windows .bat per runnable staged scene +(patha\\launch\\RUN__.BAT), each starting the virtual board + +window + DOSBox via run_demo.py. Also writes launch\\MENU.BAT (a picker). + +Skips scenes in the MUNGA key=value dialect (they hang FLYK's parser) and the +known cwd-only loaders (HPDAVE BDEMO/SAND/TEMP). + + python gen_launchers.py +""" +import os, re, glob + +HERE = os.path.dirname(os.path.abspath(__file__)) +STAGE = r"c:\temp\flykc" +OUT = os.path.join(HERE, "launch") +SKIP = {("HPDAVE", "BDEMO"), ("HPDAVE", "SAND"), ("HPDAVE", "TEMP")} + + +def munga_dialect(path): + """MUNGA map-scenes use key=value lines (viewangle=60.0) FLYK can't read.""" + try: + head = open(path, encoding='latin1').read(600) + except OSError: + return True + return bool(re.search(r'(?m)^\s*(viewangle|clip|fog|backgnd)\s*=', head)) + + +def main(): + os.makedirs(OUT, exist_ok=True) + entries = [] + for prod in ("HPDAVE", "STDAVE", "RPDAVE", "BTDAVE", "FX"): + pdir = os.path.join(STAGE, prod) + if not os.path.isdir(pdir): + continue + for scn in sorted(glob.glob(os.path.join(pdir, "*.SCN"))): + name = os.path.splitext(os.path.basename(scn))[0].upper() + if (prod, name) in SKIP or munga_dialect(scn): + continue + # ambient particle sim only where it belongs (underwater HPDAVE) + sfx = " --sfx" if (prod, name) in { + ("HPDAVE", "SHARKS"), ("HPDAVE", "SDEMO"), ("HPDAVE", "BDDEMO"), + ("HPDAVE", "FISHSPLS"), ("HPDAVE", "BDPAL")} else "" + bat = os.path.join(OUT, f"RUN_{prod}_{name}.BAT") + with open(bat, "w") as fp: + fp.write( + "@echo off\r\n" + f"rem {prod}\\{name}.SCN on the virtual VelociRender board\r\n" + f"cd /d \"{HERE}\"\r\n" + f"python run_demo.py {name} --dir {prod} --prefix {name.lower()}{sfx}\r\n" + "pause\r\n") + entries.append((prod, name)) + print(f" RUN_{prod}_{name}.BAT") + + with open(os.path.join(OUT, "MENU.BAT"), "w") as fp: + fp.write("@echo off\r\nsetlocal enabledelayedexpansion\r\n:menu\r\ncls\r\n" + "echo === DPL3 virtual VelociRender scene launcher ===\r\necho.\r\n") + for i, (prod, name) in enumerate(entries, 1): + fp.write(f"echo {i:3}. {prod:8} {name}\r\n") + fp.write("echo.\r\nset /p pick=scene number (or Q): \r\n" + "if /i \"%pick%\"==\"Q\" exit /b\r\n") + for i, (prod, name) in enumerate(entries, 1): + fp.write(f"if \"%pick%\"==\"{i}\" call \"%~dp0RUN_{prod}_{name}.BAT\"\r\n") + fp.write("goto menu\r\n") + print(f"\n{len(entries)} launchers + MENU.BAT -> {OUT}") + + +if __name__ == "__main__": + main() diff --git a/dpl3-revive/patha/heading_test.py b/dpl3-revive/patha/heading_test.py new file mode 100644 index 0000000..7878c0c --- /dev/null +++ b/dpl3-revive/patha/heading_test.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +""" +heading_test.py -- identify the 0x1d matrix convention + shark nose axis by +correlating every candidate basis vector against the swim velocity across the +whole capture. The right candidate tracks velocity with a consistently high dot. + + python heading_test.py [capture] +""" +import sys, struct +import numpy as np +sys.path.insert(0, '.') +from vrboard import Assembler +from decode_anim import find_framed_start + +path = sys.argv[1] if len(sys.argv) > 1 else 'cap7.raw.bin' +data = open(path, 'rb').read() +asm = Assembler(); asm.feed(data[find_framed_start(data):]) +recs = [] +for m in asm: + if m.iserver: continue + if m.action == 0x1d and len(m.payload) >= 56: + h = struct.unpack_from('0.9={np.mean(dots > 0.9):.2f} frac<-0.9={np.mean(dots < -0.9):.2f}' + f' frac|.|<0.5={np.mean(np.abs(dots) < 0.5):.2f}') diff --git a/dpl3-revive/patha/hunt_actions.py b/dpl3-revive/patha/hunt_actions.py new file mode 100644 index 0000000..1bbd891 --- /dev/null +++ b/dpl3-revive/patha/hunt_actions.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +hunt_actions.py -- static hunt for undecoded vr_action ids in shipped FLYK.EXE. + +Method: parse the LE fixup tables to find code sites that reference a given +data-segment string (raw LE pages hold placeholders at fixup sites, so naive +scanning cannot work), then disassemble the surrounding function with capstone +and report the immediate constants loaded near velocirender_transmit calls -- +the action id is among them. + + python hunt_actions.py "get_geom_vertices" [path-to-exe] +""" +import os, sys, struct +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from leimage import LE, rd + +EXE = sys.argv[2] if len(sys.argv) > 2 else \ + r"s:\OneDrive\Tesla III\DaveMcCoy\sda4\HPDAVE\FLYK.EXE" + + +def parse_fixups(le): + """Yield (src_linear, target_obj, target_off) for 32-bit offset fixups.""" + with open(le.path, 'rb') as f: + # page fixup offset table: n_pages+1 dwords + pgtab = rd(f, le.fixup_pg_off, 4 * (le.n_pages + 1)) + offs = struct.unpack('<%dI' % (le.n_pages + 1), pgtab) + recs_base = le.fixup_rec_off + # page -> linear address mapping: need object bases + page ranges + objs = rd(f, le.obj_tab_off, 24 * le.n_objects) + page_lin = {} + for i in range(le.n_objects): + vsize, base, flags, pgidx, pgcnt, _ = struct.unpack_from('<6I', objs, i * 24) + for k in range(pgcnt): + page_lin[pgidx + k] = base + k * le.page_size + out = [] + for pg in range(1, le.n_pages + 1): + lo, hi = offs[pg - 1], offs[pg] + if hi <= lo: + continue + blob = rd(f, recs_base + lo, hi - lo) + p = 0 + while p < len(blob) - 3: + src = blob[p]; flags = blob[p + 1]; p += 2 + srcoffs = [] + if src & 0x20: # source list + cnt = blob[p]; p += 1 + else: + srcoffs.append(struct.unpack_from(' 1 else b"get_geom_vertices" + le = LE(EXE) + base1, code = le.image(1) + base2, data = le.image(2) + print(f"code obj1 @ {base1:#x} ({len(code)} bytes), data obj2 @ {base2:#x}") + + # all occurrences of the pattern in the data object + targets = [] + i = 0 + while True: + i = data.find(pat, i) + if i < 0: + break + s = max(0, i - 60) + start = data.rfind(b'\x00', s, i) + 1 + targets.append((start, data[start:data.find(b'\x00', i)])) + i += 1 + if not targets: + print("string not found"); return + for off, s in targets: + print(f"data+{off:#x} (lin {base2+off:#x}): {s[:70]!r}") + + fixups = parse_fixups(le) + print(f"{len(fixups)} 32-bit fixups parsed") + + import capstone + md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32) + md.detail = False + + for off, s in targets: + want = {base2 + off} + sites = [src for src, tobj, toff in fixups + if tobj == 2 and (base2 + toff) in want] + print(f"\n=== refs to {s[:50]!r}: {len(sites)} site(s)") + for src in sites: + # disassemble a window before the reference site + lo = max(base1, src - 0x90) + blob = code[lo - base1: src - base1 + 0x60] + print(f"-- code around {src:#x}:") + for ins in md.disasm(blob, lo): + mark = ' <== strref' if ins.address <= src < ins.address + ins.size else '' + if ins.mnemonic in ('mov', 'push', 'call', 'cmp') or mark: + txt = f" {ins.address:#x}: {ins.mnemonic} {ins.op_str}{mark}" + if ins.mnemonic in ('mov', 'push') and ',' in ins.op_str: + try: + imm = int(ins.op_str.split(',')[1].strip(), 0) + if 0 < imm < 0x60: + txt += f" ** small imm {imm:#x}" + except ValueError: + pass + print(txt) + + +if __name__ == '__main__': + main() diff --git a/dpl3-revive/patha/hunt_pe.py b/dpl3-revive/patha/hunt_pe.py new file mode 100644 index 0000000..9917c30 --- /dev/null +++ b/dpl3-revive/patha/hunt_pe.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +""" +hunt_pe.py -- static protocol hunt for the Borland PE game binaries +(RPL4OPT/BTL4OPT under 32RTM), the PE counterpart of hunt_actions.py (LE). + +Find an error-string's data VA, locate the code xref (32-bit immediate), +and disassemble the surrounding function to read off the protocol constants +(expected reply actions, request sizes) that the DOS-side source never had. + + python hunt_pe.py [--back N] [--fwd N] +""" +import struct +import sys + +import capstone + + +def pe_sections(data): + e_lfanew = struct.unpack_from(' 0 and data[s0 - 1] != 0: + s0 -= 1 + va = file_to_va(secs, base, s0) + print(f"\nstring @ file {s0:#x} va {va:#x}: " + f"{data[s0:s0+70].rstrip(bytes(1))!r}") + if va is None: + continue + pat = struct.pack(' 1 else 2 + out = sys.argv[2] if len(sys.argv) > 2 else os.path.join(HERE, "flyk_render.png") + md = bundle.build_model(bundle.SHOWCASE[idx]) + render_preview.render(md, out, cam=CAMS.get(idx)) + +if __name__ == "__main__": + main() diff --git a/dpl3-revive/patha/ident.png b/dpl3-revive/patha/ident.png new file mode 100644 index 0000000..e18a856 Binary files /dev/null and b/dpl3-revive/patha/ident.png differ diff --git a/dpl3-revive/patha/klng_render.png b/dpl3-revive/patha/klng_render.png new file mode 100644 index 0000000..691cd06 Binary files /dev/null and b/dpl3-revive/patha/klng_render.png differ diff --git a/dpl3-revive/patha/klng_render2.png b/dpl3-revive/patha/klng_render2.png new file mode 100644 index 0000000..691cd06 Binary files /dev/null and b/dpl3-revive/patha/klng_render2.png differ diff --git a/dpl3-revive/patha/launch/MENU.BAT b/dpl3-revive/patha/launch/MENU.BAT new file mode 100644 index 0000000..1b56021 --- /dev/null +++ b/dpl3-revive/patha/launch/MENU.BAT @@ -0,0 +1,126 @@ +@echo off +setlocal enabledelayedexpansion +:menu +cls +echo === DPL3 virtual VelociRender scene launcher === +echo. +echo 1. HPDAVE BATEST +echo 2. HPDAVE BDDEMO +echo 3. HPDAVE BDPAL +echo 4. HPDAVE BIGDEMO +echo 5. HPDAVE DEMOREC +echo 6. HPDAVE DUANE +echo 7. HPDAVE FISHSPLS +echo 8. HPDAVE NEWLIGHT +echo 9. HPDAVE NL1 +echo 10. HPDAVE NL2 +echo 11. HPDAVE NL3 +echo 12. HPDAVE PHIL1 +echo 13. HPDAVE PHIL2 +echo 14. HPDAVE PHIL3 +echo 15. HPDAVE PT2 +echo 16. HPDAVE SANDTEST +echo 17. HPDAVE SDEMO +echo 18. HPDAVE SHARKS +echo 19. HPDAVE TMPRIGS +echo 20. STDAVE BLACK +echo 21. STDAVE DIVCAL +echo 22. STDAVE KLNGVID +echo 23. STDAVE TREK +echo 24. STDAVE TREKVID +echo 25. RPDAVE BLADE +echo 26. RPDAVE DESTEST +echo 27. RPDAVE ONETHING +echo 28. RPDAVE RAVTEST +echo 29. BTDAVE DTEST +echo 30. BTDAVE FXTEST +echo 31. BTDAVE POLAR4 +echo 32. FX AFC +echo 33. FX AFCDAY +echo 34. FX AFCNITE +echo 35. FX BIGBOOM +echo 36. FX BOOMDAY +echo 37. FX BOOMNITE +echo 38. FX BTFX +echo 39. FX BTFX2 +echo 40. FX CALASP +echo 41. FX DETHDAY +echo 42. FX DETHNITE +echo 43. FX DTEST +echo 44. FX FIREDAY +echo 45. FX FIRENITE +echo 46. FX FLUSH +echo 47. FX FOGTEST +echo 48. FX FXTEST +echo 49. FX LITTEST +echo 50. FX MACHDAY +echo 51. FX MACHNITE +echo 52. FX MISBMDAY +echo 53. FX MISDAY +echo 54. FX MISNITE +echo 55. FX NBOOM +echo 56. FX RAIN +echo 57. FX TEST +echo 58. FX TORCHFIR +echo. +set /p pick=scene number (or Q): +if /i "%pick%"=="Q" exit /b +if "%pick%"=="1" call "%~dp0RUN_HPDAVE_BATEST.BAT" +if "%pick%"=="2" call "%~dp0RUN_HPDAVE_BDDEMO.BAT" +if "%pick%"=="3" call "%~dp0RUN_HPDAVE_BDPAL.BAT" +if "%pick%"=="4" call "%~dp0RUN_HPDAVE_BIGDEMO.BAT" +if "%pick%"=="5" call "%~dp0RUN_HPDAVE_DEMOREC.BAT" +if "%pick%"=="6" call "%~dp0RUN_HPDAVE_DUANE.BAT" +if "%pick%"=="7" call "%~dp0RUN_HPDAVE_FISHSPLS.BAT" +if "%pick%"=="8" call "%~dp0RUN_HPDAVE_NEWLIGHT.BAT" +if "%pick%"=="9" call "%~dp0RUN_HPDAVE_NL1.BAT" +if "%pick%"=="10" call "%~dp0RUN_HPDAVE_NL2.BAT" +if "%pick%"=="11" call "%~dp0RUN_HPDAVE_NL3.BAT" +if "%pick%"=="12" call "%~dp0RUN_HPDAVE_PHIL1.BAT" +if "%pick%"=="13" call "%~dp0RUN_HPDAVE_PHIL2.BAT" +if "%pick%"=="14" call "%~dp0RUN_HPDAVE_PHIL3.BAT" +if "%pick%"=="15" call "%~dp0RUN_HPDAVE_PT2.BAT" +if "%pick%"=="16" call "%~dp0RUN_HPDAVE_SANDTEST.BAT" +if "%pick%"=="17" call "%~dp0RUN_HPDAVE_SDEMO.BAT" +if "%pick%"=="18" call "%~dp0RUN_HPDAVE_SHARKS.BAT" +if "%pick%"=="19" call "%~dp0RUN_HPDAVE_TMPRIGS.BAT" +if "%pick%"=="20" call "%~dp0RUN_STDAVE_BLACK.BAT" +if "%pick%"=="21" call "%~dp0RUN_STDAVE_DIVCAL.BAT" +if "%pick%"=="22" call "%~dp0RUN_STDAVE_KLNGVID.BAT" +if "%pick%"=="23" call "%~dp0RUN_STDAVE_TREK.BAT" +if "%pick%"=="24" call "%~dp0RUN_STDAVE_TREKVID.BAT" +if "%pick%"=="25" call "%~dp0RUN_RPDAVE_BLADE.BAT" +if "%pick%"=="26" call "%~dp0RUN_RPDAVE_DESTEST.BAT" +if "%pick%"=="27" call "%~dp0RUN_RPDAVE_ONETHING.BAT" +if "%pick%"=="28" call "%~dp0RUN_RPDAVE_RAVTEST.BAT" +if "%pick%"=="29" call "%~dp0RUN_BTDAVE_DTEST.BAT" +if "%pick%"=="30" call "%~dp0RUN_BTDAVE_FXTEST.BAT" +if "%pick%"=="31" call "%~dp0RUN_BTDAVE_POLAR4.BAT" +if "%pick%"=="32" call "%~dp0RUN_FX_AFC.BAT" +if "%pick%"=="33" call "%~dp0RUN_FX_AFCDAY.BAT" +if "%pick%"=="34" call "%~dp0RUN_FX_AFCNITE.BAT" +if "%pick%"=="35" call "%~dp0RUN_FX_BIGBOOM.BAT" +if "%pick%"=="36" call "%~dp0RUN_FX_BOOMDAY.BAT" +if "%pick%"=="37" call "%~dp0RUN_FX_BOOMNITE.BAT" +if "%pick%"=="38" call "%~dp0RUN_FX_BTFX.BAT" +if "%pick%"=="39" call "%~dp0RUN_FX_BTFX2.BAT" +if "%pick%"=="40" call "%~dp0RUN_FX_CALASP.BAT" +if "%pick%"=="41" call "%~dp0RUN_FX_DETHDAY.BAT" +if "%pick%"=="42" call "%~dp0RUN_FX_DETHNITE.BAT" +if "%pick%"=="43" call "%~dp0RUN_FX_DTEST.BAT" +if "%pick%"=="44" call "%~dp0RUN_FX_FIREDAY.BAT" +if "%pick%"=="45" call "%~dp0RUN_FX_FIRENITE.BAT" +if "%pick%"=="46" call "%~dp0RUN_FX_FLUSH.BAT" +if "%pick%"=="47" call "%~dp0RUN_FX_FOGTEST.BAT" +if "%pick%"=="48" call "%~dp0RUN_FX_FXTEST.BAT" +if "%pick%"=="49" call "%~dp0RUN_FX_LITTEST.BAT" +if "%pick%"=="50" call "%~dp0RUN_FX_MACHDAY.BAT" +if "%pick%"=="51" call "%~dp0RUN_FX_MACHNITE.BAT" +if "%pick%"=="52" call "%~dp0RUN_FX_MISBMDAY.BAT" +if "%pick%"=="53" call "%~dp0RUN_FX_MISDAY.BAT" +if "%pick%"=="54" call "%~dp0RUN_FX_MISNITE.BAT" +if "%pick%"=="55" call "%~dp0RUN_FX_NBOOM.BAT" +if "%pick%"=="56" call "%~dp0RUN_FX_RAIN.BAT" +if "%pick%"=="57" call "%~dp0RUN_FX_TEST.BAT" +if "%pick%"=="58" call "%~dp0RUN_FX_TORCHFIR.BAT" +goto menu diff --git a/dpl3-revive/patha/launch/RUN_BTDAVE_DTEST.BAT b/dpl3-revive/patha/launch/RUN_BTDAVE_DTEST.BAT new file mode 100644 index 0000000..58eb856 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_BTDAVE_DTEST.BAT @@ -0,0 +1,5 @@ +@echo off +rem BTDAVE\DTEST.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py DTEST --dir BTDAVE --prefix dtest +pause diff --git a/dpl3-revive/patha/launch/RUN_BTDAVE_FXTEST.BAT b/dpl3-revive/patha/launch/RUN_BTDAVE_FXTEST.BAT new file mode 100644 index 0000000..f042786 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_BTDAVE_FXTEST.BAT @@ -0,0 +1,5 @@ +@echo off +rem BTDAVE\FXTEST.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py FXTEST --dir BTDAVE --prefix fxtest +pause diff --git a/dpl3-revive/patha/launch/RUN_BTDAVE_POLAR4.BAT b/dpl3-revive/patha/launch/RUN_BTDAVE_POLAR4.BAT new file mode 100644 index 0000000..5b44119 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_BTDAVE_POLAR4.BAT @@ -0,0 +1,5 @@ +@echo off +rem BTDAVE\POLAR4.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py POLAR4 --dir BTDAVE --prefix polar4 +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_AFC.BAT b/dpl3-revive/patha/launch/RUN_FX_AFC.BAT new file mode 100644 index 0000000..3884dec --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_AFC.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\AFC.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py AFC --dir FX --prefix afc +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_AFCDAY.BAT b/dpl3-revive/patha/launch/RUN_FX_AFCDAY.BAT new file mode 100644 index 0000000..1f9fa75 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_AFCDAY.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\AFCDAY.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py AFCDAY --dir FX --prefix afcday +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_AFCNITE.BAT b/dpl3-revive/patha/launch/RUN_FX_AFCNITE.BAT new file mode 100644 index 0000000..ce54d68 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_AFCNITE.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\AFCNITE.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py AFCNITE --dir FX --prefix afcnite +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_BIGBOOM.BAT b/dpl3-revive/patha/launch/RUN_FX_BIGBOOM.BAT new file mode 100644 index 0000000..bc595db --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_BIGBOOM.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\BIGBOOM.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py BIGBOOM --dir FX --prefix bigboom +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_BOOMDAY.BAT b/dpl3-revive/patha/launch/RUN_FX_BOOMDAY.BAT new file mode 100644 index 0000000..f1d291c --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_BOOMDAY.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\BOOMDAY.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py BOOMDAY --dir FX --prefix boomday +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_BOOMNITE.BAT b/dpl3-revive/patha/launch/RUN_FX_BOOMNITE.BAT new file mode 100644 index 0000000..84994f3 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_BOOMNITE.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\BOOMNITE.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py BOOMNITE --dir FX --prefix boomnite +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_BTFX.BAT b/dpl3-revive/patha/launch/RUN_FX_BTFX.BAT new file mode 100644 index 0000000..c30d1db --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_BTFX.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\BTFX.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py BTFX --dir FX --prefix btfx +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_BTFX2.BAT b/dpl3-revive/patha/launch/RUN_FX_BTFX2.BAT new file mode 100644 index 0000000..a77bf60 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_BTFX2.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\BTFX2.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py BTFX2 --dir FX --prefix btfx2 +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_CALASP.BAT b/dpl3-revive/patha/launch/RUN_FX_CALASP.BAT new file mode 100644 index 0000000..ac0a0c0 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_CALASP.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\CALASP.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py CALASP --dir FX --prefix calasp +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_DETHDAY.BAT b/dpl3-revive/patha/launch/RUN_FX_DETHDAY.BAT new file mode 100644 index 0000000..0b7b89f --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_DETHDAY.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\DETHDAY.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py DETHDAY --dir FX --prefix dethday +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_DETHNITE.BAT b/dpl3-revive/patha/launch/RUN_FX_DETHNITE.BAT new file mode 100644 index 0000000..42b7bea --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_DETHNITE.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\DETHNITE.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py DETHNITE --dir FX --prefix dethnite +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_DTEST.BAT b/dpl3-revive/patha/launch/RUN_FX_DTEST.BAT new file mode 100644 index 0000000..6ef1f9f --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_DTEST.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\DTEST.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py DTEST --dir FX --prefix dtest +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_FIREDAY.BAT b/dpl3-revive/patha/launch/RUN_FX_FIREDAY.BAT new file mode 100644 index 0000000..cb0715c --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_FIREDAY.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\FIREDAY.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py FIREDAY --dir FX --prefix fireday +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_FIRENITE.BAT b/dpl3-revive/patha/launch/RUN_FX_FIRENITE.BAT new file mode 100644 index 0000000..9ed25bc --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_FIRENITE.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\FIRENITE.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py FIRENITE --dir FX --prefix firenite +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_FLUSH.BAT b/dpl3-revive/patha/launch/RUN_FX_FLUSH.BAT new file mode 100644 index 0000000..d0a1e19 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_FLUSH.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\FLUSH.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py FLUSH --dir FX --prefix flush +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_FOGTEST.BAT b/dpl3-revive/patha/launch/RUN_FX_FOGTEST.BAT new file mode 100644 index 0000000..faff55e --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_FOGTEST.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\FOGTEST.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py FOGTEST --dir FX --prefix fogtest +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_FXTEST.BAT b/dpl3-revive/patha/launch/RUN_FX_FXTEST.BAT new file mode 100644 index 0000000..44626d3 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_FXTEST.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\FXTEST.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py FXTEST --dir FX --prefix fxtest +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_LITTEST.BAT b/dpl3-revive/patha/launch/RUN_FX_LITTEST.BAT new file mode 100644 index 0000000..5f1fb54 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_LITTEST.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\LITTEST.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py LITTEST --dir FX --prefix littest +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_MACHDAY.BAT b/dpl3-revive/patha/launch/RUN_FX_MACHDAY.BAT new file mode 100644 index 0000000..00eb433 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_MACHDAY.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\MACHDAY.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py MACHDAY --dir FX --prefix machday +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_MACHNITE.BAT b/dpl3-revive/patha/launch/RUN_FX_MACHNITE.BAT new file mode 100644 index 0000000..e676f8a --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_MACHNITE.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\MACHNITE.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py MACHNITE --dir FX --prefix machnite +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_MISBMDAY.BAT b/dpl3-revive/patha/launch/RUN_FX_MISBMDAY.BAT new file mode 100644 index 0000000..e5e9f77 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_MISBMDAY.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\MISBMDAY.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py MISBMDAY --dir FX --prefix misbmday +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_MISDAY.BAT b/dpl3-revive/patha/launch/RUN_FX_MISDAY.BAT new file mode 100644 index 0000000..7368e46 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_MISDAY.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\MISDAY.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py MISDAY --dir FX --prefix misday +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_MISNITE.BAT b/dpl3-revive/patha/launch/RUN_FX_MISNITE.BAT new file mode 100644 index 0000000..8f03183 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_MISNITE.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\MISNITE.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py MISNITE --dir FX --prefix misnite +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_NBOOM.BAT b/dpl3-revive/patha/launch/RUN_FX_NBOOM.BAT new file mode 100644 index 0000000..6dd629f --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_NBOOM.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\NBOOM.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py NBOOM --dir FX --prefix nboom +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_RAIN.BAT b/dpl3-revive/patha/launch/RUN_FX_RAIN.BAT new file mode 100644 index 0000000..4049918 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_RAIN.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\RAIN.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py RAIN --dir FX --prefix rain +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_TEST.BAT b/dpl3-revive/patha/launch/RUN_FX_TEST.BAT new file mode 100644 index 0000000..dc73ef2 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_TEST.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\TEST.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py TEST --dir FX --prefix test +pause diff --git a/dpl3-revive/patha/launch/RUN_FX_TORCHFIR.BAT b/dpl3-revive/patha/launch/RUN_FX_TORCHFIR.BAT new file mode 100644 index 0000000..378e569 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_FX_TORCHFIR.BAT @@ -0,0 +1,5 @@ +@echo off +rem FX\TORCHFIR.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py TORCHFIR --dir FX --prefix torchfir +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_BATEST.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_BATEST.BAT new file mode 100644 index 0000000..2e641a5 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_BATEST.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\BATEST.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py BATEST --dir HPDAVE --prefix batest +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_BDDEMO.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_BDDEMO.BAT new file mode 100644 index 0000000..7dd1629 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_BDDEMO.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\BDDEMO.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py BDDEMO --dir HPDAVE --prefix bddemo --sfx +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_BDPAL.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_BDPAL.BAT new file mode 100644 index 0000000..a585c28 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_BDPAL.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\BDPAL.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py BDPAL --dir HPDAVE --prefix bdpal --sfx +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_BIGDEMO.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_BIGDEMO.BAT new file mode 100644 index 0000000..b359c76 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_BIGDEMO.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\BIGDEMO.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py BIGDEMO --dir HPDAVE --prefix bigdemo +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_DEMOREC.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_DEMOREC.BAT new file mode 100644 index 0000000..f0ae12d --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_DEMOREC.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\DEMOREC.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py DEMOREC --dir HPDAVE --prefix demorec +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_DUANE.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_DUANE.BAT new file mode 100644 index 0000000..da0a9ed --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_DUANE.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\DUANE.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py DUANE --dir HPDAVE --prefix duane +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_FISHSPLS.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_FISHSPLS.BAT new file mode 100644 index 0000000..7e964c8 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_FISHSPLS.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\FISHSPLS.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py FISHSPLS --dir HPDAVE --prefix fishspls --sfx +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_NEWLIGHT.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_NEWLIGHT.BAT new file mode 100644 index 0000000..6d9f70d --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_NEWLIGHT.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\NEWLIGHT.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py NEWLIGHT --dir HPDAVE --prefix newlight +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_NL1.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_NL1.BAT new file mode 100644 index 0000000..9ffd9f6 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_NL1.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\NL1.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py NL1 --dir HPDAVE --prefix nl1 +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_NL2.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_NL2.BAT new file mode 100644 index 0000000..f880a0d --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_NL2.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\NL2.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py NL2 --dir HPDAVE --prefix nl2 +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_NL3.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_NL3.BAT new file mode 100644 index 0000000..c0da38b --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_NL3.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\NL3.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py NL3 --dir HPDAVE --prefix nl3 +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_PHIL1.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_PHIL1.BAT new file mode 100644 index 0000000..2c7d003 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_PHIL1.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\PHIL1.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py PHIL1 --dir HPDAVE --prefix phil1 +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_PHIL2.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_PHIL2.BAT new file mode 100644 index 0000000..61ccf40 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_PHIL2.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\PHIL2.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py PHIL2 --dir HPDAVE --prefix phil2 +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_PHIL3.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_PHIL3.BAT new file mode 100644 index 0000000..ab10eaa --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_PHIL3.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\PHIL3.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py PHIL3 --dir HPDAVE --prefix phil3 +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_PT2.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_PT2.BAT new file mode 100644 index 0000000..f4111b5 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_PT2.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\PT2.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py PT2 --dir HPDAVE --prefix pt2 +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_SANDTEST.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_SANDTEST.BAT new file mode 100644 index 0000000..5649952 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_SANDTEST.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\SANDTEST.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py SANDTEST --dir HPDAVE --prefix sandtest +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_SDEMO.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_SDEMO.BAT new file mode 100644 index 0000000..34f90eb --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_SDEMO.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\SDEMO.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py SDEMO --dir HPDAVE --prefix sdemo --sfx +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_SHARKS.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_SHARKS.BAT new file mode 100644 index 0000000..94e37f3 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_SHARKS.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\SHARKS.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py SHARKS --dir HPDAVE --prefix sharks --sfx +pause diff --git a/dpl3-revive/patha/launch/RUN_HPDAVE_TMPRIGS.BAT b/dpl3-revive/patha/launch/RUN_HPDAVE_TMPRIGS.BAT new file mode 100644 index 0000000..658dec6 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_HPDAVE_TMPRIGS.BAT @@ -0,0 +1,5 @@ +@echo off +rem HPDAVE\TMPRIGS.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py TMPRIGS --dir HPDAVE --prefix tmprigs +pause diff --git a/dpl3-revive/patha/launch/RUN_RPDAVE_BLADE.BAT b/dpl3-revive/patha/launch/RUN_RPDAVE_BLADE.BAT new file mode 100644 index 0000000..be70b0a --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_RPDAVE_BLADE.BAT @@ -0,0 +1,5 @@ +@echo off +rem RPDAVE\BLADE.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py BLADE --dir RPDAVE --prefix blade +pause diff --git a/dpl3-revive/patha/launch/RUN_RPDAVE_DESTEST.BAT b/dpl3-revive/patha/launch/RUN_RPDAVE_DESTEST.BAT new file mode 100644 index 0000000..4f9a260 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_RPDAVE_DESTEST.BAT @@ -0,0 +1,5 @@ +@echo off +rem RPDAVE\DESTEST.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py DESTEST --dir RPDAVE --prefix destest +pause diff --git a/dpl3-revive/patha/launch/RUN_RPDAVE_ONETHING.BAT b/dpl3-revive/patha/launch/RUN_RPDAVE_ONETHING.BAT new file mode 100644 index 0000000..310b97a --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_RPDAVE_ONETHING.BAT @@ -0,0 +1,5 @@ +@echo off +rem RPDAVE\ONETHING.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py ONETHING --dir RPDAVE --prefix onething +pause diff --git a/dpl3-revive/patha/launch/RUN_RPDAVE_RAVTEST.BAT b/dpl3-revive/patha/launch/RUN_RPDAVE_RAVTEST.BAT new file mode 100644 index 0000000..0ece8ca --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_RPDAVE_RAVTEST.BAT @@ -0,0 +1,5 @@ +@echo off +rem RPDAVE\RAVTEST.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py RAVTEST --dir RPDAVE --prefix ravtest +pause diff --git a/dpl3-revive/patha/launch/RUN_STDAVE_BLACK.BAT b/dpl3-revive/patha/launch/RUN_STDAVE_BLACK.BAT new file mode 100644 index 0000000..7f7f10c --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_STDAVE_BLACK.BAT @@ -0,0 +1,5 @@ +@echo off +rem STDAVE\BLACK.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py BLACK --dir STDAVE --prefix black +pause diff --git a/dpl3-revive/patha/launch/RUN_STDAVE_DIVCAL.BAT b/dpl3-revive/patha/launch/RUN_STDAVE_DIVCAL.BAT new file mode 100644 index 0000000..7554261 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_STDAVE_DIVCAL.BAT @@ -0,0 +1,5 @@ +@echo off +rem STDAVE\DIVCAL.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py DIVCAL --dir STDAVE --prefix divcal +pause diff --git a/dpl3-revive/patha/launch/RUN_STDAVE_KLNGVID.BAT b/dpl3-revive/patha/launch/RUN_STDAVE_KLNGVID.BAT new file mode 100644 index 0000000..ca901ba --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_STDAVE_KLNGVID.BAT @@ -0,0 +1,5 @@ +@echo off +rem STDAVE\KLNGVID.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py KLNGVID --dir STDAVE --prefix klngvid +pause diff --git a/dpl3-revive/patha/launch/RUN_STDAVE_TREK.BAT b/dpl3-revive/patha/launch/RUN_STDAVE_TREK.BAT new file mode 100644 index 0000000..c6bd8fa --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_STDAVE_TREK.BAT @@ -0,0 +1,5 @@ +@echo off +rem STDAVE\TREK.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py TREK --dir STDAVE --prefix trek +pause diff --git a/dpl3-revive/patha/launch/RUN_STDAVE_TREKVID.BAT b/dpl3-revive/patha/launch/RUN_STDAVE_TREKVID.BAT new file mode 100644 index 0000000..095fa19 --- /dev/null +++ b/dpl3-revive/patha/launch/RUN_STDAVE_TREKVID.BAT @@ -0,0 +1,5 @@ +@echo off +rem STDAVE\TREKVID.SCN on the virtual VelociRender board +cd /d "S:\OneDrive\Tesla III\DaveMcCoy\dpl3-revive\patha" +python run_demo.py TREKVID --dir STDAVE --prefix trekvid +pause diff --git a/dpl3-revive/patha/leimage.py b/dpl3-revive/patha/leimage.py new file mode 100644 index 0000000..f12e83c --- /dev/null +++ b/dpl3-revive/patha/leimage.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +""" +leimage.py -- minimal loader for DOS/4G "LE" (Linear Executable) images, +as produced by Watcom C/C++32 + the Rational/Tenberry DOS/4G(W) extender. + +Path A tooling for dpl3-revive: lets us inspect FLYK.EXE (and the other HPDAVE +binaries) -- object table, names tables, debug-info presence -- and reconstruct +the flat code/data images for capstone disassembly. + +Usage: + python leimage.py header FLYK.EXE # LE header fields + python leimage.py names FLYK.EXE # resident + non-resident name tables + python leimage.py objects FLYK.EXE # object table + page reconstruction sanity + python leimage.py strings FLYK.EXE PAT # locate a symbol/string + file offset +""" +import sys, struct + +def rd(f, off, n): + f.seek(off); return f.read(n) + +class LE: + def __init__(self, path): + self.path = path + with open(path, 'rb') as f: + mz = f.read(0x40) + assert mz[:2] == b'MZ', "not an MZ file" + self.lfanew = struct.unpack_from(' {'PRESENT' if self.debug_len else 'none'}") + + def objects(self): + with open(self.path,'rb') as f: + data = rd(f, self.obj_tab_off, 24*self.n_objects) + for i in range(self.n_objects): + vsize, base, flags, pgidx, pgcnt, resv = struct.unpack_from('<6I', data, i*24) + fl = [] + if flags & 0x1: fl.append('READ') + if flags & 0x2: fl.append('WRITE') + if flags & 0x4: fl.append('EXEC') + if flags & 0x2000: fl.append('32BIT') + print(f"obj{i+1} vsize={vsize:#010x} base={base:#010x} flags={flags:#06x} [{'|'.join(fl)}] " + f"pagemap[{pgidx}..{pgidx+pgcnt-1}] ({pgcnt} pages)") + + def image(self, objno): + """Reconstruct the flat linear image of object `objno` (1-based). + Assumes sequential, non-iterated pages (true for DOS/4GW LE).""" + with open(self.path, 'rb') as f: + objs = rd(f, self.obj_tab_off, 24 * self.n_objects) + vsize, base, flags, pgidx, pgcnt, _ = struct.unpack_from('<6I', objs, (objno-1)*24) + out = bytearray() + for k in range(pgidx, pgidx + pgcnt): # k = global 1-based page number + length = self.last_page if k == self.n_pages else self.page_size + out += rd(f, self.data_pages_off + (k-1)*self.page_size, length) + out = out[:vsize] + bytes(max(0, vsize - len(out))) # trim / zero-fill to vsize + return base, bytes(out) + + def _read_names(self, off, hardlen=None): + """Read a chain of length-prefixed name entries: len(1), chars, ordinal(2).""" + out = [] + with open(self.path,'rb') as f: + f.seek(off) + blob = f.read(hardlen if hardlen else 0x8000) + p = 0 + while p < len(blob): + ln = blob[p]; p += 1 + if ln == 0: break + name = blob[p:p+ln]; p += ln + if p+2 > len(blob): break + ordv = struct.unpack_from('= upto: + break + return board, errs + + +def main(): + from vrview import Renderer + r = Renderer() + ok = True + soft_frames = {} + for name, path, upto in SCENES: + if not os.path.exists(path): + print(f'{name:8} SKIP (no capture {path})') + continue + try: + board, herrs = replay(path, upto) + board._view = r + r.cache = type(r.cache)() # fresh SceneCache per scene + t0 = time.perf_counter() + r.frame = 0; r.skip = 1 + r.draw(board) + ms = (time.perf_counter() - t0) * 1000 + r.pygame.image.save(r.screen, f'regress_{name.lower()}.png') + soft_frames[name] = r.last_frame.copy() + print(f'{name:8} OK frame={ms:6.0f}ms instances={len(r.cache.instances):4} ' + f'lights={len(r.cache.dirlights)}+amb handler-errors={herrs}') + except Exception: + ok = False + print(f'{name:8} FAIL') + traceback.print_exc() + + # ---- GPU backend pass (moderngl, headless standalone context) ---------- + # Same scenes through vrview_gl.GLRenderer: PNG each, sustained-frame + # timing (VBOs cached after frame 1), and a mean-abs-diff sanity check + # against the software reference at the same resolution. + try: + from vrview_gl import GLRenderer + g = GLRenderer() # native 832x512 target res + print(f'-- GL backend: {g.ctx.info["GL_RENDERER"]} {g.w}x{g.h} --') + for name, path, upto in SCENES: + if not os.path.exists(path): + continue + try: + board, _ = replay(path, upto) + board._view = g + g.cache = type(g.cache)() + g.frame = 0; g.skip = 1 + g.draw(board) # frame 1: VBO/texture upload + t0 = time.perf_counter(); N = 20 + for _i in range(N): # sustained: uniforms only + g.skip = 1 + g.draw(board) + ms = (time.perf_counter() - t0) * 1000 / N + arr = g.last_frame + pgs = g.pygame.image.frombuffer(arr.tobytes(), (g.w, g.h), 'RGB') + g.pygame.image.save(pgs, f'regress_gl_{name.lower()}.png') + diff = '' + sf = soft_frames.get(name) + if sf is not None: + import pygame as _pg + small = _pg.transform.smoothscale(pgs, (sf.shape[1], sf.shape[0])) + sa = np.transpose(_pg.surfarray.array3d(small), (1, 0, 2)) + diff = (f' diff-vs-soft=' + f'{np.abs(sf.astype(int) - sa.astype(int)).mean():.1f}/255') + print(f'{name:8} GL frame={ms:6.1f}ms ({1000/max(ms,1e-3):5.0f}fps)' + f'{diff}') + except Exception: + ok = False + print(f'{name:8} GL FAIL') + traceback.print_exc() + except Exception as e: + print(f'-- GL backend unavailable ({e}) -- skipped --') + + # sect_vector self-test on SHARKS: ray from the camera into the fish cluster + board, _ = replay('cap8.raw.bin', 2000) + board._view = r + r.cache = type(r.cache)() + reply = board.handle_sect_test = None + p = struct.pack('<4f6f', 0, 0, 0, 0, 0, 5, 5, -460, 380, -430) + rep = board.do_sect_vector(p) + act, = struct.unpack_from(' fish cluster): instance handle {h:#x} ' + f'({"HIT" if h else "miss"})') + p2 = struct.pack('<4f6f', 0, 0, 0, 0, 0, 5, 5, 0, 5000, 5) + h2 = struct.unpack_from(' THRUSTMASTER (joystick flight)") + s = s2 + # RETRACE n = the scene's frame-rate divider (60/n Hz on the real board) + mr = re.search(r'(?m)^\s*RETRACE\s+(\d+)', s) + if mr: + os.environ['VRVIEW_FPS'] = str(max(1, 60 // max(1, int(mr.group(1))))) + print(f"RETRACE {mr.group(1)} -> pacing {os.environ['VRVIEW_FPS']} Hz") + # ambient SPECIALFX sim is opt-in (--sfx): most scenes' defs are event-only + if "--sfx" in sys.argv: + os.environ['VRVIEW_SFX'] = '1' + print("ambient SPECIALFX sim enabled") + with open(os.path.join(STAGE, "RUNSCN.BAT"), "w") as fp: + fp.write(f"cd \\{proddir}\r\ncall SETSVGA.BAT\r\n{exe} .\\{scene}\r\n") + print(f"scene: {proddir}\\{scene} (capture prefix: {prefix})") + + proc = [] + + def launch_dosbox(): + env = dict(os.environ, VRLINK='1') + p = subprocess.Popen([DOSBOX, "-conf", CONF], env=env, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + proc.append(p) + print(f"dosbox launched (pid {p.pid})") + + threading.Timer(2.0, launch_dosbox).start() + atexit.register(lambda: [p.kill() for p in proc if p.poll() is None]) + + # --soft = software rasterizer (reference); default is the moderngl backend + sys.argv = ["vrrun.py", prefix, "--view"] + ( + ["--soft"] if "--soft" in sys.argv else []) + import vrrun + try: + vrrun.main() + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + main() diff --git a/dpl3-revive/patha/run_game.py b/dpl3-revive/patha/run_game.py new file mode 100644 index 0000000..80294b5 --- /dev/null +++ b/dpl3-revive/patha/run_game.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +""" +run_game.py -- MUNGA game launcher: run the real Red Planet (rpl4opt) / +BattleTech (btl4opt) game binaries against the virtual VelociRender board. + +Mirrors sda4's RP.BAT: `setenv t s n n` (THRUSTMASTER controls, slow clock, +nosound, no gauges), 32RTM.EXE -x (Borland DPMI host -- the game binaries are +PE images, not Watcom LE), -egg .egg, 32rtm -u. + + python run_game.py # RPDAVE rpl4opt -egg test.egg + python run_game.py --egg last # a different .EGG + python run_game.py --dir BTDAVE --exe btl4opt + python run_game.py --setenv "t s n g" # e.g. g = gauges on + python run_game.py --controls KEYBOARD # exact L4CONTROLS value (setenv's + # presets: t=THRUSTMASTER,KEYBOARD + # r=RIO,KEYBOARD; RIO = the pod's + # serial board, dead hardware) + python run_game.py --soft # software renderer reference + +Board loop runs inline (vrrun --view); the wire log is the primary instrument: +unknown actions log as `EXTRA action 0x??`, and a stall right after one means +that action expects a reply (this is how the FLYK protocol was decoded). +""" +import os, sys, subprocess, threading, atexit + +HERE = os.path.dirname(os.path.abspath(__file__)) +STAGE = r"c:\temp\flykc" +DOSBOX = r"G:\DOSBox-X\dosbox-x-vrlink.exe" +CONF = os.path.join(HERE, "flyk_vr.conf") + + +def main(): + proddir, exe, egg, prefix, senv = "RPDAVE", "rpl4opt", "test", "game", "t s n n" + controls = None + for i, a in enumerate(sys.argv): + if a == "--dir" and i + 1 < len(sys.argv): proddir = sys.argv[i + 1] + if a == "--exe" and i + 1 < len(sys.argv): exe = sys.argv[i + 1] + if a == "--egg" and i + 1 < len(sys.argv): egg = sys.argv[i + 1] + if a == "--prefix" and i + 1 < len(sys.argv): prefix = sys.argv[i + 1] + if a == "--setenv" and i + 1 < len(sys.argv): senv = sys.argv[i + 1] + if a == "--controls" and i + 1 < len(sys.argv): controls = sys.argv[i + 1] + if controls is not None: + # pre-set L4CONTROLS and give SETENV a neutral first arg (anything but + # t/r keeps a pre-existing value -- see SETENV.BAT's CONTROLSDONE path) + senv = "k " + " ".join(senv.split()[1:]) + + root = os.path.join(STAGE, proddir) + for need in (f"{exe}.exe", "32RTM.EXE", f"{egg}.egg", "SETENV.BAT"): + if not any(f.lower() == need.lower() for f in os.listdir(root)): + raise SystemExit(f"{proddir}\\{need} not staged -- run " + f"stage_assets.py --game {proddir} first") + + with open(os.path.join(STAGE, "RUNSCN.BAT"), "w") as fp: + fp.write(f"cd \\{proddir}\r\n") + if controls is not None: + fp.write(f"set L4CONTROLS={controls}\r\n") + fp.write(f"call SETENV.BAT {senv}\r\n" + f"32RTM.EXE -x\r\n") + if "--netnub" in sys.argv: + # pod-network hub wrapper (BTNET.BAT): netnub hosts the wattcp + # net and launches the game with -net; -a forwards the egg args. + # It hard-requires a packet driver ("NO PACKET DRIVER FOUND"): + # load the drive's ODI stack against the emulated NE2000 + # (LSL -> NE2000 ODI -> ODIPKT shim on int 0x60), WATTCP.CFG + # gives the pod Milo's IP 200.0.0.86 from the egg + # DOSBox-X embeds the Crynwr NE2000 packet driver on Z: -- + # no ODI stack needed (the era-authentic LSL/ODIPKT combo's FTP + # shim can't see Novell MLIDs under DOSBox anyway) + fp.write("Z:\\SYSTEM\\NE2000.COM 0x60 3 0x300\r\n" + f"set WATTCP.CFG=C:\\{proddir}\r\n" + f"NETNUB.EXE -f {exe}.exe -a -egg {egg}.egg\r\n") + else: + fp.write(f"{exe}.exe -egg {egg}.egg\r\n") + fp.write(f"32RTM.EXE -u\r\n") + print(f"game: {proddir}\\{exe} -egg {egg}.egg setenv {senv}" + + (f" L4CONTROLS={controls}" if controls else "") + + f" (capture prefix: {prefix})") + + proc = [] + + def launch_dosbox(): + env = dict(os.environ, VRLINK='1') + p = subprocess.Popen([DOSBOX, "-conf", CONF], env=env, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + proc.append(p) + print(f"dosbox launched (pid {p.pid})") + + threading.Timer(2.0, launch_dosbox).start() + atexit.register(lambda: [p.kill() for p in proc if p.poll() is None]) + + sys.argv = ["vrrun.py", prefix, "--view"] + ( + ["--soft"] if "--soft" in sys.argv else []) + import vrrun + try: + vrrun.main() + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + main() diff --git a/dpl3-revive/patha/scan_scenes.py b/dpl3-revive/patha/scan_scenes.py new file mode 100644 index 0000000..0e4e717 --- /dev/null +++ b/dpl3-revive/patha/scan_scenes.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +scan_scenes.py -- inventory the .SCN files in sda4\HPDAVE: search paths, entities, +spline/event refs, and how many of the referenced objects resolve against surviving +files (checked with the shipped loader's extensions: .BGF/.BMF/.BSL/.TGA/.VTX, +plus .B2Z which staging exposes as .BGF). + + python scan_scenes.py +""" +import os, re, glob + +SRC = r"s:\OneDrive\Tesla III\DaveMcCoy\sda4" +HP = os.path.join(SRC, "HPDAVE") +LOAD_EXTS = ('.BGF', '.B2Z', '.BMF', '.BSL', '.VTX') + +def build_index(dirs): + idx = {} + for d in dirs: + if not os.path.isdir(d): + continue + for f in os.listdir(d): + base, ext = os.path.splitext(f.upper()) + if ext in LOAD_EXTS: + idx.setdefault(base, set()).add(ext) + return idx + +def main(): + for scn in sorted(glob.glob(os.path.join(HP, "*.SCN"))): + try: + text = open(scn, encoding='latin1').read() + except OSError: + continue + lines = [l.split('//')[0].strip() for l in text.splitlines()] + geo_paths, statics, dynamics, spls, other = [], [], [], [], [] + for l in lines: + if not l: + continue + t = l.split() + kw = t[0].upper() + if kw == 'GEOMETRY' and len(t) > 1: + geo_paths.append(t[1]) + elif kw == 'STATIC' and len(t) > 1: + statics.append(t[1].lower()) + elif kw in ('DYNAMIC', 'PATH') and len(t) > 1: + dynamics.append(t[1].lower()) + for x in t[2:]: + if x.lower().endswith('.spl'): + spls.append(x.lower()) + elif kw in ('SPECIALFX', 'SCROLL', 'NOVIEWMATRIX'): + if kw == 'NOVIEWMATRIX' and len(t) > 1: + statics.append(t[1].lower()) + # resolve search dirs relative to HPDAVE + dirs = [os.path.normpath(os.path.join(HP, p.replace('\\', os.sep))) + for p in geo_paths] + idx = build_index(dirs) + names = statics + dynamics + uniq = sorted(set(names)) + found = [n for n in uniq if n.upper().split(':')[-1] in idx] + missing = [n for n in uniq if n.upper().split(':')[-1] not in idx] + spl_ok = sum(1 for s in set(spls) if os.path.exists(os.path.join(HP, s))) + size = os.path.getsize(scn) + pct = (100 * len(found) // len(uniq)) if uniq else 0 + print(f"{os.path.basename(scn):14} {size:6}B paths={','.join(geo_paths) or '-'}") + print(f" entities: {len(names):3} ({len(uniq)} unique) resolvable: {len(found)}/{len(uniq)} ({pct}%)" + f" splines: {spl_ok}/{len(set(spls))}") + if missing: + print(f" missing: {', '.join(missing[:12])}{' ...' if len(missing) > 12 else ''}") + print() + print("note: resolvable = name found (any loadable ext) in the scene's own GEOMETRY search") + print(" dirs on the archive drive; staging exposes .B2Z as .BGF the same way.") + +if __name__ == '__main__': + main() diff --git a/dpl3-revive/patha/sdemo_bsl.png b/dpl3-revive/patha/sdemo_bsl.png new file mode 100644 index 0000000..34d9b84 Binary files /dev/null and b/dpl3-revive/patha/sdemo_bsl.png differ diff --git a/dpl3-revive/patha/sdemo_fidelity.png b/dpl3-revive/patha/sdemo_fidelity.png new file mode 100644 index 0000000..79acf22 Binary files /dev/null and b/dpl3-revive/patha/sdemo_fidelity.png differ diff --git a/dpl3-revive/patha/sdemo_render.png b/dpl3-revive/patha/sdemo_render.png new file mode 100644 index 0000000..f6ea1b0 Binary files /dev/null and b/dpl3-revive/patha/sdemo_render.png differ diff --git a/dpl3-revive/patha/sdemo_tex.png b/dpl3-revive/patha/sdemo_tex.png new file mode 100644 index 0000000..5192451 Binary files /dev/null and b/dpl3-revive/patha/sdemo_tex.png differ diff --git a/dpl3-revive/patha/sdemo_tex_orders.png b/dpl3-revive/patha/sdemo_tex_orders.png new file mode 100644 index 0000000..8fa668b Binary files /dev/null and b/dpl3-revive/patha/sdemo_tex_orders.png differ diff --git a/dpl3-revive/patha/sdemo_tex_perms.png b/dpl3-revive/patha/sdemo_tex_perms.png new file mode 100644 index 0000000..dd6a3fe Binary files /dev/null and b/dpl3-revive/patha/sdemo_tex_perms.png differ diff --git a/dpl3-revive/patha/sdemo_tex_planes.png b/dpl3-revive/patha/sdemo_tex_planes.png new file mode 100644 index 0000000..f288f9f Binary files /dev/null and b/dpl3-revive/patha/sdemo_tex_planes.png differ diff --git a/dpl3-revive/patha/sdemo_tex_slices.png b/dpl3-revive/patha/sdemo_tex_slices.png new file mode 100644 index 0000000..f685a01 Binary files /dev/null and b/dpl3-revive/patha/sdemo_tex_slices.png differ diff --git a/dpl3-revive/patha/sharks_check.png b/dpl3-revive/patha/sharks_check.png new file mode 100644 index 0000000..d03d624 Binary files /dev/null and b/dpl3-revive/patha/sharks_check.png differ diff --git a/dpl3-revive/patha/sharks_check2.png b/dpl3-revive/patha/sharks_check2.png new file mode 100644 index 0000000..4507d5b Binary files /dev/null and b/dpl3-revive/patha/sharks_check2.png differ diff --git a/dpl3-revive/patha/sharks_final.png b/dpl3-revive/patha/sharks_final.png new file mode 100644 index 0000000..9ecccd7 Binary files /dev/null and b/dpl3-revive/patha/sharks_final.png differ diff --git a/dpl3-revive/patha/sharks_sfx.png b/dpl3-revive/patha/sharks_sfx.png new file mode 100644 index 0000000..8d5ec3f Binary files /dev/null and b/dpl3-revive/patha/sharks_sfx.png differ diff --git a/dpl3-revive/patha/stage_assets.py b/dpl3-revive/patha/stage_assets.py new file mode 100644 index 0000000..ab84df9 --- /dev/null +++ b/dpl3-revive/patha/stage_assets.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +""" +stage_assets.py -- build the staged C: tree for running the shipped FLYK.EXE +(c:\\temp\\flykc, mounted by flyk_vr.conf). + +Why staging exists (found 2026-07-04, session 2): + - HPDAVE\\SHARKS.SCN searches ..\\geometry, ..\\sharks, ..\\texture -- none of + which exist beside sda4\\HPDAVE. FLYK builds an empty world (object=NULL on + every instance) and uploads no geometry over the link. + - The shipped FLYK registers object-load extensions .bgf/.bmf/.bsl/.tga/.vtx + (NOT .b2z/.svt), but its dpl_bgfRead reads the same DIV-BIZ2 container the + .B2Z files hold. A silent load failure was simply "ocean.bgf not found". + - So: stage DPL3\\GEOMETRY\\*.B2Z as GEOMETRY\\*.BGF, and decode + DPL3\\TEXTURE\\*.SVT (raw xBGR 32-bit, square) to TEXTURE\\*.TGA. + + python stage_assets.py [dest] # default c:\\temp\\flykc + python stage_assets.py --game RPDAVE # add the MUNGA game tree (MAPS/ + # MODELS/SOLIDS/... + 32RTM from BTDAVE) + # to an already-staged product dir +""" +import os, shutil, stat, struct, sys + + +def cp(src, dst): + """copy2 that tolerates a read-only destination (the archive drive's RO + attribute is preserved by copy2, which breaks restaging).""" + if os.path.exists(dst): + os.chmod(dst, stat.S_IWRITE) + shutil.copy2(src, dst) + +SRC = r"s:\OneDrive\Tesla III\DaveMcCoy\sda4" +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(os.path.dirname(HERE), "parser")) +import svt + + +def write_tga(path, edge, texels): + """Uncompressed 24-bit type-2 TGA from SVT texels (file order x,B,G,R).""" + hdr = struct.pack(' s:\resource\BT\l4\video.zip) and is empty of the video\geo|mat|tex + # trees its BTDPL.INI searches -- BTLIVE (the pod's live snapshot) has the + # full 1585-file set, but its BTL4.RES is older than the exe accepts + # ("btl4.res v1.0.1.2 is obsolete!"). So: BTDAVE build + BTLIVE video. + if name == "BTDAVE": + n = cptree(os.path.join(SRC, "BTLIVE", "VIDEO"), + os.path.join(out, "VIDEO")) + print(f"{name}\\VIDEO: +{n} files grafted from BTLIVE") + # 32RTM refuses to start without its DPMI overlay on the path + # ("Can't find DPMI32VM.OVL" -- first MUNGA boot attempt) + for f in ("32RTM.EXE", "DPMI32VM.OVL"): + cp(os.path.join(SRC, "BTDAVE", f), os.path.join(out, f)) + print(f"{name}\\{f} (borrowed from BTDAVE)") + + +def main(): + if "--game" in sys.argv: + i = sys.argv.index("--game") + name = sys.argv[i + 1] if i + 1 < len(sys.argv) else "RPDAVE" + stage_game(name.upper(), r"c:\temp\flykc") + return + dst = sys.argv[1] if len(sys.argv) > 1 else r"c:\temp\flykc" + for d in ("HPDAVE", "GEOMETRY", "TEXTURE", "SHARKS"): + os.makedirs(os.path.join(dst, d), exist_ok=True) + + # HPDAVE: top-level files (FLYK.EXE, SCN/SPL/EVT, BTL, MNG, BATs, TGAs) + hp = os.path.join(SRC, "HPDAVE") + n = 0 + for f in os.listdir(hp): + s = os.path.join(hp, f) + if os.path.isfile(s): + cp(s, os.path.join(dst, "HPDAVE", f)); n += 1 + print(f"HPDAVE: {n} files") + + # HPDAVE\VIDEO: the intact asset library most HPDAVE scenes point at + # (GEOMETRY .\video) -- real .BGF/.BSL/.TGA/.VTX/.BMF content + vid = os.path.join(hp, "VIDEO") + os.makedirs(os.path.join(dst, "HPDAVE", "VIDEO"), exist_ok=True) + n = 0 + for f in os.listdir(vid): + s = os.path.join(vid, f) + if os.path.isfile(s): + cp(s, os.path.join(dst, "HPDAVE", "VIDEO", f)); n += 1 + print(f"HPDAVE\\VIDEO: {n} files") + + # Other products (Star Trek, Red Planet, ...): their scenes use ABSOLUTE + # search paths (\STDAVE\VIDEO, \RPDAVE\VIDEO), so each stages at the mount + # root with its VIDEO dir; FLYK.EXE + firmware + SETSVGA.BAT are copied in + # from HPDAVE so scenes run from \ with the known-good runner. + def stage_product(name, extra_dirs=()): + src = os.path.join(SRC, name) + os.makedirs(os.path.join(dst, name, "VIDEO"), exist_ok=True) + n = 0 + for f in os.listdir(src): + s = os.path.join(src, f) + if os.path.isfile(s): + cp(s, os.path.join(dst, name, f)); n += 1 + for sub in ("VIDEO",) + tuple(extra_dirs): + sd = os.path.join(src, sub) + if not os.path.isdir(sd): + continue + for f in os.listdir(sd): + s = os.path.join(sd, f) + if os.path.isfile(s): + cp(s, os.path.join(dst, name, "VIDEO", f)); n += 1 + # scene files from SCENES/MAPS land beside the runner for easy launch. + # SCENES takes precedence: MAPS holds same-named files in a key=value + # dialect FLYK's scene parser cannot read (found via MAPS\BLADE.SCN). + for sub in ("SCENES", "MAPS"): + sd = os.path.join(src, sub) + if not os.path.isdir(sd): + continue + for f in os.listdir(sd): + s = os.path.join(sd, f) + d = os.path.join(dst, name, f) + if (os.path.isfile(s) and not os.path.exists(d) + and f.upper().endswith((".SCN", ".SPL", ".EVT"))): + cp(s, d); n += 1 + for f in ("FLYK.EXE", "VRENDMON.BTL", "VREND.MNG", "VRNOSTEX.MNG", + "SETSVGA.BAT", "JOYSTICK.CAL"): + cp(os.path.join(hp, f), os.path.join(dst, name, f)); n += 1 + print(f"{name}: {n} files (+ FLYK runner)") + + stage_product("STDAVE") + stage_product("RPDAVE") + stage_product("BTDAVE") # BattleTech (FXTEST.SCN etc. at product root) + stage_product("FX") # effects sandbox: \FX\VIDEO = Thor mech, arena, + # explosion disks; .SPL/.EVT/.PFX at \FX root + + # default scene selector (flyk_vr.conf autoexec does `call C:\RUNSCN.BAT`; + # run_demo.py rewrites this to pick a product dir / exe / scene) + bat = os.path.join(dst, "RUNSCN.BAT") + if not os.path.exists(bat): + with open(bat, "w") as fp: + fp.write("cd \\HPDAVE\r\ncall SETSVGA.BAT\r\nflyk .\\SHARKS.SCN\r\n") + print("RUNSCN.BAT -> HPDAVE flyk SHARKS.SCN (default)") + + # GEOMETRY: DPL3\GEOMETRY, with .B2Z additionally exposed as .BGF + ge = os.path.join(SRC, "DPL3", "GEOMETRY") + n = b = 0 + for f in os.listdir(ge): + s = os.path.join(ge, f) + if not os.path.isfile(s): + continue + cp(s, os.path.join(dst, "GEOMETRY", f)); n += 1 + if f.upper().endswith(".B2Z"): + cp(s, os.path.join(dst, "GEOMETRY", f[:-4] + ".BGF")); b += 1 + print(f"GEOMETRY: {n} files, {b} exposed as .BGF") + + # TEXTURE: DPL3\TEXTURE .SVT decoded to .TGA (+ originals for reference) + te = os.path.join(SRC, "DPL3", "TEXTURE") + n = 0 + for f in os.listdir(te): + s = os.path.join(te, f) + if not (os.path.isfile(s) and f.upper().endswith(".SVT")): + continue + try: + data, edge = svt.load(s) + except ValueError as e: + print(f" skip {f}: {e}"); continue + write_tga(os.path.join(dst, "TEXTURE", f[:-4] + ".TGA"), edge, data) + n += 1 + print(f"TEXTURE: {n} SVT -> TGA") + + +if __name__ == "__main__": + main() diff --git a/dpl3-revive/patha/test_view.py b/dpl3-revive/patha/test_view.py new file mode 100644 index 0000000..ac3b878 --- /dev/null +++ b/dpl3-revive/patha/test_view.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +""" +test_view.py -- offline smoke test: replay a captured FLYK run through the +virtual board, then render one frame with vrview and save it as a PNG. + + python test_view.py [cap5.raw.bin] [frames_into_run] [out.png] + +`frames_into_run` picks how many 0x1d/draw messages to replay before rendering +(the shark swims, so later = different pose). +""" +import os, sys, struct +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +os.environ.setdefault('SDL_VIDEODRIVER', 'dummy') # offscreen pygame +from vrboard import VirtualBoard, Assembler +from decode_anim import find_framed_start + + +def main(): + path = sys.argv[1] if len(sys.argv) > 1 else 'cap5.raw.bin' + upto = int(sys.argv[2]) if len(sys.argv) > 2 else 600 + out = sys.argv[3] if len(sys.argv) > 3 else 'flyk_live_render.png' + + data = open(path, 'rb').read() + board = VirtualBoard() + asm = Assembler(); asm.feed(data[find_framed_start(data):]) + for m in asm: + try: + board.handle(m) + except Exception as e: + print('board error:', e) + if board.frames >= upto: + break + print(f'replayed to frame {board.frames}: nodes={len(board.nodes)} ' + f'uploads={len(board.uploads)} conns={len(board.conns)} anim={len(board.anim)}') + + from vrview import Renderer + r = Renderer() + r.draw(board) + r.cache.maybe_rebuild(board) + print(f'instances renderable: {len(r.cache.instances)}') + print('cam chain:', [hex(h) for h in r.cache.cam_chain]) + r.pygame.image.save(r.screen, out) + print('wrote', out) + + +if __name__ == '__main__': + main() diff --git a/dpl3-revive/patha/verify_framing.py b/dpl3-revive/patha/verify_framing.py new file mode 100644 index 0000000..3b71d2a --- /dev/null +++ b/dpl3-revive/patha/verify_framing.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +verify_framing.py -- static check that the SHIPPED FLYK.EXE frames the VelociRender +protocol exactly the way vrboard.py assumes (framing derived from VR_COMMS.C source). + +Confirms, inside the real binary's code object: + * the length-word broadcast mask (0xff<<16) = 0x00FF0000 (velocirender_transmit) + * the iserver control bit 0x80000000 (send_link_protocol) + * the packetize chunk size 512-4 = 508 = 0x1FC (velocirender_packetize) + * C012 link-adapter port I/O IN/OUT via DX (LINKIO.C) + * the default link base 0x150 (getXputer) + +Usage: python verify_framing.py /path/to/FLYK.EXE +""" +import sys +from leimage import LE +try: + from capstone import Cs, CS_ARCH_X86, CS_MODE_32 +except ImportError: + Cs = None + +def le32(v): return bytes([v & 0xff, (v>>8)&0xff, (v>>16)&0xff, (v>>24)&0xff]) + +def scan(img, needle): + hits, i = [], img.find(needle) + while i != -1: + hits.append(i); i = img.find(needle, i+1) + return hits + +def main(path): + le = LE(path) + base, img = le.image(1) # code object + print(f"code object: base={base:#x} size={len(img):#x} ({len(img)} bytes)\n") + + checks = { + "length-word mask 0x00FF0000 (broadcast id | count)": le32(0x00FF0000), + "iserver control bit 0x80000000": le32(0x80000000), + "packetize chunk 0x000001FC (512-4)": le32(0x000001FC), + "default link base 0x00000150 (getXputer)": le32(0x00000150), + } + results = {} + for label, needle in checks.items(): + hits = scan(img, needle) + results[label] = hits + mark = "OK " if hits else "-- " + where = ("VA " + ", ".join(f"{base+h:#x}" for h in hits[:4]) + + (" ..." if len(hits) > 4 else "")) if hits else "not found as literal" + print(f"[{mark}] {label:52} {len(hits):3} hit(s) {where}") + + # x86 port I/O: IN/OUT with DX (EC in AL,DX / ED in eAX,DX / EE out DX,AL / EF out DX,eAX) + portio = {op: img.count(bytes([op])) for op in (0xEC,0xED,0xEE,0xEF)} + print(f"\nport-I/O opcodes (raw byte freq, upper bound): " + f"IN AL,DX={portio[0xEC]} IN eAX,DX={portio[0xED]} " + f"OUT DX,AL={portio[0xEE]} OUT DX,eAX={portio[0xEF]}") + + # Disassemble a window around the first length-word-mask hit to show the OR that builds it. + if Cs and results[next(iter(results))]: + h = results["length-word mask 0x00FF0000 (broadcast id | count)"][0] + md = Cs(CS_ARCH_X86, CS_MODE_32) + lo = max(0, h-24) + print(f"\n--- disasm around length-word construction (VA {base+lo:#x}) ---") + for ins in md.disasm(img[lo:h+16], base+lo): + print(f" {ins.address:#010x} {ins.mnemonic:6} {ins.op_str}") + elif not Cs: + print("\n(capstone not available -- byte-literal scan only)") + + print("\nVERDICT:", "framing constants present -> shipped FLYK matches the source-derived protocol" + if results["iserver control bit 0x80000000"] and + results["length-word mask 0x00FF0000 (broadcast id | count)"] + else "framing constants NOT all found as literals -- inspect (may be computed)") + +if __name__ == '__main__': + main(sys.argv[1]) diff --git a/dpl3-revive/patha/vrboard.py b/dpl3-revive/patha/vrboard.py new file mode 100644 index 0000000..210b471 --- /dev/null +++ b/dpl3-revive/patha/vrboard.py @@ -0,0 +1,575 @@ +#!/usr/bin/env python3 +""" +vrboard.py -- virtual VelociRender board (protocol core for Path A). + +This is the piece the DOSBox-X C012 device wraps: it consumes the byte stream the +DOS host (FLYK.EXE via VR_COMMS.C) writes to the link, reassembles messages, dispatches +the 24 vr_actions, and produces reply bytes. The rendering guts (geometry/texture +ingest, DPL transform, raster) plug in where marked TODO -- those are the dpl3-revive +parser/viewer pipeline. + +Framing is implemented straight from sda4/DPL3/VR_COMMS.C and VR_PROT.H. +See spec/VELOCIRENDER_PROTOCOL.md. + +Run directly for a self-test that round-trips a realistic command sequence: + python vrboard.py +""" +import struct +from enum import IntEnum + +# ---- vr_action enum (VR_PROT.H, positional/0-based) ------------------------- +class A(IntEnum): + init=0; create=1; delete=2; flush=3 + sect_pixel=4; sect_vector=5 + dcs_nest=6; dcs_link=7; dcs_prune=8 + draw_scene=9; draw_scene_complete=10 + list_add=11; list_remove=12 + morph=13; version=14; statistics=15; readpixels=16 + hspcode=17; code860=18; data860=19; bss860=20; args860=21 + set_geom_verts=22; set_texmap_texels=23 + +BOOT_ACTIONS = {A.hspcode, A.code860, A.data860, A.bss860, A.args860} + +# ---- envelope (VR_COMMS.C velocirender_transmit / receive_protocol) ---------- +# length word = [flags : top bits][id : bits 16-23][count : low 16] +# count = bytes following the length word (= 4 action + N payload for vr_net) +# bit31 = iserver control message (else vr_net render command) +# bit30 = 0x40000000: set on EVERY length word by the shipped Aug-1995 HPDAVE build +# (verify_framing.py found `or ebp,0x40000000` in FLYK's transmit + a global +# pre-set to 0x40000000). Absent in the 1994 VR_COMMS.C source. Exact receive- +# side semantics TBD (confirm in the dynamic capture); we MIRROR it so our +# replies are byte-identical to what FLYK's board would send. +LW_FLAG = 0x40000000 + +def pack_vr(action, payload=b''): + lw = LW_FLAG | (0xff << 16) | (len(payload) + 4) # broadcast id 0xff + return struct.pack('" + return f"<{A(self.action).name:16} {len(self.payload):4}B>" + +class Assembler: + """Byte stream (host->board) -> complete Msg objects. Models the C012 input FIFO.""" + def __init__(self): + self.buf = bytearray() + def feed(self, data): + self.buf += data + def __iter__(self): + return self + def __next__(self): + if len(self.buf) < 4: + raise StopIteration + (lw,) = struct.unpack_from('> 16) & 0xff + nb = lw & 0xffff # bytes following the length word + if len(self.buf) < 4 + nb: + raise StopIteration # wait for more bytes + body = bytes(self.buf[4:4 + nb]) + del self.buf[:4 + nb] + if iserver: + return Msg(True, sender, None, body) + action = struct.unpack_from(' {'type','body'} + self.edges = [] # (op, parent, child) scene-graph log + self.geom = {} # handle -> list of raw vertex/conn blocks + self.tex = {} # handle -> {'u','v','mode','texels'} + self._geom_acc = None # in-progress set_geom_verts + self._tex_acc = None # in-progress action-23 upload + self._conn_acc = None # in-progress action-0x19 upload + self._texel_acc = None # in-progress action-0x1a upload + self.uploads = {} # handle -> [ {hdr, scale, data} ] + self.conns = {} # handle -> [ {n_conns, per, flags, data} ] + self.anim = {} # handle -> latest 0x1d (9f mat + 3f t, COMPOSES) + self.anim_abs = {} # handle -> latest 0x1f record (REPLACES) + self.anim4 = {} # handle -> latest 0x1b (16f 4x4) + self.morphs = {} # morphed geom -> (a, b, alpha) + self.names = {} # 0x8000xxxx name id -> handle (0x22) + self.sfx = {} # SPECIALFX defs by code (0x1c) + self.munga = False # game-build dialect (rpl4opt/btl4opt) + self.frames = 0 + self.log = [] + + def _alloc(self): + h = self.next_handle; self.next_handle += 1; return h + + def handle(self, msg): + """Process one Msg; return reply bytes (b'' = no reply).""" + if msg.iserver: + self.log.append(f"iserver {len(msg.payload)}B (serviced, discarded)") + return b'' + try: + a = A(msg.action) + except ValueError: + # shipped build has actions beyond the 1994 enum. 0x2a is the velocirender_sync + # barrier -- it expects an echo reply. Others (e.g. 0x1c, an 80B data op) are + # fire-and-forget; replying to them desyncs the next sync. + if msg.action in (0x2a, 0x2d): + # velocirender_sync barrier: 0x2a in the FLYK demos, 0x2d in the + # MUNGA game builds. Disasm of rpl4opt's vr_sync (hunt_pe.py, + # 0x497080): reply ACTION is never checked -- the reply PAYLOAD + # word0 must echo the request's word0 (a sync cookie; cmp ebx, + # [ebp-0x28] / exit(9) on mismatch). FLYK's 0x2a cookie is 0, so + # the old zero reply was accidentally correct. + # 0x2d is also the MUNGA dialect marker -- the init argv is + # IDENTICAL between builds ('/device~0x150~...' tildes included), + # so argv sniffing misclassified FLYK and yawed every camera. + if msg.action == 0x2d and not self.munga: + self.munga = True + self.log.append("[MUNGA dialect: sync 0x2d seen]") + cookie = msg.payload[:4] if len(msg.payload) >= 4 else b'\x00' * 4 + self.log.append(f"sync {msg.action:#x} cookie={cookie.hex()} -> ack") + return (struct.pack('= 56: + # per-frame transform (shipped flush_artics): + # [dcs_type=1 matrix][dcs handle][3x3 row-major][tx ty tz] + h = struct.unpack_from('= 8: + # MUNGA batched flush_artics: [n_records] then n records of + # [dcs handle][payload], where payload is EITHER a full pose + # (12f: 3x3 row-major + t, ABSOLUTE -- replaces the flush + # matrix; composing doubles it) OR a joint angle (sin,cos: + # 2f in BTL4 v1.1, 5f sin,cos,0,0,0 in RPL4 v1.2). Record + # sizes are version-dependent, so parse by BACKTRACKING: every + # record starts with a valid DCS handle, and float bit + # patterns don't collide with small handle ints. (A fixed- + # size guess corrupted mid-packet records -- BT camera chain + # picked up det=0 matrices and rendered nothing.) + p = msg.payload + n = min(struct.unpack_from(' len(p): + return None + h = struct.unpack_from(' len(p): + continue + rest = parse(end, k - 1) + if rest is not None: + return [(h, off + 4, nf)] + rest + return None + + recs = parse(4, n) + for h, off, nf in recs or (): + if nf == 12: + self.anim_abs[h] = struct.unpack_from('<12f', p, off) + # joint sin/cos records: axis semantics unknown -- the + # flushed matrix stands (mech limbs won't articulate yet) + return b'' + if msg.action == 0x1c and len(msg.payload) >= 64: + # install_sfx: SPECIALFX def, fields per the .SCN column comments: + # [code][texture ref][type][velocity][size][off_y][bias] + # [cook_r][cook_g][cook_b][variance][gravity][o_cool][cool] + # [frags][rpt][host ptrs...] + w = struct.unpack_from('<3I', msg.payload, 0) + f = struct.unpack_from('<11f', msg.payload, 12) + fr, rpt = struct.unpack_from('<2I', msg.payload, 56) + self.sfx[w[0]] = { + 'tex': w[1], 'type': w[2], 'velocity': f[0], 'size': f[1], + 'off_y': f[2], 'bias': f[3], 'cook': f[4:7], 'variance': f[7], + 'gravity': f[8], 'o_cool': f[9], 'cool': f[10], + 'frags': fr, 'rpt': rpt} + self.log.append(f"install_sfx code={w[0]} type={w[2]} " + f"vel={f[0]:.2f} frags={fr} rpt={rpt}") + return b'' + if msg.action == 0x18 and len(msg.payload) >= 12: + return self.do_get_geom_vertices(msg.payload) + if msg.action == 0x23 and len(msg.payload) >= 8: + # fire/pick at screen (u,v) -- observed on joystick trigger: + # [u:f][v:f][host ptr][-1]. EXPERIMENT: reply with the instance + # under the crosshair (sect-style); may drive the damage-FX chain. + u, v = struct.unpack_from('<2f', msg.payload, 0) + h = 0 + view = getattr(self, '_view', None) + if view is not None: + try: + h = view.pick_screen(self, u, v) + except Exception as e: + self.log.append(f"0x23 pick error: {e}") + self.log.append(f"fire/pick 0x23 ({u:.2f},{v:.2f}) -> {h:#x}") + # CONFIRMED fire-and-forget: replying desyncs the host stream + # (fxfire run stalled immediately after the first reply, while + # unanswered 0x23s in the prior run were harmless). The pick + # result stays board-side telemetry only. + return b'' + if msg.action == 0x22 and len(msg.payload) >= 8: + # name binding: [handle][0x8000xxxx board-side name id] + # (board createName; fire-and-forget) + h, nm = struct.unpack_from('= 72: + # per-frame full-matrix DCS update: [zone][dcs][4x4 row-major] + # (SDEMO; fire-and-forget; row-3 junk words = uninitialized host mem) + h = struct.unpack_from('= 8 else self._alloc() + self.nodes[h] = {'type': typ, 'body': b''} + self.log.append(f"create type={typ:#x} handle={h:#x} (no reply)") + return b'' + def do_delete(self, p): + h = struct.unpack_from('remote + n = self.nodes.setdefault(h, {'type': None, 'body': b''}); n['body'] = p[4:] + self.log.append(f"flush handle {h:#x} ({len(p)-4}B struct body)") + return b'' # no reply + + # -- scene graph plumbing -- + def _edge(self, op, p): + x, y = struct.unpack_from('= acc['need']: + self.geom[acc['h']] = acc['blocks'] + self.log.append(f" geometry {acc['h']:#x} complete ({len(acc['blocks'])} blocks) -> ack") + self._geom_acc = None + return pack_vr(A.set_geom_verts, struct.pack('= acc['need']: + self.uploads.setdefault(acc['h'], []).append(acc) + self._tex_acc = None + # MUNGA bulk ops are ack-less: the host follows each with + # vr_sync(cookie=action) and the cookie echo IS the ack (an extra + # 0x1a ack surfaced as "unexpected action 26 in velocirender_sync") + if self.munga: + self.log.append(f" upload23 {acc['h']:#x} complete " + f"({len(acc['data'])}B) [no ack: MUNGA syncs]") + return b'' + self.log.append(f" upload23 {acc['h']:#x} complete ({len(acc['data'])}B) -> ack") + return pack_vr(A.set_texmap_texels, struct.pack('= acc['need']: + self.conns.setdefault(acc['h'], []).append(acc) + self._conn_acc = None + if self.munga: # see upload23: sync IS the ack + self.log.append(f" conn19 {acc['h']:#x} complete " + f"({len(acc['data'])}B) [no ack: MUNGA syncs]") + return b'' + self.log.append(f" conn19 {acc['h']:#x} complete ({len(acc['data'])}B) -> ack") + return pack_vr(0x19, struct.pack('= acc['need']: + self.tex[acc['h']] = acc + self._texel_acc = None + if self.munga: # see upload23: sync IS the ack + self.log.append(f" texel1a {acc['h']:#x} complete " + f"({len(acc['data'])}B) [no ack: MUNGA syncs]") + return b'' + self.log.append(f" texel1a {acc['h']:#x} complete ({len(acc['data'])}B) -> ack") + return pack_vr(0x1a, struct.pack('b + # into the morphed geometry (SDEMO fish schools, .V2Z). Fire-and-forget. + def do_morph(self, p): + if len(p) >= 20: + mo, a, b = struct.unpack_from('<3I', p, 0) + alpha = struct.unpack_from(' frame {self.frames}, async ack") + return pack_vr(A.draw_scene, struct.pack(' pixel words from the last rendered frame, + # fragmented into <=508B action-echoed messages (mirrors upload + # packetization). Texel word convention [pad,B,G,R]. EXPERIMENTAL: + # no shipped binary has exercised this path yet. + view = getattr(self, '_view', None) + if len(p) < 12 or view is None or getattr(view, 'last_frame', None) is None: + self.log.append("readpixels: no frame available -> zero pixels") + return pack_vr(A.readpixels, b'\x00\x00\x00\x00') + x, y, n = struct.unpack_from('<3I', p, 0) + arr = view.last_frame # (H, W, 3) uint8 + H, W = arr.shape[:2] + flat = arr.reshape(-1, 3) + start = min(y * W + x, len(flat)) + n = min(n, len(flat) - start) + px = flat[start:start + n] + words = bytearray(n * 4) + words[1::4] = px[:, 2].tobytes() # B + words[2::4] = px[:, 1].tobytes() # G + words[3::4] = px[:, 0].tobytes() # R + out = bytearray() + for off in range(0, len(words), 508): + out += pack_vr(A.readpixels, bytes(words[off:off + 508])) + self.log.append(f"readpixels {x},{y} n={n} -> {len(out)}B in fragments") + return bytes(out) + + # -- action 0x18 = get_geom_vertices (host reads vertex data back) -- + # Located by LE-fixup + capstone hunt in shipped FLYK: transmit with + # eax=0x18, 0x28-byte request, reply MUST echo action 0x18; host receives + # into a 0x100-byte buffer per chunk. Request word0 = geometry handle, + # then vertex0/count-style fields. Reply = echoed-action frames carrying + # the geometry's current vertex floats (<=240B data per frame). + # EXPERIMENTAL until a binary calls it live. + def do_get_geom_vertices(self, p): + w = struct.unpack_from('<%dI' % (len(p) // 4), p, 0) + gh = w[0] + ups = self.uploads.get(gh) or [] + if not ups: + self.log.append(f"get_geom_vertices {gh:#x}: no data -> empty reply") + return pack_vr(0x18, b'') + stride = ups[-1]['hdr'][3] + total = 0 + for up in ups: + h = up['hdr'] + total = max(total, h[1] + h[2], h[7]) + buf = bytearray(total * stride * 4) + for up in ups: + h = up['hdr'] + if h[3] != stride: + continue + n = min(h[2] * stride * 4, len(up['data'])) + buf[h[1] * stride * 4: h[1] * stride * 4 + n] = up['data'][:n] + # optional sub-range: request words 1/2 as vertex0/count when sane + v0 = w[1] if len(w) > 1 and w[1] < total else 0 + cnt = w[2] if len(w) > 2 and 0 < w[2] <= total - v0 else total - v0 + data = bytes(buf[v0 * stride * 4:(v0 + cnt) * stride * 4]) + out = bytearray() + for off in range(0, len(data), 240): + out += pack_vr(0x18, data[off:off + 240]) + if not out: + out = pack_vr(0x18, b'') + self.log.append(f"get_geom_vertices {gh:#x} v0={v0} n={cnt} " + f"stride={stride} -> {len(out)}B") + return bytes(out) + + # -- intersection queries: ray-triangle over the live scene (via the view) -- + # 1994 payloads: sect_pixel = dpl_POINT[4f] + screen x,y (24B); + # sect_vector = dpl_POINT[4f] + x0 y0 z0 x1 y1 z1 (40B). + # Reply = int32 instance handle (0 = miss). Games use these for collision. + def do_sect_vector(self, p): + h = 0 + view = getattr(self, '_view', None) + if view is not None and len(p) >= 40: + f = struct.unpack_from('<6f', p, 16) + o = f[0:3] + d = (f[3] - f[0], f[4] - f[1], f[5] - f[2]) + try: + h = view.pick(self, o, d) + except Exception as e: + self.log.append(f"sect_vector pick error: {e}") + self.log.append(f"sect_vector -> instance {h:#x}") + return pack_vr(A.sect_vector, struct.pack('= 24: + x, y = struct.unpack_from('<2f', p, 16) + u = x / 832.0 if abs(x) > 1.5 else x + v = y / 512.0 if abs(y) > 1.5 else y + try: + h = view.pick_screen(self, u, v) + except Exception as e: + self.log.append(f"sect_pixel pick error: {e}") + self.log.append(f"sect_pixel ({x:.1f},{y:.1f}) -> {h:#x}") + return pack_vr(A.sect_pixel, struct.pack(' board wire bytes + asm.feed(raw) + for msg in asm: + r = board.handle(msg) + if r: replies.append((msg, r)) + + # boot firmware (discarded) + init + host_send(pack_vr(A.code860, b'\x00' * 40)) + host_send(pack_vr(A.init, b'video=svga|qual=0x14|\n\x00')) + # build a tiny scene: a DCS (transform) with a 4x4 matrix, a geometry node + host_send(pack_vr(A.create, struct.pack(' reply {A(act).name} ({len(r)-8}B payload)") + # sanity assertions + assert board.frames == 1 + assert dcs_handle in board.nodes and board.nodes[dcs_handle]['body'] == identity + assert geo_handle in board.geom and len(board.geom[geo_handle]) == 4 + assert ('nest', dcs_handle, geo_handle) in board.edges + assert asm.buf == bytearray(), "assembler left partial bytes" + print("\nOK: framing round-trips, handles/flush/geometry/scene-graph all consistent.") + +if __name__ == '__main__': + _selftest() diff --git a/dpl3-revive/patha/vrcapture.py b/dpl3-revive/patha/vrcapture.py new file mode 100644 index 0000000..83bf518 --- /dev/null +++ b/dpl3-revive/patha/vrcapture.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +""" +vrcapture.py -- quiet raw capture board for the first live FLYK run. + +Listens where the DOSBox-X vrlink C012 bridge connects (127.0.0.1:8620), logs every +host->board byte to capture.raw.bin (+ an annotated hex log), and replies NOTHING +(input-status stays 0). FLYK therefore streams its whole boot -- transputer .BTL, then +framed vr_860code/data/bss/args, then vr_init -- without the stock garbage-read error, +and we record the exact bytes to design the responder's boot handling against. + + python vrcapture.py # listen; Ctrl-C or kill to stop +""" +import socket, sys, time + +HOST, PORT = '127.0.0.1', 8620 +BIN = "capture.raw.bin" +HEX = "capture.hex.log" + +def main(): + binf = open(BIN, "wb") + hexf = open(HEX, "w") + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind((HOST, PORT)); srv.listen(1) + print(f"vrcapture listening on {HOST}:{PORT} -> {BIN} / {HEX}") + conn, addr = srv.accept() + print(f"vrlink connected from {addr}") + total = 0 + last = time.time() + with conn: + conn.setblocking(True) + conn.settimeout(2.0) + while True: + try: + data = conn.recv(4096) + except ConnectionResetError: + print("(peer reset the connection -- FLYK closed/exited)") + break + except socket.timeout: + # note idle gaps -- they mark phase boundaries (BTL end / read waits) + if total: + hexf.write(f"# --- idle {time.time()-last:.1f}s at offset {total} ---\n"); hexf.flush() + continue + if not data: + break + binf.write(data); binf.flush() + # annotated hex: offset + first bytes of each chunk + head = ' '.join(f"{b:02x}" for b in data[:16]) + hexf.write(f"[{total:7}] +{len(data):-5} {head}{' ...' if len(data)>16 else ''}\n") + hexf.flush() + total += len(data) + last = time.time() + print(f"connection closed; captured {total} bytes -> {BIN}") + +if __name__ == '__main__': + main() diff --git a/dpl3-revive/patha/vrrun.py b/dpl3-revive/patha/vrrun.py new file mode 100644 index 0000000..a1094d3 --- /dev/null +++ b/dpl3-revive/patha/vrrun.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +vrrun.py -- live responding board for the custom-DOSBox-X FLYK run. + +Phase machine matched to what disassembly of shipped FLYK proved: + + BOOT : swallow the raw transputer .BTL stream (not framed). When it ends + (idle gap after a substantial burst), send ONE vr_net frame -- this is + all start_velocirender's velocirender_input() needs to stop timing out + (it loops on iserver msgs, returns on the first non-iserver frame with + count >= 8; the content is ignored). No iserver tag => no exit(666) risk. + + FRAMED : feed everything to vrboard -- discards the framed vr_860* MNG download, + replies to vr_init, then handles the scene (create/flush/geometry/ + textures/draw). Every message + reply is logged. + +Raw bytes also go to capture.raw.bin / capture.hex.log for offline analysis. + + python vrrun.py [prefix] # listen 127.0.0.1:8620; Ctrl-C to stop + # writes .raw.bin / .log + python vrrun.py [prefix] --view # + real-time board-side window: moderngl GPU + # backend (vrview_gl.py, 832x512) by default, + # renders each draw_scene from live wire state, + # ack paced to <=60 Hz so FLYK runs real-time + python vrrun.py [prefix] --view --soft # software rasterizer (vrview.py, + # the debugging reference; also VRVIEW_SOFT=1) +""" +import os, socket, struct, time, sys +from vrboard import VirtualBoard, Assembler, pack_vr, A + +def make_renderer(soft=False): + """GPU backend by default; --soft / VRVIEW_SOFT=1 or any GL failure falls + back to the software rasterizer (the reference implementation).""" + if not soft and os.environ.get('VRVIEW_SOFT') != '1': + try: + from vrview_gl import GLRenderer + return GLRenderer() + except Exception as e: + print(f"[view] GPU backend unavailable ({e}) -- software fallback") + from vrview import Renderer + return Renderer() + +HOST, PORT = '127.0.0.1', 8620 +RSET = b'\x00RSET' +IDLE = 0.5 # seconds of no writes => a phase boundary +MIN_BOOT = 4096 # bytes received before we treat an idle as "BTL done" + +def main(): + args = [a for a in sys.argv[1:] if not a.startswith("--")] + prefix = args[0] if args else "capture" + view = None + if "--view" in sys.argv: + view = make_renderer("--soft" in sys.argv) + binf = open(prefix + ".raw.bin", "wb") + log = open(prefix + ".log", "w") + def emit(s): log.write(s + "\n"); log.flush(); print(s) + + board = VirtualBoard(); asm = Assembler() + board._view = view + phase = "BOOT"; recv_since_reset = 0; handshake_sent = False + + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind((HOST, PORT)); srv.listen(1) + emit(f"vrrun listening on {HOST}:{PORT}") + conn, addr = srv.accept(); emit(f"vrlink connected from {addr}") + conn.settimeout(IDLE) + + def send_handshake(): + msg = pack_vr(A.init, b'\x00' * 8) # vr_net, count = 12 (action+8) -- clears the >=8 check + conn.sendall(msg) + emit(f"[BOOT] BTL done ({recv_since_reset}B). Sent 1 vr_net handshake frame " + f"({len(msg)}B) -> velocirender_input should succeed.") + + with conn: + while True: + try: + data = conn.recv(8192) + except socket.timeout: + if phase == "BOOT" and recv_since_reset > MIN_BOOT and not handshake_sent: + send_handshake(); handshake_sent = True; phase = "FRAMED" + if view is not None: + view.pump() # keep the window alive while the link idles + continue + except ConnectionResetError: + emit("(peer reset -- FLYK closed)"); break + if not data: + break + binf.write(data); binf.flush() + + # peel RSET control frames (vrlink injects these only on port-0x160 writes, + # which FLYK does only during boot -- so ignore the marker once past BOOT) + while phase == "BOOT" and RSET in data: + i = data.index(RSET) + pre, data = data[:i], data[i+len(RSET):] + if pre: recv_since_reset += _consume(pre, phase, asm, board, conn, emit) + emit("[reset] link reset strobe"); recv_since_reset = 0 + phase = "BOOT"; handshake_sent = False; asm.buf.clear() + if data: + recv_since_reset += _consume(data, phase, asm, board, conn, emit) + emit(f"connection closed. frames drawn={board.frames}, " + f"nodes={len(board.nodes)}, geom={len(board.geom)}, tex={len(board.tex)}") + +def _consume(data, phase, asm, board, conn, emit): + """Return count of bytes accounted; frame+dispatch when past boot.""" + if phase == "BOOT": + return len(data) # swallow BTL raw + view = getattr(board, '_view', None) + asm.feed(data) + for msg in asm: + board.log.clear() + is_draw = (not msg.iserver and msg.action == A.draw_scene) + try: + reply = board.handle(msg) + except Exception as e: + emit(f" [board error on {msg!r}: {e}]"); reply = b'' + quiet = is_draw and board.frames > 3 # don't spam the log at 60 Hz + if not quiet: + for line in board.log: emit(" " + line) + if is_draw and view is not None: + # render this frame board-side, then ack -- FLYK's loop is paced by us + try: + view.draw(board) + except KeyboardInterrupt: + raise + except Exception as e: + if board.frames % 300 == 1: emit(f" [render error: {e}]") + if board.frames % 300 == 0: + emit(f" [frame {board.frames}]") + if reply: + conn.sendall(reply) + if not quiet: + act = struct.unpack_from(' reply {nm} ({len(reply)}B)") + return len(data) + +if __name__ == '__main__': + main() diff --git a/dpl3-revive/patha/vrserver.py b/dpl3-revive/patha/vrserver.py new file mode 100644 index 0000000..a89b0d4 --- /dev/null +++ b/dpl3-revive/patha/vrserver.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +vrserver.py -- the board behind a socket. This is the endpoint the DOSBox-X C012 +I/O handler connects to (see CAPTURE.md). It speaks RAW link bytes: + + host -> board : the exact byte stream FLYK writes to port 0x151 (0x150 base) + board -> host : reply bytes FLYK reads from port 0x150 + +It logs every reassembled message and reply to a capture file, so a real FLYK boot +produces an annotated transcript we can diff against the source-derived spec and use +to pin the open items (readpixels layout, bit30 receive semantics). + +Boot handling: on a link RESET marker (control byte from the handler), the board +re-arms and, per the source handshake, queues 3 iserver transactions so FLYK's +startup_handshake() proceeds into the framed HSP/860/init traffic. + + python vrserver.py # listen on 127.0.0.1:8620, log to capture.log + python vrserver.py --selftest # loopback test: replay a synthetic boot+scene +""" +import socket, struct, sys, threading, time +from vrboard import VirtualBoard, Assembler, pack_iserver, A + +HOST, PORT = '127.0.0.1', 8620 +RESET_MARK = b'\x00RSET' # control frame the handler sends on a link reset strobe + +class BoardSession: + def __init__(self, log): + self.board = VirtualBoard() + self.asm = Assembler() + self.out = bytearray() # board->host bytes waiting to be read + self.log = log + self.seq = 0 + + def on_reset(self): + self._emit("RESET (link re-armed; queuing 3 iserver handshake txns)") + self.out.clear() + # source: C-runtime does 3 iserver transactions before the host proceeds + for _ in range(3): + self.out += pack_iserver(2, b'\x20\x00\x00') # getenv-shaped stub + self.board.log.clear() + + def feed(self, data): + """Consume host->board bytes; return board->host bytes to send back.""" + if not data: + return b'' + self.asm.feed(data) + for msg in self.asm: + self.board.log.clear() + reply = self.board.handle(msg) + for line in self.board.log: + self._emit(" " + line) + self._emit(f"MSG {msg!r}") + if reply: + self.out += reply + act = struct.unpack_from(' (board queues 3 iserver) -> host would service them -> + # then framed boot/init/scene traffic + sess.on_reset() + assert len(sess.out) > 0, "reset should queue handshake bytes" + sess.out.clear() + from vrboard import pack_vr + stream = pack_vr(A.code860, b'\x00'*40) + stream += pack_vr(A.init, b'video=svga|\n\x00') + stream += pack_vr(A.create, struct.pack('host stream:", names) + assert names == ['init', 'create', 'draw_scene'], names + assert sess.board.frames == 1 + log.close() + print("OK: socket board reassembles chunked input, boots, and replies correctly.") + print(" (see selftest_capture.log for the annotated transcript)") + +if __name__ == '__main__': + if '--selftest' in sys.argv: + _selftest() + else: + serve() diff --git a/dpl3-revive/patha/vrview.py b/dpl3-revive/patha/vrview.py new file mode 100644 index 0000000..965d997 --- /dev/null +++ b/dpl3-revive/patha/vrview.py @@ -0,0 +1,939 @@ +#!/usr/bin/env python3 +""" +vrview.py -- board-side real-time display for the virtual VelociRender board. + +Renders, in a pygame window, exactly what FLYK.EXE streams over the link: +geometry uploaded via action 0x17 (vertices) + 0x19 (connections), placed by the +instance/DCS graph, viewed through the VIEW node's projection with the camera +pose taken from DCS 0x2 under the animated root DCS 0x1, and animated by the +per-frame 0x1d transform updates. This is the picture the dead i860/PixelPlanes +board would have put on its video output. + +Wire facts this depends on (see spec/VELOCIRENDER_PROTOCOL.md, session 2026-07-04): + - DCS flush body: 4x4 row-major matrix at words 4..19 (translation in row 3), + parent DCS handle at word 20. + - instance flush body: object handle at word 6. + - object -> lod -> geogroup -> geometry via list_add; geogroup's f_material at + word 16 of its flush body. + - vertex strides: vtype 0x13 = [x y z nx ny nz u v], vtype 0x15 = [x y z r g b a u v]. + - gtype 2 = single implicit polygon (fan); others use conn19 index lists + (n_conns x per indices, fan-triangulated per connection). + - view body floats [6..17] = x0 y0 x1 y1 zeye xs ys hither yon back_r g b; + [18..23] = fog_enable, fog near/far/r/g/b. Camera looks down -Z, row-vector + convention (p' = p @ M), child-to-parent composition = M_child @ M_parent. + - 0x1d = [1][dcs handle][3x3][t] replacing that DCS's rotation+translation. +""" +import os +import struct +import numpy as np + +TYPE = {2:'zone',3:'view',4:'instance',5:'dcs',6:'lmodel',7:'object',8:'lod', + 9:'geogroup',0xa:'geometry',0xb:'material',0xc:'texture',0xd:'texmap', + 0xe:'light'} + + +# NOTE: vrboard.do_flush stores the flush payload MINUS the leading remote word, +# so stored-body word k = wire-payload word k+1. Offsets below are stored-body. +def _mat_from_dcs(body): + return np.frombuffer(body[12:76], dtype='root), tris,...}] + self.view = None + self.cam_chain = [] + self._meshcache = {} # geom handle -> {ver, mesh} + + def maybe_rebuild(self, board): + key = (len(board.nodes), len(board.edges), len(board.uploads), + len(board.conns), len(board.tex)) + if key == getattr(self, '_key', None): + return + self._key = key + self.rebuild(board) + + def rebuild(self, board): + nodes = board.nodes + tname = lambda h: TYPE.get(nodes.get(h, {}).get('type')) + kids = {} + inst_dcs = {} + # vr_dcs_NEST = parent/child; vr_dcs_LINK = SIBLING ring (1994 protocol: + # [bro, sis]) -- FLYK's top-level DCSs are a flat sibling list, NOT a + # tree. Treating link as parentage chained every instance through the + # tracker-animated head DCS 0x1, cancelling all camera motion. + self.dcs_parent = {} + self.link_parent = {} + for op, a, b in board.edges: + if op == 'nest': # true hierarchy only + self.dcs_parent[b] = a + if op == 'link': # camera-rig hierarchy (MUNGA: + self.link_parent[b] = a # vehicle->cockpit->view head) + if op != 'list_add': + continue + if tname(a) == 'dcs' and tname(b) == 'instance': + inst_dcs[b] = a + else: + kids.setdefault(a, []).append(b) + + # camera: the view hangs off its DCS via list_add; the tracker-animated + # HEAD is DCS 0x1 (a sibling root, not a parent) -- pose = M_viewdcs @ M_head + self.view = None + view_dcs = None + for op, a, b in board.edges: + if op == 'list_add' and tname(b) == 'view' and tname(a) == 'dcs': + self.view = b; view_dcs = a + # MUNGA ONLY: the camera chain follows dcs_link parentage -- the game + # rigs the view as vehicle --link--> cockpit --link--> head DCS (world + # pose lives in the vehicle DCS, 0x1f-animated). FLYK's links are flat + # SIBLING rings; following them there relocated every scene's camera + # (regress diffs blew up), so FLYK keeps M(view dcs) . M(head 0x1). + # INSTANCE chains stay nest-only in both dialects (link-as-parentage + # on instances is the session-3 camera-cancellation bug). + self.cam_chain = self._chain(board, view_dcs, + links=getattr(board, 'munga', False)) + if tname(1) == 'dcs' and 1 not in self.cam_chain: + self.cam_chain = self.cam_chain + [1] + + # lights: lmodel (0x6) and light (0xe) bodies carry [dcs][type][r g b] + # at stored words 2,3 + floats 4..6; type 2 = ambient, 3 = directional + # (aim = the light DCS's -Z; SHARKS: ambient .1/.3/.4, sun (0,1,0) ✓) + self.ambient = np.zeros(3) + self.dirlights = [] # [(L_toward_light_world, color)] + for h, nd in nodes.items(): + if nd.get('type') not in (0x6, 0xe): + continue + body = nd.get('body') or b'' + if len(body) < 28: + continue + ldcs, ltype = struct.unpack_from(' 100: + continue + if ltype == 2: + self.ambient += col + elif ltype == 3: + chain = self._chain(board, ldcs) + M = np.eye(4) + for d in chain: + b2 = nodes.get(d, {}).get('body') or b'' + if len(b2) >= 76: + M = M @ _mat_from_dcs(b2) + L = M[2, :3] # light DCS +Z row = toward light + n = np.linalg.norm(L) + if n > 1e-6: + self.dirlights.append((L / n, col)) + + # meshes are resolved lazily per-draw via _mesh() (version-cached per + # handle) so per-frame vertex-update uploads don't rebuild the graph + + # geometry -> texture RGB array, resolved geogroup -> f_material -> texture + # node -> texmap node -> board.tex. Refs are found type-directed (scan the + # body words for a handle of the right node type) -- robust to layout drift. + def find_ref(h, want_type): + body = nodes.get(h, {}).get('body') or b'' + for i in range(len(body) // 4): + w = struct.unpack_from(' 0) texture node + self._sliced_packs = set() + for h, nd in nodes.items(): + if nd.get('type') == 0xc: + body = nd.get('body') or b'' + if len(body) >= 56 and struct.unpack_from(' file bitslice 0 (unsliced / whole page) + sel = 0x13 + b -> file bitslice b (1..8) + and bitslice b addresses nibble plane b + 2 (pad byte occupies + planes 0-1; proven vs GENH: bitslices 0..5 = the exact set of + content planes 2..7). b >= 6 clamps to plane 7 (rare; only the + 9-texture GENS-class packs). + """ + entry = board.tex.get(texm) + if entry is None: + return None + body = nodes.get(texn, {}).get('body') or b'' + sel = struct.unpack_from('= 56 else 0 + # sel==0 on a pack that also has sliced siblings = bitslice 0; + # sel==0 on a plain page = full-colour texture + if entry['mode'] == 0 and (sel or texm in self._sliced_packs): + b = (sel - 0x13) if sel else 0 + plane = min(b + 2, 7) + key = (texm, plane) + if key not in texcache: + w = np.frombuffer(bytes(entry['data']), '> (4 * plane)) & 0xF).astype(np.float32)) * 17.0 + texcache[key] = np.repeat(g[:, :, None], 3, axis=2) + return texcache[key] + if texm not in texcache: + a = np.frombuffer(bytes(entry['data']), np.uint8) + a = a[:entry['u'] * entry['v'] * 4].reshape(entry['v'], entry['u'], 4) + texcache[texm] = a[:, :, [3, 2, 1]].astype(np.float32) # [pad,B,G,R] + return texcache[texm] + + def mtl_props(mtl): + body = nodes.get(mtl, {}).get('body') or b'' + if len(body) < 84: + return None + # MUNGA material bodies carry one extra leading word (88B vs 84B): + # every FLYK offset shifts +1 (misparse showed as a green-washed, + # all-dithered world -- ambient read one float early) + s = 1 if getattr(board, 'munga', False) and len(body) >= 88 else 0 + f = struct.unpack_from('<22f' if s else '<21f', body, 0) + amb, dif = np.array(f[7+s:10+s]), np.array(f[10+s:13+s]) + # exact (1,0,0)/(1,0,0) ambient+diffuse = the shipped build's UNSET + # material marker (seen on all SHARKS/.B2Z-default and star mtls), + # not a colour -- render as white + if np.array_equal(amb, [1, 0, 0]) and np.array_equal(dif, [1, 0, 0]): + amb = dif = np.ones(3) + op = float(np.clip(np.mean(f[13+s:16+s]), 0, 1)) + return {'emissive': np.array(f[4+s:7+s]), 'ambient': amb, + 'diffuse': dif, 'opacity': op, + 'specular': np.array(f[16+s:20+s])} + + for gg, members in kids.items(): + if tname(gg) != 'geogroup': + continue + mtl = find_ref(gg, 0xb) + texn = find_ref(mtl, 0xc) if mtl else None + gg_texm = find_ref(texn, 0xd) if texn else None + props = mtl_props(mtl) if mtl else None + for g in members: + if tname(g) != 'geometry': + continue + # PER-GEOMETRY refs take precedence: FLYK geometry bodies name + # their texmap directly (word 3); MUNGA/BTL4 bodies name their + # TEXTURE node (word 2) and often their material -- resolving + # only through the geogroup chain assigned one shared texture + # (BT: every terrain tile got the cloud page). + g_texn = find_ref(g, 0xc) + tn = g_texn or texn + g_texm = (find_ref(g, 0xd) + or (find_ref(g_texn, 0xd) if g_texn else None)) + texm = g_texm or gg_texm + rgb = texmap_rgb(texm, tn) if texm else None + if rgb is not None: + self.geom_tex[g] = rgb + if tn is not None: + self.geom_texn[g] = tn # for live u0/v0/du/dv scroll + g_mtl = find_ref(g, 0xb) + g_props = mtl_props(g_mtl) if g_mtl else props + if g_props is not None: + self.geom_mtl[g] = g_props + + # instances -> flattened triangle lists in model space + self.instances = [] + for h, nd in nodes.items(): + if tname(h) != 'instance' or h not in inst_dcs: + continue + body = nd.get('body') or b'' + if len(body) < 24: + continue + obj = struct.unpack_from('= 84 + and struct.unpack_from(' render in + # camera space (skip the view matrix) + hud = False + if len(dbody) >= 80: + par = struct.unpack_from('= 68: + sw_in, sw_out = struct.unpack_from('<2f', lb, 60) + if not (np.isfinite(sw_in) and np.isfinite(sw_out) + and 0 <= sw_in < sw_out < 1e7): + sw_in, sw_out = 0.0, 1e9 + lods.append((sw_in, sw_out, lg)) + if not lods: + continue + # MUNGA: vehicle/mech part instances get their world pose through + # the same dcs_link rig as the camera (torso 0x1f-animated) -- + # nest-only chains left the player mech rendering at world origin. + # FLYK stays nest-only (links there are sibling rings; session 3). + self.instances.append({'dcs': inst_dcs[h], 'handle': h, + 'chain': self._chain( + board, inst_dcs[h], + links=getattr(board, 'munga', False)), + 'geoms': lods[0][2], 'lods': lods, + 'billboard': bboard, 'hud': hud}) + + # vertex strides seen on the wire (SHARKS + SDEMO): stride -> field slices + # 3 = [x y z] (vtype 0x01) + # 4 = [x y z r?] (vtype 0x41, gtype 0xa -- spheres/points; positions only) + # 5 = [x y z u v] (vtype 0x11) + # 8 = [x y z nx ny nz u v] (vtype 0x13) + # 9 = [x y z r g b a u v] (vtype 0x15) + def _mesh(self, board, gh, _depth=0): + ups = board.uploads.get(gh) or [] + cns = board.conns.get(gh) or [] + morph = board.morphs.get(gh) if _depth == 0 else None + ver = (len(ups), len(cns), len(ups[-1]['data']) if ups else 0, + ups[-1]['hdr'][1] if ups else 0, morph) + cached = self._meshcache.get(gh) + if cached is not None and cached['ver'] == ver: + return cached['mesh'] + + stride = ups[-1]['hdr'][3] + scale = ups[-1]['scale'] or 1.0 + # merge uploads at their vertex offsets (hdr word1 = first vertex, + # word7 = total) -- later offset uploads are vertex animation updates + total = 0 + for up in ups: + w = up['hdr'] + total = max(total, w[1] + w[2], w[7]) + v = np.zeros((total, stride)) + for up in ups: + w = up['hdr'] + if w[3] != stride: + continue + fl = np.frombuffer(bytes(up['data']), dtype='= 9: + # vtype 0x15 = [x y z r g b ? u v]: field 6 is NOT alpha -- values + # run -3.75..1.0 across products (blending on it made SHARKS' kelp + # ghostly). Kept as mesh['alpha'] DATA only; semantics undecoded. + col = v[:, 3:6]; alpha = v[:, 6]; uv = v[:, 7:9] + + tris = [] + if cns: + for c in cns: + idx = np.frombuffer(bytes(c['data']), dtype='= 3 and ups[-1]['hdr'][6] == 2: # gtype 2: implicit polygon fan + fan = np.arange(1, n - 1) + tris.append(np.stack([np.zeros_like(fan), fan, fan + 1], 1)) + mesh = None + if tris: + mesh = {'pos': pos, 'col': col, 'nrm': nrm, 'uv': uv, + 'alpha': alpha, 'tri': np.concatenate(tris, 0)} + self._meshcache[gh] = {'ver': ver, 'mesh': mesh} + return mesh + + def _chain(self, board, dcs, links=False): + """leaf->root list of DCS handles following vr_dcs_nest hierarchy ONLY + (the flush-body word at stored offset 76 is the SIBLING pointer, not a + parent -- following it merged the camera head into every instance chain + and cancelled all tracker motion). links=True additionally follows + vr_dcs_link parentage where no nest parent exists -- used for the + CAMERA chain only (MUNGA vehicle->cockpit->head rig; FLYK 0x1->0x2).""" + nodes = board.nodes + chain = [] + seen = set() + while dcs and dcs not in seen and nodes.get(dcs, {}).get('type') == 5: + chain.append(dcs); seen.add(dcs) + nxt = getattr(self, 'dcs_parent', {}).get(dcs) + if nxt is None and links: + nxt = getattr(self, 'link_parent', {}).get(dcs) + dcs = nxt + return chain + + +class Renderer: + def __init__(self, w=512, h=320, title="VelociRender virtual board -- FLYK live"): + import pygame + self.pygame = pygame + pygame.init() + sz = os.environ.get('VRVIEW_SIZE') # e.g. 384x240 for more fps + if sz and 'x' in sz: + try: + w, h = (int(v) for v in sz.lower().split('x')) + except ValueError: + pass + self.w, self.h = w, h + self.screen = pygame.display.set_mode((w, h), pygame.SCALED | pygame.RESIZABLE) + pygame.display.set_caption(title) + self.cache = SceneCache() + self.frame = 0 + self.clock = pygame.time.Clock() + # RETRACE pacing: run_demo sets VRVIEW_FPS from the scene's divider + self.fps = int(os.environ.get('VRVIEW_FPS', '60')) + self.skip = 1 # adaptive: render 1 of N draw_scenes + self._last_ms = 0.0 + self._psys = {} # SPECIALFX particle pools by code + self.light = np.array([0.3, 0.8, 0.5]); self.light /= np.linalg.norm(self.light) + + def dcs_matrix(self, board, h): + m = None + body = board.nodes.get(h, {}).get('body') or b'' + if len(body) >= 80: + m = _mat_from_dcs(body) + else: + m = np.eye(4) + if h in getattr(board, 'anim_abs', {}): # live 0x1f pose (MUNGA): REPLACES + f = board.anim_abs[h] + a = np.eye(4) + a[:3, :3] = np.array(f[:9]).reshape(3, 3) + a[3, :3] = f[9:12] + if np.all(np.isfinite(a)): + m = a + elif h in board.anim: # live 0x1d articulation + f = board.anim[h] + a = np.eye(4) + # 0x1d 3x3 is row-vector convention, same as flush matrices + # (heading_test.py: model -Z tracks velocity, dot~-1, 95% of samples) + a[:3, :3] = np.array(f[:9]).reshape(3, 3) + a[3, :3] = f[9:12] + if np.all(np.isfinite(a)): + # flush_artics COMPOSES with the flushed base matrix (KLNGVID: + # the DCS holds the 0.1 model scale, the spline pose rides on + # top; SHARKS' identity base hid the distinction) + m = m @ a + if h in board.anim4: # live 0x1b full-matrix overrides + f4 = np.array(board.anim4[h]).reshape(4, 4) + # identity 3x3 + junk row3 = host sent an identity-flagged DCS + # verbatim (uninitialized translation) -- keep the flushed matrix + if not np.allclose(f4[:3, :3], np.eye(3)): + m = f4.copy() + m[:, 3] = (0, 0, 0, 1) + return m + + def chain_matrix(self, board, chain): + m = np.eye(4) + for h in chain: # leaf first: M_leaf @ ... @ M_root + m = m @ self.dcs_matrix(board, h) + return m + + # MUNGA vehicles fly nose-along-+Z: straight-fast-flight samples of the + # RAW vehicle 0x1f records put local velocity 3.8 deg off +Z (the earlier + # +X reading came from the composed chain, biased by the attract head-yaw + # swinging +/-54 deg). The view projection looks down -Z, so the raw chain + # renders 180 deg backward -- yaw(180) maps render-forward onto vehicle +Z. + # Camera only; instances carry their pose in their own DCS frames. + _CAMFIX = np.eye(4) + _CAMFIX[:3, :3] = [[-1, 0, 0], [0, 1, 0], [0, 0, -1]] + + def cam_matrix(self, board): + if os.environ.get('VRVIEW_CHASE') in ('1', '2'): + m = self._chase_cam(board) + if m is not None: + return m + m = self.chain_matrix(board, self.cache.cam_chain) + if getattr(board, 'munga', False): + m = self._CAMFIX @ m + return m + + def _chase_cam(self, board): + """Debug camera: follow the most-traveled 0x1f-animated DCS (the + vehicle/mech torso) from behind-above. VRVIEW_CHASE=1 chases behind + the target's nose (turns with it); =2 keeps a world-locked offset + (no rotation when the target spins in place).""" + anim = getattr(board, 'anim_abs', None) + if not anim: + return None + # prefer the PLAYER vehicle: the root of the camera rig (last DCS in + # the cam chain) when animated; else the most-traveled animated DCS + chain = getattr(self.cache, 'cam_chain', []) + if chain and chain[-1] in anim: + h = chain[-1] + else: + h = max(anim, + key=lambda k: float(np.abs(np.array(anim[k][9:12])).sum())) + f = anim[h] + R = np.array(f[:9]).reshape(3, 3) + t = np.array(f[9:12]) + if os.environ.get('VRVIEW_CHASE') == '2': + offset = np.array([-45.0, 25.0, -45.0]) # world-locked NE-above + else: + nose = R[2, :3] # MUNGA vehicles nose along +Z + offset = -nose * 60.0 + np.array([0.0, 25.0, 0.0]) + eye = t + offset + fwd = t - eye + fwd /= max(np.linalg.norm(fwd), 1e-6) + back = -fwd + right = np.cross([0.0, 1.0, 0.0], back) + right /= max(np.linalg.norm(right), 1e-6) + up = np.cross(back, right) + M = np.eye(4) + M[0, :3], M[1, :3], M[2, :3], M[3, :3] = right, up, back, eye + return M + + def pick(self, board, origin, direction): + """World-space ray -> nearest instance handle (sect_pixel/sect_vector). + Moller-Trumbore over every instance's world triangles.""" + self.cache.maybe_rebuild(board) + o = np.asarray(origin, np.float64) + d = np.asarray(direction, np.float64) + dn = np.linalg.norm(d) + if dn < 1e-9: + return 0 + d = d / dn + best_t, best_h = np.inf, 0 + for inst in self.cache.instances: + M = self.chain_matrix(board, inst['chain']) + R, T = M[:3, :3], M[3, :3] + for gh in inst['geoms']: + mesh = self.cache._mesh(board, gh) + if mesh is None or 'sphere' in mesh: + continue + pw = mesh['pos'] @ R + T + tri = mesh['tri'] + v0, v1, v2 = pw[tri[:, 0]], pw[tri[:, 1]], pw[tri[:, 2]] + e1, e2 = v1 - v0, v2 - v0 + pv = np.cross(d[None, :], e2) + det = (e1 * pv).sum(1) + okd = np.abs(det) > 1e-12 + inv = np.where(okd, 1.0 / np.where(okd, det, 1.0), 0.0) + tv = o[None, :] - v0 + u = (tv * pv).sum(1) * inv + qv = np.cross(tv, e1) + vv = (d[None, :] * qv).sum(1) * inv + t = (e2 * qv).sum(1) * inv + hit = okd & (u >= 0) & (vv >= 0) & (u + vv <= 1) & (t > 1e-6) + if hit.any(): + tm = float(t[hit].min()) + if tm < best_t: + best_t, best_h = tm, inst['handle'] + return best_h + + def pick_screen(self, board, u, v): + """Pick through screen point (u,v in 0..1) using the live camera + + view projection (0.5,0.5 = crosshair center). Returns instance handle.""" + c = self.cache + c.maybe_rebuild(board) + cam = self.cam_matrix(board) + origin = cam[3, :3] + # image-plane point in eye space: x0..x1 / y0..y1 at z = -zeye + x0, x1, y0, y1, zeye = -1.0, 1.0, -0.615, 0.615, 1.3 + if c.view is not None: + vb = board.nodes[c.view].get('body') or b'' + if len(vb) >= 96: + f = struct.unpack_from('<24f', vb, 0) + x0, y0, x1, y1, zeye = f[5], f[6], f[7], f[8], max(f[9], 1e-3) + de = np.array([x0 + u * (x1 - x0), y0 + (1 - v) * (y1 - y0), -zeye]) + dw = de @ cam[:3, :3] + return self.pick(board, origin, dw) + + def pump(self): + """Keep the window responsive while the link is idle (no draw_scene).""" + pg = self.pygame + for ev in pg.event.get(): + if ev.type == pg.QUIT: + pg.quit(); raise KeyboardInterrupt + + def draw(self, board): + pg = self.pygame + for ev in pg.event.get(): + if ev.type == pg.QUIT: + pg.quit(); raise KeyboardInterrupt + self.frame += 1 + # adaptive frame skip: keep FLYK paced at 60/skip Hz while the software + # rasterizer draws what it can (heavy scenes render 1 of N frames) + if self.frame % self.skip: + self.clock.tick(self.fps) + return + import time as _t + _t0 = _t.perf_counter() + self.cache.maybe_rebuild(board) + c = self.cache + W, H = self.w, self.h + + # view parameters + vp = None + if c.view is not None: + vb = board.nodes[c.view].get('body') or b'' + if len(vb) >= 96: + f = struct.unpack_from('<24f', vb, 0) + # stored-body float idx = wire idx - 1: x0@5 y0@6 x1@7 y1@8 zeye@9 + # xs@10 ys@11 hither@12 yon@13 back@14..16 fog_enable@17 fog@18..22 + vp = {'x0': f[5], 'y0': f[6], 'x1': f[7], 'y1': f[8], 'zeye': f[9], + 'hither': max(f[12], 1e-3), 'yon': f[13], 'back': f[14:17], + 'fog_on': struct.unpack_from(' -Z). Statics carry their pose in the .SCN. + FIX = np.eye(4) + FIX[:3, :3] = [[0, 0, -1], [0, 1, 0], [1, 0, 0]] + + for inst in c.instances: + Mw = self.chain_matrix(board, inst['chain']) + # NOVIEWMATRIX instances live in camera space: no view transform + M = Mw if inst.get('hud') else Mw @ V + if inst['chain'] and inst['chain'][0] in board.anim: + M = FIX @ M + if inst['billboard']: + # spherical billboard: keep scale + eye position, face the camera + s = np.linalg.norm(M[0, :3]) + M = M.copy() + M[:3, :3] = np.eye(3) * s + R, T = M[:3, :3], M[3, :3] + geoms = inst['geoms'] + if len(inst['lods']) > 1: # range-select LOD by eye distance + dist = float(np.linalg.norm(T)) + for sw_in, sw_out, lg in inst['lods']: + if sw_in <= dist < sw_out: + geoms = lg + break + for gh in geoms: + mesh = c._mesh(board, gh) + if mesh is None: + continue + if 'sphere' in mesh: + self._spheres(mesh, M, c.geom_mtl.get(gh), vp, img, zbuf, W, H) + continue + pe = mesh['pos'] @ R + T # eye space + z = -pe[:, 2] + # near AND far clip (yon): beyond the fog wall nothing is + # visible anyway -- kills most of a 6 km canal per frame + ok = (z > vp['hither']) & (z < vp['yon'] * 1.05) + zs = np.where(ok, z, 1.0) + px = (pe[:, 0] * vp['zeye'] / zs - vp['x0']) / (vp['x1'] - vp['x0']) * W + py = (1 - (pe[:, 1] * vp['zeye'] / zs - vp['y0']) / (vp['y1'] - vp['y0'])) * H + # whole-mesh early-out: nothing in front of the camera, or the + # visible verts' screen bbox misses the viewport entirely + if not ok.any(): + continue + vx, vy = px[ok], py[ok] + if (vx.max() < 0 or vx.min() >= W + or vy.max() < 0 or vy.min() >= H): + continue + # per-vertex shade: emissive + diffuse * (wire ambient + suns) + props = c.geom_mtl.get(gh) + diffuse = props['diffuse'] if props is not None else np.array([0.75, 0.78, 0.82]) + emissive = props['emissive'] if props is not None else np.zeros(3) + if c.dirlights or c.ambient.any(): + amb, suns = c.ambient, c.dirlights + else: # no lights on the wire + amb, suns = np.full(3, 0.35), [(self.light, np.full(3, 0.65))] + lit_rgb = np.repeat(amb[None, :], len(pe), 0).copy() + if mesh['nrm'] is not None and suns: + # world-space normals; DPL lights double-sided (abs) + Rm = Mw[:3, :3] + nw = mesh['nrm'] @ Rm + nw /= np.maximum(np.linalg.norm(nw, axis=1, keepdims=True), 1e-9) + spec = props['specular'] if props is not None else None + for L, lcol in suns: + nl = np.abs(nw @ L) + lit_rgb += nl[:, None] * lcol[None, :] + if spec is not None and spec[:3].max() > 1e-3: + p_exp = spec[3] if 1.0 < spec[3] < 200 else 16.0 + lit_rgb += (nl ** p_exp)[:, None] * spec[None, :3] * lcol[None, :] + else: + for L, lcol in suns: + lit_rgb += 0.7 * lcol[None, :] + base = (emissive[None, :] + diffuse[None, :] * lit_rgb) * 255 + if mesh['col'] is not None: + base = mesh['col'] * base # vertex colors modulate + base = np.clip(base, 0, 255) + # board-side SCROLL: texture body u0/v0 + du/dv * seconds + # (FLYK); MUNGA treats floats 10/11 as a STATIC uv offset + uvoff = (0.0, 0.0) + texn = c.geom_texn.get(gh) + if texn is not None: + tb = board.nodes.get(texn, {}).get('body') or b'' + if len(tb) >= 48: + u0, v0, du, dv = struct.unpack_from('<4f', tb, 32) + if all(np.isfinite((u0, v0, du, dv))): + if getattr(board, 'munga', False): + uvoff = (u0 + du, v0 + dv) + else: + t = self.frame / 60.0 + uvoff = (u0 + du * t, v0 + dv * t) + tex = c.geom_tex.get(gh) + if tex is not None: + # texture modulated by shade; fog applied to the modulator only + # when untextured, so blend fog after texturing instead + shade = base / 200.0 + else: + shade = None + fz = None + if vp['fog_on'] and fog_far > fog_near: + fz = np.clip((z - fog_near) / (fog_far - fog_near), 0, 1) + if tex is None: + base = base * (1 - fz)[:, None] + fog_col[None, :] * fz[:, None] + # transparency: texture alpha flag (texture body stored word 4) + # -> cutout near-black texels; material opacity < 1 (DITHER n) + # -> ordered screen-door + acut = False + if texn is not None and tex is not None: + tb = board.nodes.get(texn, {}).get('body') or b'' + if len(tb) >= 20 and struct.unpack_from(' life + nd = int(dead.sum()) + if nd: + rad = float(np.clip(d['size'] * 0.15, 4.0, 90.0)) + p = rng.uniform(-1, 1, (nd, 3)) * rad + p[:, 1] = rng.uniform(0, 1, nd) * rad * 0.5 + d['bias'] + d['off_y'] + # keep spawns off the camera lens + nrm = np.linalg.norm(p, axis=1) + close = nrm < 8.0 + p[close] *= (8.0 / np.maximum(nrm[close], 1e-6))[:, None] + ps['pos'][dead] = eye + p + v = rng.uniform(-1, 1, (nd, 3)) * d['variance'] * max(d['velocity'], 0.5) + v[:, 1] = d['velocity'] * rng.uniform(0.6, 1.2, nd) + ps['vel'][dead] = v + ps['age'][dead] = rng.uniform(0, life * 0.5, nd) + ps['vel'][:, 1] -= d['gravity'] * 0.002 * dt + ps['pos'] += ps['vel'] * dt + ps['age'] += dt + # draw as fading discs + pe = ps['pos'] @ V[:3, :3] + V[3, :3] + z = -pe[:, 2] + fade = np.clip(d['o_cool'] ** (ps['age'] * 60.0), 0.05, 1.0) + cook = np.clip(np.array(d['cook']), 0, 2) + pr_world = float(np.clip(d['size'] * 0.008, 0.15, 4.0)) + kx = vp['zeye'] / (vp['x1'] - vp['x0']) * W + for i in np.argsort(-z): + zi = z[i] + if zi <= vp['hither'] or zi > vp['yon']: + continue + cx = (pe[i, 0] * vp['zeye'] / zi - vp['x0']) / (vp['x1'] - vp['x0']) * W + cy = (1 - (pe[i, 1] * vp['zeye'] / zi - vp['y0']) / (vp['y1'] - vp['y0'])) * H + pr = min(max(pr_world * kx / zi, 0.6), H * 0.08) + x0 = int(max(0, cx - pr)); x1 = int(min(W - 1, cx + pr)) + 1 + y0 = int(max(0, cy - pr)); y1 = int(min(H - 1, cy + pr)) + 1 + if x0 >= x1 or y0 >= y1: + continue + gx, gy = np.meshgrid(np.arange(x0, x1), np.arange(y0, y1)) + m = ((gx - cx) ** 2 + (gy - cy) ** 2) <= pr * pr + tz = zbuf[y0:y1, x0:x1] + upd = m & (zi < tz) + if upd.any(): + # additive-ish blend, no zbuf write (soft particles) + tile = img[y0:y1, x0:x1] + tile[upd] = np.clip( + tile[upd] * (1 - 0.6 * fade[i]) + + cook[None, :] * 255 * 0.75 * fade[i], 0, 255) + + def _spheres(self, mesh, M, props, vp, img, zbuf, W, H): + """Sphere-list geometry (vtype 0x41): shaded screen-space discs.""" + R, T = M[:3, :3], M[3, :3] + s = np.linalg.norm(M[0, :3]) + pe = mesh['sphere'] @ R + T + z = -pe[:, 2] + col = np.array([0.8, 0.8, 0.8]) + if props is not None: + col = np.clip(props['emissive'] + props['diffuse'], 0, 1) + rgb = col * 255 + kx = vp['zeye'] / (vp['x1'] - vp['x0']) * W + for i in np.argsort(-z): + zi = z[i] + if zi <= vp['hither']: + continue + cx = (pe[i, 0] * vp['zeye'] / zi - vp['x0']) / (vp['x1'] - vp['x0']) * W + cy = (1 - (pe[i, 1] * vp['zeye'] / zi - vp['y0']) / (vp['y1'] - vp['y0'])) * H + pr = max(mesh['rad'][i] * s * kx / zi, 0.7) + x0 = int(max(0, cx - pr)); x1 = int(min(W - 1, cx + pr)) + 1 + y0 = int(max(0, cy - pr)); y1 = int(min(H - 1, cy + pr)) + 1 + if x0 >= x1 or y0 >= y1: + continue + gx, gy = np.meshgrid(np.arange(x0, x1), np.arange(y0, y1)) + m = ((gx - cx) ** 2 + (gy - cy) ** 2) <= pr * pr + tz = zbuf[y0:y1, x0:x1] + upd = m & (zi < tz) + if upd.any(): + tz[upd] = zi + img[y0:y1, x0:x1][upd] = rgb + + def _raster(self, tri, px, py, z, ok, col, uv, tex, shade, fz, fog_col, + img, zbuf, W, H, uvoff=(0.0, 0.0), alpha_cut=False, opacity=1.0): + if len(tri) == 0: + return + # vectorized pre-cull: clip-rejected verts, off-screen bboxes and + # degenerate triangles never reach the per-triangle Python loop + A, B, C = tri[:, 0], tri[:, 1], tri[:, 2] + keep = ok[A] & ok[B] & ok[C] + if not keep.any(): + return + xs = np.stack([px[A], px[B], px[C]]) + ys = np.stack([py[A], py[B], py[C]]) + minxs, maxxs = xs.min(0), xs.max(0) + minys, maxys = ys.min(0), ys.max(0) + keep &= (maxxs >= 0) & (minxs < W) & (maxys >= 0) & (minys < H) + areas = ((xs[1] - xs[0]) * (ys[2] - ys[0]) + - (xs[2] - xs[0]) * (ys[1] - ys[0])) + keep &= np.abs(areas) > 1e-9 + for t in tri[keep]: + a, b, cc = int(t[0]), int(t[1]), int(t[2]) + x0, y0, x1, y1, x2, y2 = px[a], py[a], px[b], py[b], px[cc], py[cc] + minx = int(max(0, min(x0, x1, x2))); maxx = int(min(W - 1, max(x0, x1, x2))) + 1 + miny = int(max(0, min(y0, y1, y2))); maxy = int(min(H - 1, max(y0, y1, y2))) + 1 + if minx >= maxx or miny >= maxy: + continue + area = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0) + xs = np.arange(minx, maxx, dtype=np.float32) + 0.5 + ys = np.arange(miny, maxy, dtype=np.float32) + 0.5 + gx, gy = np.meshgrid(xs, ys) + w0 = ((x1 - gx) * (y2 - gy) - (x2 - gx) * (y1 - gy)) / area + w1 = ((x2 - gx) * (y0 - gy) - (x0 - gx) * (y2 - gy)) / area + w2 = 1 - w0 - w1 + inside = (w0 >= 0) & (w1 >= 0) & (w2 >= 0) + if not inside.any(): + continue + # perspective-correct weights via 1/z + wa = w0 / z[a]; wb = w1 / z[b]; wc = w2 / z[cc] + iz = wa + wb + wc + zi = 1.0 / np.maximum(iz, 1e-12) + tile_z = zbuf[miny:maxy, minx:maxx] + upd = inside & (zi < tile_z) + if opacity < 0.99: + # DITHER screen-door: 2x2 ordered pattern vs opacity + bay = (((gx.astype(np.int32) & 1) * 2 + (gy.astype(np.int32) & 1)) + + 0.5) / 4.0 + upd &= bay <= opacity + if not upd.any(): + continue + na = wa / iz; nb = wb / iz; nc = wc / iz + if tex is not None and uv is not None: + u = na * uv[a, 0] + nb * uv[b, 0] + nc * uv[cc, 0] + uvoff[0] + v = na * uv[a, 1] + nb * uv[b, 1] + nc * uv[cc, 1] + uvoff[1] + th, tw = tex.shape[:2] + tx = (u % 1.0 * tw).astype(np.int32) % tw + ty = (v % 1.0 * th).astype(np.int32) % th + rgb = tex[ty, tx] + if alpha_cut: + upd &= rgb.sum(axis=-1) > 24.0 # near-black = transparent + if not upd.any(): + continue + mod = (na[..., None] * shade[a] + nb[..., None] * shade[b] + + nc[..., None] * shade[cc]) + rgb = rgb * mod + if fz is not None: + f = na * fz[a] + nb * fz[b] + nc * fz[cc] + rgb = rgb * (1 - f)[..., None] + fog_col[None, None, :] * f[..., None] + else: + rgb = (na[..., None] * col[a] + nb[..., None] * col[b] + + nc[..., None] * col[cc]) + tile_z[upd] = zi[upd] + tile_c = img[miny:maxy, minx:maxx] + tile_c[upd] = rgb[upd] diff --git a/dpl3-revive/patha/vrview_gl.py b/dpl3-revive/patha/vrview_gl.py new file mode 100644 index 0000000..125e026 --- /dev/null +++ b/dpl3-revive/patha/vrview_gl.py @@ -0,0 +1,621 @@ +#!/usr/bin/env python3 +""" +vrview_gl.py -- moderngl GPU backend for the virtual VelociRender board. + +Same wire-state-in, pixels-out contract as vrview.Renderer (the software +rasterizer, which stays intact as the debugging reference -- select it with +vrrun --soft / VRVIEW_SOFT=1). SceneCache, DCS chain math, and picking are +inherited; only the draw path is replaced: + + - SceneCache._mesh() results become static VBOs, cached per geometry handle + on the mesh dict's identity (the _mesh version key already covers vertex + updates + morph alpha -- a new mesh dict means re-upload, else zero work). + - Per frame only camera/instance matrices and a few uniforms change. + - Row-vector convention rides for free: numpy row-major mat4 bytes are read + column-major by GLSL (= transposed), so `u_m * v` in the shader IS p @ M. + - The scene renders linear into an offscreen 832x512 FBO; a present pass + applies the Division DAC gamma (out = in^(1/1.25)) exactly where the + software path does (once, over the final image, back color included). + - Shaders replicate the software formulas: per-vertex lighting (ambient + + |n.L| suns double-sided, specular, 0.7-per-sun no-normal fallback), + vertex-color modulate, texture * shade(=base*1.275), SCROLL uv offset, + alpha cutout (texel sum <= 24/255), DITHER 2x2 screen-door discard, + per-vertex linear fog mixed after texturing, sphere-list and SPECIALFX + point sprites. + +Headless (SDL_VIDEODRIVER=dummy, e.g. regress.py) uses a standalone WGL +context -- no window, frames read back from the FBO via .last_frame. +""" +import os +import struct +import numpy as np + +import vrview +from vrview import SceneCache, _mat_from_dcs + +MESH_VS = """ +#version 330 +uniform mat4 u_mv; // model -> eye (row-vector: u_mv * v == p @ M) +uniform mat4 u_p; // eye -> clip +uniform mat4 u_model; // model -> world (normals; matches software: no FIX) +uniform vec3 u_ambient; +uniform int u_nsuns; +uniform vec3 u_sun_dir[4]; +uniform vec3 u_sun_col[4]; +uniform vec3 u_diffuse; +uniform vec3 u_emissive; +uniform vec4 u_specular; // rgb + exponent +uniform int u_has_nrm; +uniform int u_has_col; +uniform int u_fog_on; +uniform vec2 u_fogrange; // near, far +in vec3 in_pos; +in vec3 in_nrm; +in vec3 in_col; +in vec2 in_uv; +in float in_alpha; // vtype 0x15 vertex alpha (cloud/sky layers) +out vec3 v_base; +out vec2 v_uv; +out float v_fog; +out float v_alpha; +void main() { + vec4 pe = u_mv * vec4(in_pos, 1.0); + gl_Position = u_p * pe; + v_alpha = in_alpha; + float z = -pe.z; + vec3 lit = u_ambient; + if (u_has_nrm == 1 && u_nsuns > 0) { + vec3 nw = mat3(u_model) * in_nrm; + nw /= max(length(nw), 1e-9); + float smax = max(u_specular.r, max(u_specular.g, u_specular.b)); + for (int i = 0; i < u_nsuns; i++) { + float nl = abs(dot(nw, u_sun_dir[i])); // double-sided + lit += nl * u_sun_col[i]; + if (smax > 1e-3) { + float p = (u_specular.w > 1.0 && u_specular.w < 200.0) + ? u_specular.w : 16.0; + lit += pow(nl, p) * u_specular.rgb * u_sun_col[i]; + } + } + } else { + for (int i = 0; i < u_nsuns; i++) lit += 0.7 * u_sun_col[i]; + } + vec3 base = u_emissive + u_diffuse * lit; + if (u_has_col == 1) base *= in_col; // vertex colors modulate + v_base = clamp(base, 0.0, 1.0); + v_uv = in_uv; + v_fog = (u_fog_on == 1) + ? clamp((z - u_fogrange.x) / (u_fogrange.y - u_fogrange.x), 0.0, 1.0) + : 0.0; +} +""" + +MESH_FS = """ +#version 330 +uniform sampler2D u_tex; +uniform int u_has_tex; +uniform int u_alpha_cut; +uniform vec2 u_uvoff; // board-side SCROLL +uniform float u_opacity; // DITHER n -> screen-door +uniform vec3 u_fogcol; +in vec3 v_base; +in vec2 v_uv; +in float v_fog; +in float v_alpha; +out vec4 f_color; +void main() { + if (u_opacity < 0.99) { // 2x2 ordered screen-door vs opacity + float bay = (float((int(gl_FragCoord.x) & 1) * 2 + + (int(gl_FragCoord.y) & 1)) + 0.5) / 4.0; + if (bay > u_opacity) discard; + } + vec3 rgb; + if (u_has_tex == 1) { + vec3 t = texture(u_tex, v_uv + u_uvoff).rgb; + if (u_alpha_cut == 1 && (t.r + t.g + t.b) <= 24.0 / 255.0) + discard; // near-black texel = transparent + rgb = t * (v_base * 1.275); // software: texel * (base/200), base 0..255 + } else { + rgb = v_base; + } + // alpha only takes effect in the deferred blended pass (opaque pass + // renders with BLEND disabled) + f_color = vec4(mix(rgb, u_fogcol, v_fog), clamp(v_alpha, 0.0, 1.0)); +} +""" + +POINT_VS = """ +#version 330 +uniform mat4 u_mv; +uniform mat4 u_p; +uniform float u_kx; // zeye / (x1-x0) * W (pixels per unit at z=1) +uniform float u_minpr; +uniform float u_maxpr; // <=0 = unclamped (spheres) +in vec3 in_pos; +in float in_rad; // world radius (spheres) / pr_world (particles) +in vec3 in_col; +in float in_fade; +out vec3 g_col; +out float g_fade; +void main() { + vec4 pe = u_mv * vec4(in_pos, 1.0); + gl_Position = u_p * pe; + float z = max(-pe.z, 1e-6); + float pr = max(in_rad * u_kx / z, u_minpr); + if (u_maxpr > 0.0) pr = min(pr, u_maxpr); + gl_PointSize = min(2.0 * pr, 4096.0); + g_col = in_col; + g_fade = in_fade; +} +""" + +POINT_FS = """ +#version 330 +uniform int u_particle; // 0 = opaque disc (spheres), 1 = SPECIALFX blend +in vec3 g_col; +in float g_fade; +out vec4 f_color; +void main() { + vec2 d = gl_PointCoord - vec2(0.5); + if (dot(d, d) > 0.25) discard; + if (u_particle == 1) { + // dst*(1 - 0.6*fade) + cook*0.75*fade via (ONE, ONE_MINUS_SRC_ALPHA) + f_color = vec4(g_col * 0.75 * g_fade, 0.6 * g_fade); + } else { + f_color = vec4(g_col, 1.0); + } +} +""" + +PRESENT_VS = """ +#version 330 +out vec2 v_uv; +void main() { // fullscreen triangle from gl_VertexID + vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2)); + v_uv = p; + gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); +} +""" + +PRESENT_FS = """ +#version 330 +uniform sampler2D u_tex; +in vec2 v_uv; +out vec4 f_color; +void main() { // Division DAC gamma, once over the final image + vec3 c = texture(u_tex, v_uv).rgb; + f_color = vec4(pow(c, vec3(1.0 / 1.25)), 1.0); +} +""" + + +class GLRenderer(vrview.Renderer): + """Drop-in replacement for vrview.Renderer with a moderngl draw path.""" + + def __init__(self, w=832, h=512, + title="VelociRender virtual board -- FLYK live [GL]"): + import pygame + import moderngl + self.pygame = pygame + self.moderngl = moderngl + pygame.init() + sz = os.environ.get('VRVIEW_SIZE') + if sz and 'x' in sz: + try: + w, h = (int(v) for v in sz.lower().split('x')) + except ValueError: + pass + self.w, self.h = w, h + self.headless = os.environ.get('SDL_VIDEODRIVER') == 'dummy' + if self.headless: + self.screen = None + self.ctx = moderngl.create_standalone_context(require=330) + else: + pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MAJOR_VERSION, 3) + pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MINOR_VERSION, 3) + pygame.display.gl_set_attribute( + pygame.GL_CONTEXT_PROFILE_MASK, pygame.GL_CONTEXT_PROFILE_CORE) + self.screen = pygame.display.set_mode( + (w, h), pygame.OPENGL | pygame.DOUBLEBUF | pygame.RESIZABLE) + pygame.display.set_caption(title) + self.ctx = moderngl.create_context(require=330) + + ctx = self.ctx + self._fbo_tex = ctx.texture((w, h), 3) + self._fbo = ctx.framebuffer( + color_attachments=[self._fbo_tex], + depth_attachment=ctx.depth_renderbuffer((w, h))) + self._mesh_prog = ctx.program(vertex_shader=MESH_VS, fragment_shader=MESH_FS) + self._point_prog = ctx.program(vertex_shader=POINT_VS, fragment_shader=POINT_FS) + self._present_prog = ctx.program(vertex_shader=PRESENT_VS, + fragment_shader=PRESENT_FS) + self._present_vao = ctx.vertex_array(self._present_prog, []) + ctx.enable(moderngl.PROGRAM_POINT_SIZE) + + self._vbocache = {} # geom handle -> {mesh(ref), vao, vbo, ibo, n} + self._texcache = {} # id(rgb array) -> {arr(ref), tex} + self._pbuf = None # dynamic particle/sphere staging buffer + self._pvao = None + self._pcap = 0 + + self.cache = SceneCache() + self.frame = 0 + self.clock = pygame.time.Clock() + self.fps = int(os.environ.get('VRVIEW_FPS', '60')) + self.skip = 1 + self._last_ms = 0.0 + self._seq = 0 # monotonic rendered-frame counter (readback key; + # self.frame gets reset by regress per scene) + self._lf_seq = -1 + self._lf = None + self._psys = {} + self.light = np.array([0.3, 0.8, 0.5]); self.light /= np.linalg.norm(self.light) + + # ---- uniform helpers ------------------------------------------------- + def _set(self, prog, name, value): + u = prog.get(name, None) + if u is not None: + u.value = value + + def _mat(self, prog, name, m): + u = prog.get(name, None) + if u is not None: + u.write(np.ascontiguousarray(m, dtype=' self._pcap: + if self._pbuf is not None: + self._pvao.release(); self._pbuf.release() + self._pcap = max(nbytes, 4096) + self._pbuf = self.ctx.buffer(reserve=self._pcap, dynamic=True) + self._pvao = self.ctx.vertex_array( + self._point_prog, + [(self._pbuf, '3f 1f 3f 1f', 'in_pos', 'in_rad', 'in_col', 'in_fade')]) + self._pbuf.orphan(self._pcap) + self._pbuf.write(data.tobytes()) + return self._pvao + + # ---- projection -------------------------------------------------------- + @staticmethod + def _proj(vp): + """Row-vector frustum matching the software mapping: x_ndc = + (2*(x_e*zeye/z) - (x0+x1))/(x1-x0), z = -z_e, far = yon*1.05.""" + x0, x1, y0, y1 = vp['x0'], vp['x1'], vp['y0'], vp['y1'] + ze, n = vp['zeye'], vp['hither'] + f = max(vp['yon'] * 1.05, n * 2) + P = np.zeros((4, 4)) + P[0, 0] = 2 * ze / (x1 - x0) + P[1, 1] = 2 * ze / (y1 - y0) + P[2, 0] = (x0 + x1) / (x1 - x0) + P[2, 1] = (y0 + y1) / (y1 - y0) + P[2, 2] = -(f + n) / (f - n) + P[2, 3] = -1.0 + P[3, 2] = -2 * f * n / (f - n) + return P + + # ---- frame readback (readpixels replies, regress PNGs) ----------------- + @property + def last_frame(self): + if self._seq == 0: + return None + if self._lf_seq != self._seq: + raw = self._fbo.read(components=3) + arr = np.frombuffer(raw, np.uint8).reshape(self.h, self.w, 3)[::-1] + # post-gamma, like the software path's stored last_frame + arr = (np.power(arr / 255.0, 1 / 1.25) * 255).astype(np.uint8) + self._lf, self._lf_seq = arr, self._seq + return self._lf + + def pump(self): + if self.screen is None: + return + pg = self.pygame + for ev in pg.event.get(): + if ev.type == pg.QUIT: + pg.quit(); raise KeyboardInterrupt + + # ---- the draw path ------------------------------------------------------ + def draw(self, board): + pg = self.pygame + if self.screen is not None: + for ev in pg.event.get(): + if ev.type == pg.QUIT: + pg.quit(); raise KeyboardInterrupt + self.frame += 1 + if self.frame % self.skip: + self.clock.tick(self.fps) + return + import time as _t + _t0 = _t.perf_counter() + self.cache.maybe_rebuild(board) + c = self.cache + W, H = self.w, self.h + ctx, mgl = self.ctx, self.moderngl + + vp = None + if c.view is not None: + vb = board.nodes[c.view].get('body') or b'' + if len(vb) >= 96: + f = struct.unpack_from('<24f', vb, 0) + vp = {'x0': f[5], 'y0': f[6], 'x1': f[7], 'y1': f[8], 'zeye': f[9], + 'hither': max(f[12], 1e-3), 'yon': f[13], 'back': f[14:17], + 'fog_on': struct.unpack_from(' vp['fog'][0]) else 0 + + fbo = self._fbo + fbo.use() + fbo.depth_mask = True + ctx.viewport = (0, 0, W, H) + ctx.enable(mgl.DEPTH_TEST) + ctx.disable(mgl.BLEND) + back = vp['back'] + fbo.clear(back[0], back[1], back[2], depth=1.0) + + # frame-constant uniforms + prog = self._mesh_prog + if c.dirlights or c.ambient.any(): + amb, suns = c.ambient, c.dirlights + else: + amb, suns = np.full(3, 0.35), [(self.light, np.full(3, 0.65))] + suns = suns[:4] + sd = np.zeros((4, 3), ' 1: + dist = float(np.linalg.norm(M[3, :3])) + for sw_in, sw_out, lg in inst['lods']: + if sw_in <= dist < sw_out: + geoms = lg + break + for gh in geoms: + mesh = c._mesh(board, gh) + if mesh is None: + continue + if 'sphere' in mesh: + self._draw_spheres(mesh, M, c.geom_mtl.get(gh)) + continue + self._draw_geom(board, c, gh, mesh, M, Mw) + + self._draw_particles(board, V, vp, self.skip / 60.0) + + # present: FBO -> window with the DAC gamma + if self.screen is not None: + ctx.screen.use() + ww, wh = self.screen.get_size() + ctx.viewport = (0, 0, ww, wh) + ctx.disable(mgl.DEPTH_TEST) + self._fbo_tex.use(0) + self._set(self._present_prog, 'u_tex', 0) + self._present_vao.render(mgl.TRIANGLES, vertices=3) + pg.display.flip() + cam = self.cam_matrix(board) + ct = cam[3, :3] + pg.display.set_caption( + f"VelociRender virtual board [GL] -- cam ({ct[0]:.1f}, {ct[1]:.1f}, " + f"{ct[2]:.1f}) {1000.0 / max(self._last_ms, 0.1):.0f}fps/{self.skip}") + self._seq += 1 + self._last_ms = (_t.perf_counter() - _t0) * 1000 + self.skip = max(1, min(10, int(self._last_ms / 50))) + if not self.headless: + self.clock.tick(self.fps) # pace draw_scene acks (RETRACE-aware) + + def _draw_geom(self, board, c, gh, mesh, M, Mw): + """Set per-geometry uniforms and render one mesh (both passes).""" + prog = self._mesh_prog + self._mat(prog, 'u_mv', M) + self._mat(prog, 'u_model', Mw) + props = c.geom_mtl.get(gh) + self._set(prog, 'u_diffuse', + tuple(props['diffuse']) if props else (0.75, 0.78, 0.82)) + self._set(prog, 'u_emissive', + tuple(props['emissive']) if props else (0.0, 0.0, 0.0)) + self._set(prog, 'u_specular', + tuple(props['specular']) if props else (0.0, 0.0, 0.0, 0.0)) + self._set(prog, 'u_opacity', + float(props['opacity']) if props else 1.0) + self._set(prog, 'u_has_nrm', 1 if mesh['nrm'] is not None else 0) + self._set(prog, 'u_has_col', 1 if mesh['col'] is not None else 0) + + tex = c.geom_tex.get(gh) + has_tex = tex is not None and mesh['uv'] is not None + self._set(prog, 'u_has_tex', 1 if has_tex else 0) + acut = 0 + uvoff = (0.0, 0.0) + if has_tex: + texn = c.geom_texn.get(gh) + if texn is not None: + tb = board.nodes.get(texn, {}).get('body') or b'' + if len(tb) >= 48: # FLYK SCROLL u0/v0 + du/dv per + u0, v0, du, dv = struct.unpack_from('<4f', tb, 32) + if all(np.isfinite((u0, v0, du, dv))): + if getattr(board, 'munga', False): + # MUNGA: floats 10/11 are a STATIC uv offset + # (atlas positioning) -- animating them scrolled + # the BT terrain "like clouds" (user-observed) + uvoff = (u0 + du, v0 + dv) + else: + t = self.frame / 60.0 + uvoff = (u0 + du * t, v0 + dv * t) + if len(tb) >= 20 and struct.unpack_from(' cutout + self._gl_tex(tex).use(0) + self._set(prog, 'u_alpha_cut', acut) + self._set(prog, 'u_uvoff', (float(uvoff[0]), float(uvoff[1]))) + self._vao(gh, mesh)['vao'].render(self.moderngl.TRIANGLES) + + def _draw_spheres(self, mesh, M, props): + """Sphere-list geometry (vtype 0x41) as depth-tested point-sprite discs.""" + n = len(mesh['sphere']) + if n == 0: + return + col = np.array([0.8, 0.8, 0.8]) + if props is not None: + col = np.clip(props['emissive'] + props['diffuse'], 0, 1) + s = float(np.linalg.norm(M[0, :3])) + data = np.zeros((n, 8), ' life + nd = int(dead.sum()) + if nd: + rad = float(np.clip(d['size'] * 0.15, 4.0, 90.0)) + p = rng.uniform(-1, 1, (nd, 3)) * rad + p[:, 1] = rng.uniform(0, 1, nd) * rad * 0.5 + d['bias'] + d['off_y'] + nrm = np.linalg.norm(p, axis=1) + close = nrm < 8.0 + p[close] *= (8.0 / np.maximum(nrm[close], 1e-6))[:, None] + ps['pos'][dead] = eye + p + v = rng.uniform(-1, 1, (nd, 3)) * d['variance'] * max(d['velocity'], 0.5) + v[:, 1] = d['velocity'] * rng.uniform(0.6, 1.2, nd) + ps['vel'][dead] = v + ps['age'][dead] = rng.uniform(0, life * 0.5, nd) + ps['vel'][:, 1] -= d['gravity'] * 0.002 * dt + ps['pos'] += ps['vel'] * dt + ps['age'] += dt + fade = np.clip(d['o_cool'] ** (ps['age'] * 60.0), 0.05, 1.0) + cook = np.clip(np.array(d['cook']), 0, 2) + pr_world = float(np.clip(d['size'] * 0.008, 0.15, 4.0)) + data = np.zeros((n, 8), '= va-3 and ins.address <= va+1 else "" + print(f" {ins.address:#010x} {ins.mnemonic:7} {ins.op_str}{mark}") + + if arg.startswith('0x'): + va = int(arg, 16) + print(f"--- disasm @ {va:#x} ---"); disasm(va, 0, 160); return + + needle = arg.encode('latin1') + soffs = find_all(data, needle) + if not soffs: + print("string not found in data object"); return + for so in soffs[:4]: + sva = dbase + so + # show the full string + end = data.find(b'\x00', so) + s = data[so:end].decode('latin1', 'replace') + print(f'\n=== string @ {sva:#x}: "{s}" ===') + # Watcom LE stores the OBJECT-RELATIVE offset in code; the fixup adds the base. + refs = find_all(code, le32(so)) or find_all(code, le32(sva)) + if not refs: + print(" (no code references found)") + for r in refs[:6]: + rva = cbase + r + print(f" referenced by imm32 at code VA {rva:#x}:") + disasm(rva, 40, 40) + print() + +if __name__ == '__main__': + main() diff --git a/dpl3-revive/samples/BALL.B2Z b/dpl3-revive/samples/BALL.B2Z new file mode 100644 index 0000000..552a3b9 Binary files /dev/null and b/dpl3-revive/samples/BALL.B2Z differ diff --git a/dpl3-revive/samples/BALL.obj b/dpl3-revive/samples/BALL.obj new file mode 100644 index 0000000..bb2655e --- /dev/null +++ b/dpl3-revive/samples/BALL.obj @@ -0,0 +1,93 @@ +# exported from DIV-BIZ2 by b2z.py +# object: +g geogroup_0 +v 0 -1 0 +v 0.587785 -0.809017 0 +v 0.181636 -0.809017 0.559017 +v -0.475528 -0.809017 0.345492 +v -0.475528 -0.809017 -0.345491 +v 0.181636 -0.809017 -0.559017 +v 0.587785 -0.809017 0 +f 1 2 3 +f 1 3 4 +f 1 4 5 +f 1 5 6 +f 1 6 7 +v 0.951057 -0.309017 0 +v 0.587785 -0.809017 0 +v 0.293893 -0.309017 -0.904509 +v 0.181636 -0.809017 -0.559017 +v -0.769421 -0.309017 -0.559017 +v -0.475528 -0.809017 -0.345491 +v -0.769421 -0.309017 0.559017 +v -0.475528 -0.809017 0.345492 +v 0.293893 -0.309017 0.904508 +v 0.181636 -0.809017 0.559017 +v 0.951057 -0.309017 0 +v 0.587785 -0.809017 0 +f 8 9 10 +f 10 9 11 +f 10 11 12 +f 12 11 13 +f 12 13 14 +f 14 13 15 +f 14 15 16 +f 16 15 17 +f 16 17 18 +f 18 17 19 +v 0.951057 0.309017 0 +v 0.951057 -0.309017 0 +v 0.293893 0.309017 -0.904509 +v 0.293893 -0.309017 -0.904509 +v -0.769421 0.309017 -0.559017 +v -0.769421 -0.309017 -0.559017 +v -0.769421 0.309017 0.559017 +v -0.769421 -0.309017 0.559017 +v 0.293893 0.309017 0.904508 +v 0.293893 -0.309017 0.904508 +v 0.951057 0.309017 0 +v 0.951057 -0.309017 0 +f 20 21 22 +f 22 21 23 +f 22 23 24 +f 24 23 25 +f 24 25 26 +f 26 25 27 +f 26 27 28 +f 28 27 29 +f 28 29 30 +f 30 29 31 +v 0.587785 0.809017 0 +v 0.951057 0.309017 0 +v 0.181636 0.809017 -0.559017 +v 0.293893 0.309017 -0.904509 +v -0.475528 0.809017 -0.345491 +v -0.769421 0.309017 -0.559017 +v -0.475528 0.809017 0.345492 +v -0.769421 0.309017 0.559017 +v 0.181636 0.809017 0.559017 +v 0.293893 0.309017 0.904508 +v 0.587785 0.809017 0 +v 0.951057 0.309017 0 +f 32 33 34 +f 34 33 35 +f 34 35 36 +f 36 35 37 +f 36 37 38 +f 38 37 39 +f 38 39 40 +f 40 39 41 +f 40 41 42 +f 42 41 43 +v 0 1 0 +v 0.587785 0.809017 0 +v 0.181636 0.809017 -0.559017 +v -0.475528 0.809017 -0.345491 +v -0.475528 0.809017 0.345492 +v 0.181636 0.809017 0.559017 +v 0.587785 0.809017 0 +f 44 45 46 +f 44 46 47 +f 44 47 48 +f 44 48 49 +f 44 49 50 diff --git a/dpl3-revive/samples/CANAL.SVT b/dpl3-revive/samples/CANAL.SVT new file mode 100644 index 0000000..6c5d1ae Binary files /dev/null and b/dpl3-revive/samples/CANAL.SVT differ diff --git a/dpl3-revive/samples/CANAL.png b/dpl3-revive/samples/CANAL.png new file mode 100644 index 0000000..2a9d476 Binary files /dev/null and b/dpl3-revive/samples/CANAL.png differ diff --git a/dpl3-revive/samples/CANAL_avg.png b/dpl3-revive/samples/CANAL_avg.png new file mode 100644 index 0000000..f182c02 Binary files /dev/null and b/dpl3-revive/samples/CANAL_avg.png differ diff --git a/dpl3-revive/samples/CANAL_max.png b/dpl3-revive/samples/CANAL_max.png new file mode 100644 index 0000000..595814e Binary files /dev/null and b/dpl3-revive/samples/CANAL_max.png differ diff --git a/dpl3-revive/samples/CANDA_S1.B2Z b/dpl3-revive/samples/CANDA_S1.B2Z new file mode 100644 index 0000000..974a24a Binary files /dev/null and b/dpl3-revive/samples/CANDA_S1.B2Z differ diff --git a/dpl3-revive/samples/CANDA_S1.obj b/dpl3-revive/samples/CANDA_S1.obj new file mode 100644 index 0000000..51ab1d2 --- /dev/null +++ b/dpl3-revive/samples/CANDA_S1.obj @@ -0,0 +1,13 @@ +# exported from DIV-BIZ2 by b2z.py +# object: +g geogroup_0 +v 846.09 20.852 -311.497 +v 881.21 16.164 -315.164 +v 851.5 -26.582 -326.276 +vt 6.2273 0.7 +vt 6.3545 0.7 +vt 6.2273 0.5 +f 1/1 2/2 3/3 +g geogroup_1 +g geogroup_2 +g geogroup_3 diff --git a/dpl3-revive/samples/CANYONS1.B2Z b/dpl3-revive/samples/CANYONS1.B2Z new file mode 100644 index 0000000..a3b991d Binary files /dev/null and b/dpl3-revive/samples/CANYONS1.B2Z differ diff --git a/dpl3-revive/samples/FLAME64.SVT b/dpl3-revive/samples/FLAME64.SVT new file mode 100644 index 0000000..864437b Binary files /dev/null and b/dpl3-revive/samples/FLAME64.SVT differ diff --git a/dpl3-revive/samples/FLAME64.png b/dpl3-revive/samples/FLAME64.png new file mode 100644 index 0000000..a08d234 Binary files /dev/null and b/dpl3-revive/samples/FLAME64.png differ diff --git a/dpl3-revive/samples/GROUPER.SVT b/dpl3-revive/samples/GROUPER.SVT new file mode 100644 index 0000000..743f9ee Binary files /dev/null and b/dpl3-revive/samples/GROUPER.SVT differ diff --git a/dpl3-revive/samples/GROUPER.png b/dpl3-revive/samples/GROUPER.png new file mode 100644 index 0000000..decc60e Binary files /dev/null and b/dpl3-revive/samples/GROUPER.png differ diff --git a/dpl3-revive/samples/MAD1.SVT b/dpl3-revive/samples/MAD1.SVT new file mode 100644 index 0000000..288ee79 Binary files /dev/null and b/dpl3-revive/samples/MAD1.SVT differ diff --git a/dpl3-revive/samples/MAD1.png b/dpl3-revive/samples/MAD1.png new file mode 100644 index 0000000..94370b5 Binary files /dev/null and b/dpl3-revive/samples/MAD1.png differ diff --git a/dpl3-revive/samples/MAD2.SVT b/dpl3-revive/samples/MAD2.SVT new file mode 100644 index 0000000..1ce815b Binary files /dev/null and b/dpl3-revive/samples/MAD2.SVT differ diff --git a/dpl3-revive/samples/MAD2.png b/dpl3-revive/samples/MAD2.png new file mode 100644 index 0000000..9f10bd3 Binary files /dev/null and b/dpl3-revive/samples/MAD2.png differ diff --git a/dpl3-revive/samples/MAD3.SVT b/dpl3-revive/samples/MAD3.SVT new file mode 100644 index 0000000..29f0f43 Binary files /dev/null and b/dpl3-revive/samples/MAD3.SVT differ diff --git a/dpl3-revive/samples/MAD3.png b/dpl3-revive/samples/MAD3.png new file mode 100644 index 0000000..81040ce Binary files /dev/null and b/dpl3-revive/samples/MAD3.png differ diff --git a/dpl3-revive/samples/MAD4.SVT b/dpl3-revive/samples/MAD4.SVT new file mode 100644 index 0000000..cae8626 Binary files /dev/null and b/dpl3-revive/samples/MAD4.SVT differ diff --git a/dpl3-revive/samples/MAD4.png b/dpl3-revive/samples/MAD4.png new file mode 100644 index 0000000..987ff4f Binary files /dev/null and b/dpl3-revive/samples/MAD4.png differ diff --git a/dpl3-revive/samples/MADALL.SVT b/dpl3-revive/samples/MADALL.SVT new file mode 100644 index 0000000..e2b3657 Binary files /dev/null and b/dpl3-revive/samples/MADALL.SVT differ diff --git a/dpl3-revive/samples/MADALL.png b/dpl3-revive/samples/MADALL.png new file mode 100644 index 0000000..164211a Binary files /dev/null and b/dpl3-revive/samples/MADALL.png differ diff --git a/dpl3-revive/samples/MADCOL.SVT b/dpl3-revive/samples/MADCOL.SVT new file mode 100644 index 0000000..380d970 Binary files /dev/null and b/dpl3-revive/samples/MADCOL.SVT differ diff --git a/dpl3-revive/samples/MADCOL.png b/dpl3-revive/samples/MADCOL.png new file mode 100644 index 0000000..c8ccdff Binary files /dev/null and b/dpl3-revive/samples/MADCOL.png differ diff --git a/dpl3-revive/samples/PT_RED.B2Z b/dpl3-revive/samples/PT_RED.B2Z new file mode 100644 index 0000000..0622e0d Binary files /dev/null and b/dpl3-revive/samples/PT_RED.B2Z differ diff --git a/dpl3-revive/samples/PT_WHITE.B2Z b/dpl3-revive/samples/PT_WHITE.B2Z new file mode 100644 index 0000000..c94ca54 Binary files /dev/null and b/dpl3-revive/samples/PT_WHITE.B2Z differ diff --git a/dpl3-revive/samples/ROCKNOIZ.SVT b/dpl3-revive/samples/ROCKNOIZ.SVT new file mode 100644 index 0000000..0462905 Binary files /dev/null and b/dpl3-revive/samples/ROCKNOIZ.SVT differ diff --git a/dpl3-revive/samples/SHARK.SVT b/dpl3-revive/samples/SHARK.SVT new file mode 100644 index 0000000..d28a6fc Binary files /dev/null and b/dpl3-revive/samples/SHARK.SVT differ diff --git a/dpl3-revive/samples/SHARK.png b/dpl3-revive/samples/SHARK.png new file mode 100644 index 0000000..0fc6244 Binary files /dev/null and b/dpl3-revive/samples/SHARK.png differ diff --git a/dpl3-revive/samples/STEH.BSL b/dpl3-revive/samples/STEH.BSL new file mode 100644 index 0000000..836adb3 Binary files /dev/null and b/dpl3-revive/samples/STEH.BSL differ diff --git a/dpl3-revive/samples/STEH_nib0.png b/dpl3-revive/samples/STEH_nib0.png new file mode 100644 index 0000000..11bce32 Binary files /dev/null and b/dpl3-revive/samples/STEH_nib0.png differ diff --git a/dpl3-revive/samples/STEH_nib1.png b/dpl3-revive/samples/STEH_nib1.png new file mode 100644 index 0000000..11bce32 Binary files /dev/null and b/dpl3-revive/samples/STEH_nib1.png differ diff --git a/dpl3-revive/samples/STEH_nib2.png b/dpl3-revive/samples/STEH_nib2.png new file mode 100644 index 0000000..14e438e Binary files /dev/null and b/dpl3-revive/samples/STEH_nib2.png differ diff --git a/dpl3-revive/samples/STEH_nib3.png b/dpl3-revive/samples/STEH_nib3.png new file mode 100644 index 0000000..41fceaa Binary files /dev/null and b/dpl3-revive/samples/STEH_nib3.png differ diff --git a/dpl3-revive/samples/STEH_nib4.png b/dpl3-revive/samples/STEH_nib4.png new file mode 100644 index 0000000..68f79c2 Binary files /dev/null and b/dpl3-revive/samples/STEH_nib4.png differ diff --git a/dpl3-revive/samples/STEH_nib5.png b/dpl3-revive/samples/STEH_nib5.png new file mode 100644 index 0000000..af06296 Binary files /dev/null and b/dpl3-revive/samples/STEH_nib5.png differ diff --git a/dpl3-revive/samples/STEH_nib6.png b/dpl3-revive/samples/STEH_nib6.png new file mode 100644 index 0000000..aa364b6 Binary files /dev/null and b/dpl3-revive/samples/STEH_nib6.png differ diff --git a/dpl3-revive/samples/STEH_nib7.png b/dpl3-revive/samples/STEH_nib7.png new file mode 100644 index 0000000..6043671 Binary files /dev/null and b/dpl3-revive/samples/STEH_nib7.png differ diff --git a/dpl3-revive/samples/TANK.SVT b/dpl3-revive/samples/TANK.SVT new file mode 100644 index 0000000..d76e403 Binary files /dev/null and b/dpl3-revive/samples/TANK.SVT differ diff --git a/dpl3-revive/samples/TANK.png b/dpl3-revive/samples/TANK.png new file mode 100644 index 0000000..39edf32 Binary files /dev/null and b/dpl3-revive/samples/TANK.png differ diff --git a/dpl3-revive/samples/TESTBOX.B2Z b/dpl3-revive/samples/TESTBOX.B2Z new file mode 100644 index 0000000..fe78d58 Binary files /dev/null and b/dpl3-revive/samples/TESTBOX.B2Z differ diff --git a/dpl3-revive/samples/TESTBOX.obj b/dpl3-revive/samples/TESTBOX.obj new file mode 100644 index 0000000..8e0864a --- /dev/null +++ b/dpl3-revive/samples/TESTBOX.obj @@ -0,0 +1,439 @@ +# exported from DIV-BIZ2 by b2z.py +# object: +g geogroup_0 +v -4.16 -2.56 -1 +v 4.16 -2.56 -1 +v 4.16 -2.56 1 +v -4.16 -2.56 1 +v 0 0 1 +v 4.16 2.56 1 +v -4.16 2.56 1 +v 0 0 -1 +v -4.16 2.56 -1 +v 4.16 2.56 -1 +f 3 5 4 +f 5 6 7 +f 1 8 2 +f 8 9 10 +f 1 2 3 +f 1 3 4 +g geogroup_1 +v -4.16 2.56 -1 +v -4.16 2.56 1 +v 4.16 2.56 1 +v 4.16 2.56 -1 +v -4.16 -2.56 -1 +v 0 0 -1 +v 4.16 -2.56 1 +v 0 0 1 +v -4.16 -2.56 1 +v 4.16 -2.56 -1 +f 15 11 16 +f 17 13 18 +f 19 18 12 +f 20 16 14 +f 11 12 13 +f 11 13 14 +g geogroup_2 +v 4.16 -2.56 -1 +v 4.16 2.56 -1 +v 4.16 2.56 1 +v 4.16 -2.56 1 +v 0 -1.99878 1.05836 +v 0.069799 -1.99878 1.05836 +v 0.209057 -1.98904 1.05836 +v 0.347296 -1.96961 1.05836 +v 0.483844 -1.94059 1.05836 +v 0.618034 -1.90211 1.05836 +v 0.749213 -1.85437 1.05836 +v 0.876742 -1.79759 1.05836 +v 0.999999 -1.73205 1.05836 +v 1.11838 -1.65807 1.05836 +v 1.23132 -1.57602 1.05836 +v 1.33826 -1.48629 1.05836 +v 1.43868 -1.38932 1.05836 +v 1.53209 -1.28558 1.05836 +v 1.61803 -1.17557 1.05836 +v 1.6961 -1.05984 1.05836 +v 1.7659 -0.938943 1.05836 +v 1.82709 -0.813473 1.05836 +v 1.87938 -0.68404 1.05836 +v 1.92252 -0.551275 1.05836 +v 1.9563 -0.415823 1.05836 +v 1.98054 -0.278346 1.05836 +v 1.99513 -0.139513 1.05836 +v 2 0 1.05836 +v 0 0 1.05836 +v 0 1.99878 1.05836 +v -0.069794 1.99878 1.05836 +v -0.209052 1.98904 1.05836 +v -0.347292 1.96962 1.05836 +v -0.48384 1.94059 1.05836 +v -0.61803 1.90211 1.05836 +v -0.74921 1.85437 1.05836 +v -0.876739 1.79759 1.05836 +v -0.999998 1.73205 1.05836 +v -1.11838 1.65808 1.05836 +v -1.23132 1.57602 1.05836 +v -1.33826 1.48629 1.05836 +v -1.43868 1.38932 1.05836 +v -1.53209 1.28558 1.05836 +v -1.61803 1.17557 1.05836 +v -1.69609 1.05984 1.05836 +v -1.7659 0.938946 1.05836 +v -1.82709 0.813476 1.05836 +v -1.87939 0.684043 1.05836 +v -1.92252 0.551278 1.05836 +v -1.9563 0.415826 1.05836 +v -1.98054 0.278348 1.05836 +v -1.99513 0.139516 1.05836 +v -2 2e-06 1.05836 +v 0 -1.99878 -1.05836 +v 0 0 -1.05836 +v 2 0 -1.05836 +v 1.99513 -0.139513 -1.05836 +v 1.98054 -0.278346 -1.05836 +v 1.9563 -0.415823 -1.05836 +v 1.92252 -0.551275 -1.05836 +v 1.87938 -0.68404 -1.05836 +v 1.82709 -0.813473 -1.05836 +v 1.7659 -0.938943 -1.05836 +v 1.6961 -1.05984 -1.05836 +v 1.61803 -1.17557 -1.05836 +v 1.53209 -1.28558 -1.05836 +v 1.43868 -1.38932 -1.05836 +v 1.33826 -1.48629 -1.05836 +v 1.23132 -1.57602 -1.05836 +v 1.11838 -1.65807 -1.05836 +v 0.999999 -1.73205 -1.05836 +v 0.876742 -1.79759 -1.05836 +v 0.749213 -1.85437 -1.05836 +v 0.618034 -1.90211 -1.05836 +v 0.483844 -1.94059 -1.05836 +v 0.347296 -1.96961 -1.05836 +v 0.209057 -1.98904 -1.05836 +v 0.069799 -1.99878 -1.05836 +v -2 2e-06 -1.05836 +v -1.99513 0.139516 -1.05836 +v -1.98054 0.278348 -1.05836 +v -1.9563 0.415826 -1.05836 +v -1.92252 0.551278 -1.05836 +v -1.87939 0.684043 -1.05836 +v -1.82709 0.813476 -1.05836 +v -1.7659 0.938946 -1.05836 +v -1.69609 1.05984 -1.05836 +v -1.61803 1.17557 -1.05836 +v -1.53209 1.28558 -1.05836 +v -1.43868 1.38932 -1.05836 +v -1.33826 1.48629 -1.05836 +v -1.23132 1.57602 -1.05836 +v -1.11838 1.65808 -1.05836 +v -0.999998 1.73205 -1.05836 +v -0.876739 1.79759 -1.05836 +v -0.74921 1.85437 -1.05836 +v -0.61803 1.90211 -1.05836 +v -0.48384 1.94059 -1.05836 +v -0.347292 1.96962 -1.05836 +v -0.209052 1.98904 -1.05836 +v -0.069794 1.99878 -1.05836 +v 0 1.99878 -1.05836 +v -4 1 1.07838 +v -4 -1 1.07838 +v -3 0 1.07838 +v 4 1 1.07838 +v 3 0 1.07838 +v 4 -1 1.07838 +f 123 124 125 +f 126 127 128 +f 21 22 23 +f 21 23 24 +f 25 46 47 +f 25 47 48 +f 25 48 49 +f 49 70 71 +f 49 71 72 +f 49 72 73 +f 74 95 96 +f 74 96 97 +f 74 97 98 +f 75 119 120 +f 75 120 121 +f 75 121 122 +f 25 26 27 +f 25 27 28 +f 25 28 29 +f 25 29 30 +f 25 30 31 +f 25 31 32 +f 25 32 33 +f 25 33 34 +f 25 34 35 +f 25 35 36 +f 25 36 37 +f 25 37 38 +f 25 38 39 +f 25 39 40 +f 25 40 41 +f 25 41 42 +f 25 42 43 +f 25 43 44 +f 25 44 45 +f 25 45 46 +f 49 50 51 +f 49 51 52 +f 49 52 53 +f 49 53 54 +f 49 54 55 +f 49 55 56 +f 49 56 57 +f 49 57 58 +f 49 58 59 +f 49 59 60 +f 49 60 61 +f 49 61 62 +f 49 62 63 +f 49 63 64 +f 49 64 65 +f 49 65 66 +f 49 66 67 +f 49 67 68 +f 49 68 69 +f 49 69 70 +f 74 75 76 +f 74 76 77 +f 74 77 78 +f 74 78 79 +f 74 79 80 +f 74 80 81 +f 74 81 82 +f 74 82 83 +f 74 83 84 +f 74 84 85 +f 74 85 86 +f 74 86 87 +f 74 87 88 +f 74 88 89 +f 74 89 90 +f 74 90 91 +f 74 91 92 +f 74 92 93 +f 74 93 94 +f 74 94 95 +f 75 99 100 +f 75 100 101 +f 75 101 102 +f 75 102 103 +f 75 103 104 +f 75 104 105 +f 75 105 106 +f 75 106 107 +f 75 107 108 +f 75 108 109 +f 75 109 110 +f 75 110 111 +f 75 111 112 +f 75 112 113 +f 75 113 114 +f 75 114 115 +f 75 115 116 +f 75 116 117 +f 75 117 118 +f 75 118 119 +g geogroup_3 +v -4.16 -2.56 1 +v -4.16 2.56 1 +v -4.16 2.56 -1 +v -4.16 -2.56 -1 +f 129 130 131 +f 129 131 132 +g geogroup_4 +v 0 -1.99878 1.05836 +v 0 0 1.05836 +v -2 2e-06 1.05836 +v -1.99513 -0.139511 1.05836 +v -1.98054 -0.278344 1.05836 +v -1.9563 -0.415822 1.05836 +v -1.92253 -0.551273 1.05836 +v -1.87939 -0.684039 1.05836 +v -1.8271 -0.813472 1.05836 +v -1.7659 -0.938942 1.05836 +v -1.6961 -1.05984 1.05836 +v -1.61803 -1.17557 1.05836 +v -1.53209 -1.28558 1.05836 +v -1.43868 -1.38932 1.05836 +v -1.33826 -1.48629 1.05836 +v -1.23132 -1.57602 1.05836 +v -1.11839 -1.65807 1.05836 +v -1 -1.73205 1.05836 +v -0.876743 -1.79759 1.05836 +v -0.749213 -1.85437 1.05836 +v -0.618034 -1.90211 1.05836 +v -0.483844 -1.94059 1.05836 +v -0.347297 -1.96961 1.05836 +v -0.209057 -1.98904 1.05836 +v -0.069799 -1.99878 1.05836 +v 2 0 1.05836 +v 1.99513 0.139504 1.05836 +v 1.98054 0.278338 1.05836 +v 1.9563 0.415815 1.05836 +v 1.92253 0.551267 1.05836 +v 1.87939 0.684033 1.05836 +v 1.8271 0.813466 1.05836 +v 1.7659 0.938936 1.05836 +v 1.6961 1.05983 1.05836 +v 1.61804 1.17556 1.05836 +v 1.5321 1.28557 1.05836 +v 1.43869 1.38931 1.05836 +v 1.33827 1.48628 1.05836 +v 1.23133 1.57602 1.05836 +v 1.11839 1.65807 1.05836 +v 1.00001 1.73205 1.05836 +v 0.876749 1.79759 1.05836 +v 0.74922 1.85436 1.05836 +v 0.618041 1.90211 1.05836 +v 0.48385 1.94059 1.05836 +v 0.347302 1.96961 1.05836 +v 0.209063 1.98904 1.05836 +v 0.069805 1.99878 1.05836 +v 0 1.99878 1.05836 +v 0 -1.99878 -1.05836 +v -0.069799 -1.99878 -1.05836 +v -0.209057 -1.98904 -1.05836 +v -0.347297 -1.96961 -1.05836 +v -0.483844 -1.94059 -1.05836 +v -0.618034 -1.90211 -1.05836 +v -0.749213 -1.85437 -1.05836 +v -0.876743 -1.79759 -1.05836 +v -1 -1.73205 -1.05836 +v -1.11839 -1.65807 -1.05836 +v -1.23132 -1.57602 -1.05836 +v -1.33826 -1.48629 -1.05836 +v -1.43868 -1.38932 -1.05836 +v -1.53209 -1.28558 -1.05836 +v -1.61803 -1.17557 -1.05836 +v -1.6961 -1.05984 -1.05836 +v -1.7659 -0.938942 -1.05836 +v -1.8271 -0.813472 -1.05836 +v -1.87939 -0.684039 -1.05836 +v -1.92253 -0.551273 -1.05836 +v -1.9563 -0.415822 -1.05836 +v -1.98054 -0.278344 -1.05836 +v -1.99513 -0.139511 -1.05836 +v -2 2e-06 -1.05836 +v 0 0 -1.05836 +v 2 0 -1.05836 +v 0 1.99878 -1.05836 +v 0.069805 1.99878 -1.05836 +v 0.209063 1.98904 -1.05836 +v 0.347302 1.96961 -1.05836 +v 0.48385 1.94059 -1.05836 +v 0.618041 1.90211 -1.05836 +v 0.74922 1.85436 -1.05836 +v 0.876749 1.79759 -1.05836 +v 1.00001 1.73205 -1.05836 +v 1.11839 1.65807 -1.05836 +v 1.23133 1.57602 -1.05836 +v 1.33827 1.48628 -1.05836 +v 1.43869 1.38931 -1.05836 +v 1.5321 1.28557 -1.05836 +v 1.61804 1.17556 -1.05836 +v 1.6961 1.05983 -1.05836 +v 1.7659 0.938936 -1.05836 +v 1.8271 0.813466 -1.05836 +v 1.87939 0.684033 -1.05836 +v 1.92253 0.551267 -1.05836 +v 1.9563 0.415815 -1.05836 +v 1.98054 0.278338 -1.05836 +v 1.99513 0.139504 -1.05836 +f 133 154 155 +f 133 155 156 +f 133 156 157 +f 158 179 180 +f 158 180 181 +f 158 181 134 +f 182 203 204 +f 182 204 205 +f 182 205 206 +f 207 227 228 +f 207 228 229 +f 207 229 230 +f 133 134 135 +f 133 135 136 +f 133 136 137 +f 133 137 138 +f 133 138 139 +f 133 139 140 +f 133 140 141 +f 133 141 142 +f 133 142 143 +f 133 143 144 +f 133 144 145 +f 133 145 146 +f 133 146 147 +f 133 147 148 +f 133 148 149 +f 133 149 150 +f 133 150 151 +f 133 151 152 +f 133 152 153 +f 133 153 154 +f 158 159 160 +f 158 160 161 +f 158 161 162 +f 158 162 163 +f 158 163 164 +f 158 164 165 +f 158 165 166 +f 158 166 167 +f 158 167 168 +f 158 168 169 +f 158 169 170 +f 158 170 171 +f 158 171 172 +f 158 172 173 +f 158 173 174 +f 158 174 175 +f 158 175 176 +f 158 176 177 +f 158 177 178 +f 158 178 179 +f 182 183 184 +f 182 184 185 +f 182 185 186 +f 182 186 187 +f 182 187 188 +f 182 188 189 +f 182 189 190 +f 182 190 191 +f 182 191 192 +f 182 192 193 +f 182 193 194 +f 182 194 195 +f 182 195 196 +f 182 196 197 +f 182 197 198 +f 182 198 199 +f 182 199 200 +f 182 200 201 +f 182 201 202 +f 182 202 203 +f 207 206 208 +f 207 208 209 +f 207 209 210 +f 207 210 211 +f 207 211 212 +f 207 212 213 +f 207 213 214 +f 207 214 215 +f 207 215 216 +f 207 216 217 +f 207 217 218 +f 207 218 219 +f 207 219 220 +f 207 220 221 +f 207 221 222 +f 207 222 223 +f 207 223 224 +f 207 224 225 +f 207 225 226 +f 207 226 227 diff --git a/dpl3-revive/samples/VTVALL.SVT b/dpl3-revive/samples/VTVALL.SVT new file mode 100644 index 0000000..a5e5e99 Binary files /dev/null and b/dpl3-revive/samples/VTVALL.SVT differ diff --git a/dpl3-revive/samples/VTVALL.png b/dpl3-revive/samples/VTVALL.png new file mode 100644 index 0000000..c3e92ed Binary files /dev/null and b/dpl3-revive/samples/VTVALL.png differ diff --git a/dpl3-revive/samples/VTVCAB.SVT b/dpl3-revive/samples/VTVCAB.SVT new file mode 100644 index 0000000..ab1bc4e Binary files /dev/null and b/dpl3-revive/samples/VTVCAB.SVT differ diff --git a/dpl3-revive/samples/VTVCAB.png b/dpl3-revive/samples/VTVCAB.png new file mode 100644 index 0000000..b3d0249 Binary files /dev/null and b/dpl3-revive/samples/VTVCAB.png differ diff --git a/dpl3-revive/samples/VTVENG.SVT b/dpl3-revive/samples/VTVENG.SVT new file mode 100644 index 0000000..3173fac Binary files /dev/null and b/dpl3-revive/samples/VTVENG.SVT differ diff --git a/dpl3-revive/samples/VTVENG.png b/dpl3-revive/samples/VTVENG.png new file mode 100644 index 0000000..ee4e981 Binary files /dev/null and b/dpl3-revive/samples/VTVENG.png differ diff --git a/dpl3-revive/samples/VTVTOP.SVT b/dpl3-revive/samples/VTVTOP.SVT new file mode 100644 index 0000000..dd621aa Binary files /dev/null and b/dpl3-revive/samples/VTVTOP.SVT differ diff --git a/dpl3-revive/samples/VTVTOP.png b/dpl3-revive/samples/VTVTOP.png new file mode 100644 index 0000000..4399394 Binary files /dev/null and b/dpl3-revive/samples/VTVTOP.png differ diff --git a/dpl3-revive/source-ref/BASICTYP.H b/dpl3-revive/source-ref/BASICTYP.H new file mode 100644 index 0000000..baed74f --- /dev/null +++ b/dpl3-revive/source-ref/BASICTYP.H @@ -0,0 +1,83 @@ +/* + * PROJECT: DVS + * SUBSYSTEM: vl + * MODULE: basictypes.h + * + * File: $RCSfile: basictypes.h,v $ + * Revision: $Revision: 1.1 $ + * Date: $Date: 1993/07/12 08:12:20 $ + * Author: $Author: john $ + * RCS Ident: $Id: basictypes.h,v 1.1 1993/07/12 08:12:20 john Exp $ + * + * FUNCTION: + * + * This file contains the lowest level definitions needed by + * the vl library. + * + * $Log: basictypes.h,v $ + * Revision 1.1 1993/07/12 08:12:20 john + * Initial revision + * + * Revision 1.3 1992/08/19 11:50:24 jeff + * got rid of temporary bodge definition of agentlist now i've got the real thing. + * + * Revision 1.2 1992/07/27 13:30:38 jon + * Temporary fix. AgentList is uint32 not void type. + * + * Revision 1.1 1992/07/27 12:24:20 jeff + * Initial revision + * + * + * Copyright (c) 1992 Division Ltd. + * + * All Rights Reserved. + * + * This Document may not, in whole or in part, be copied, + * photocopied, reproduced, translated, or reduced to any + * electronic medium or machine readable form without prior + * written consent from Division Ltd. + */ +#ifndef _BASIC_TYPES_H +#define _BASIC_TYPES_H + + +/* + * mangle the basictypes.def file to get machine specific low level + * definitions for all our basic types. + */ +#define DEF_TYPE(a,b,c) typedef c b; +#include "basictyp.def" +#undef DEF_TYPE + +/* + * generate an enum list of all the symbolic values for the types. + */ +#define DEF_TYPE(a,b,c) a, +typedef enum BasicType { +#include "basictyp.def" +LAST_TYPE +} BasicType; +#undef DEF_TYPE +#define BASIC_TYPE_CNT ((int)LAST_TYPE) + +typedef char *String; + +/* + * A structure to define the names etc of the basic types for internal + * use. + */ +typedef struct TypeSpec{ + int8 endian16[3]; + int8 endian32[5]; + struct { + String name; + uint32 size; + uint32 alignment; + }Sizes[BASIC_TYPE_CNT]; +} *TypeSpec; + +extern void GenBasicTypes(TypeSpec spec); + +#endif /* _BASIC_TYPES_H */ + + diff --git a/dpl3-revive/source-ref/BIZREAD.C b/dpl3-revive/source-ref/BIZREAD.C new file mode 100644 index 0000000..082fbdc --- /dev/null +++ b/dpl3-revive/source-ref/BIZREAD.C @@ -0,0 +1,1950 @@ +#include +#include +#include +#include +#include "dpltypes.h" +#include "dpl.h" +#include "dpl_load.h" + +/*{{{ banner*/ +/* ************************************************** + + Copyright DIVISION Limited (c) 1994 + All rights reserved + + + File : bizread.c + Project : dpl interface + host / platform independent section + Author : PJA + Date : 26/07/94 + + Function: Implements the b2z file reader for the dpl + interface + READ THE V2Z FILE FORMAT FOR DETAILS + + History : Rev 1.1, 26 / 07 / 1994 + +**************************** */ +/*}}} */ + +/*{{{ some statics to aid the process*/ +static int32 little_endian=1; + +static char material_name[256]; +static char texture_name[256]; +static char texmap_name[256]; +static char ramp_name[256]; +static char object_name[256]; +static char patch_name[256]; + +static FILE *b2z_file=NULL; + +static float b2z_units=1.0f; +static float b2z_scale=1.0f; + +static char buffer[(1024*8)*2]; +static char *buff_p=NULL; +static int buffered_bytes=0; +static int buffer_read_start=0; +static int b2z_ok = 1; +static int blocks_read=0; +static int b2z_open = 0; + +static dpl_TEXMAP *texmap=NULL; +static dpl_TEXTURE *texture=NULL; +static dpl_MATERIAL *material=NULL, *default_mtl=NULL, + *f_mtl =NULL, *b_mtl =NULL, + *of_mtl=NULL, *ob_mtl=NULL; +static dpl_OBJECT *object=NULL; +static dpl_LOD *lod=NULL; +static dpl_GEOGROUP *geogroup=NULL; +static dpl_GEOMETRY *geometry=NULL; +static dpl_VERTEX_LIST *vertex_list=NULL; +static dpl_CONNECTION_LIST *connection_list=NULL; +static int32 draw_mode=0; + +dpl_superlist named_local_list; +/*}}} */ + +static int b2z_trace = 0; +#define trprintf if (b2z_trace) printf + +/*{{{ static void new_block ()*/ +static void new_block () +{ + if (b2z_open == 0) + b2z_ok=0; + if (b2z_ok == 0) + return; + + /* copy whats left in buffer to beginning of buffer */ + memcpy ( buffer, buff_p, buffered_bytes ); + + buff_p=buffer; + if (fread ( &buffer[buffered_bytes], 1, 8*1024, b2z_file ) == 0) + b2z_open=0; + + blocks_read++; + buffered_bytes+=8*1024; + + trprintf ("new block, now buffered_bytes=%d\n", buffered_bytes ); +} +/*}}} */ +/*{{{ buffer access with endian correction*/ +/*{{{ static char get_char ()*/ +static char get_char () +{ + char v; + if (buffered_bytes < 1) new_block(); + + v=*buff_p++; + buffered_bytes--; + + return v; +} +/*}}} */ +/*{{{ static char get_char ()*/ +static char get_int8 () +{ + int8 v; + char *vp=(char *) &v; + + if (buffered_bytes < 1) new_block(); + + *vp=*buff_p++; + buffered_bytes--; + + return v; +} +/*}}} */ +/*{{{ static int16 get_int16 ( )*/ +static int16 get_int16 ( ) +{ + int16 v; + char *cv=(char *) &v; + + if (buffered_bytes < 2) new_block(); + + if (little_endian) { + cv[0]=*buff_p++; + cv[1]=*buff_p++; + + buffered_bytes-=2; + + return v; + } + else { + cv[1]=*buff_p++; + cv[0]=*buff_p++; + + buffered_bytes-=2; + + return v; + } +} +/*}}} */ +/*{{{ static char *get_int32 ( char *b, int32 *v )*/ +static int32 get_int32 () +{ + int32 v; + char *cv=(char *) &v; + + if (buffered_bytes < 4) new_block(); + + if (little_endian) { + cv[0]=*buff_p++; + cv[1]=*buff_p++; + cv[2]=*buff_p++; + cv[3]=*buff_p++; + + buffered_bytes-=4; + + return v; + } + else { + cv[3]=*buff_p++; + cv[2]=*buff_p++; + cv[1]=*buff_p++; + cv[0]=*buff_p++; + + buffered_bytes-=4; + + return v; + } +} +/*}}} */ +/*{{{ static float32 get_float32 ()*/ +static float32 get_float32 () +{ + float32 v; + char *cv=(char *) &v; + + if (buffered_bytes < 4) new_block(); + + if (little_endian) { + cv[0]=*buff_p++; + cv[1]=*buff_p++; + cv[2]=*buff_p++; + cv[3]=*buff_p++; + + buffered_bytes-=4; + + return v; + } + else { + cv[3]=*buff_p++; + cv[2]=*buff_p++; + cv[1]=*buff_p++; + cv[0]=*buff_p++; + + buffered_bytes-=4; + + return v; + } +} +/*}}} */ +/*{{{ static void get_struct32 ( void *data, int n_words )*/ +static void get_struct32 ( void *data, int n_words ) +{ + char *cv=(char *) data; + + while (n_words) { + n_words--; + + if (buffered_bytes < 4) new_block(); + + if (little_endian) { + cv[0]=*buff_p++; + cv[1]=*buff_p++; + cv[2]=*buff_p++; + cv[3]=*buff_p++; + + buffered_bytes-=4; + + cv+=4; + } + else { + cv[3]=*buff_p++; + cv[2]=*buff_p++; + cv[1]=*buff_p++; + cv[0]=*buff_p++; + + buffered_bytes-=4; + + cv+=4; + } + } +} +/*}}} */ +/*{{{ static void get_string ( char *str, int32 length )*/ +static void get_string ( char *str, int32 length ) +{ + int8 l; + int i; + + if (length == (-1)) { + l=get_int8(); + length=(int32) l; + } + for (i=0; i> 12) & 0xc) { + case 0x8 : + block_len=get_int32(); + bytes+=4; + break; + case 0x4 : + block_len=0xffff & get_int16(); + bytes+=2; + break; + case 0x0 : + block_len=0xff & get_int8(); + bytes+=1; + break; + default : + printf ("Unrecognised block header, exiting\n" ); + b2z_ok=0; + break; + } + + trprintf ("block 0x%x of length %d bytes\n", block_hdr, block_len ); + + *hdr =block_hdr; + *length=block_len; + + return bytes; +} +/*}}} */ +/*{{{ static void skip_block ( int length )*/ +static void skip_block ( int length ) +{ + trprintf ("skip_block %d\n", length ); + + if (b2z_ok == 0) return; + + while (length > 2048) { + skip_bytes(2048); + length-=2048; + } + skip_bytes(length); +} +/*}}} */ + +/*{{{ static dpl_node *look_up ( dpl_type t, char *name, node_create_function *f )*/ +static dpl_node *look_up ( dpl_type t, char *name, node_create_function f ) +{ + dpl_node *n; + + trprintf ( "lookup %s\n", name ); + + if ((n = dpl_FindNamedTypedNode ( t, name )) == NULL) { + trprintf ( "had to create it\n" ); + n = f(); + dpl_NameNode ( n, name ); + } + else trprintf ( "found it!\n" ); + + return n; +} +/*}}} */ + +/*{{{ defaultize ( dpl_MATERIAL *default_mtl )*/ +static void +defaultize ( dpl_MATERIAL *default_mtl ) +{ + default_mtl->ambient[0]=1.0f; + default_mtl->ambient[1]=0.0f; + default_mtl->ambient[2]=0.0f; + + default_mtl->diffuse[0]=1.0f; + default_mtl->diffuse[1]=0.0f; + default_mtl->diffuse[2]=0.0f; + + default_mtl->specular[0]=0.0f; + default_mtl->specular[1]=0.0f; + default_mtl->specular[2]=0.0f; + default_mtl->specular[3]=1.0f; + + default_mtl->emissive[0]=0.0f; + default_mtl->emissive[1]=0.0f; + default_mtl->emissive[2]=0.0f; + + default_mtl->ramp =NULL; + default_mtl->texture=NULL; + + dpl_Flush ( default_mtl ); +} +/*}}} */ + +/*{{{ deal with multiple connection lists per pmesh*/ +typedef int pre_connect[7]; + +static pre_connect *pre_connections=NULL; +static int pre_connects=0; + +static pre_connect *grow_connections(int more) +{ + int prev=pre_connects; + pre_connect *new=(pre_connect *) malloc((prev+more)*sizeof(pre_connect)); + + trprintf ("grow %d connections\n", more ); + + if (pre_connections) { + memcpy (new, pre_connections, + prev*sizeof(pre_connect)); + free(pre_connections); + } + pre_connects+=more; + pre_connections=new; + + trprintf ("grew to %d connections\n", pre_connects ); + + return &new[prev]; +} + +/*}}} */ +/*{{{ static void read_vertices ( int32 n_vertices, int32 hdr )*/ +static int normal_offset; +static int texture_2doffset; +static int texture_3doffset; +static int color_offset; +static int luminance_offset; + +static void read_vertices ( int32 floats_per_vertex, int32 n_vertices, int32 hdr ) +{ + float this_vert[16]; + int i, vix; + dpl_VERTEX *vert; + dpl_VERTEX_LIST *vl; + + trprintf ("read_vertices %d\n", n_vertices ); + + vl = vertex_list; + vix = 0; + vert = &vl->vertices[0]; + + for (i=0; inormcol[3]=1.0f; + vert->texcoords[2]=1.0f; + + vert->position[0]=/* b2z_scale*b2z_units* */ this_vert[0]; + vert->position[1]=/* b2z_scale*b2z_units* */ this_vert[1]; + vert->position[2]=/* b2z_scale*b2z_units* */ this_vert[2]; + + if (normal_offset != (-1)) { + /* move normal into place */ + vert->normcol[0]=this_vert[normal_offset]; + vert->normcol[1]=this_vert[normal_offset+1]; + vert->normcol[2]=this_vert[normal_offset+2]; + } + if (texture_2doffset != (-1)) { + /* move texture coords into place */ + vert->texcoords[0]=this_vert[texture_2doffset]; + vert->texcoords[1]=this_vert[texture_2doffset+1]; + } + if (texture_3doffset != (-1)) { + /* move 3d texture coords into place */ + vert->texcoords[0]=this_vert[texture_3doffset]; + vert->texcoords[1]=this_vert[texture_3doffset+1]; + vert->texcoords[2]=this_vert[texture_3doffset+2]; + } + if (color_offset != (-1)) { + /* move cooked rgba values into place */ + vert->normcol[0]=this_vert[color_offset]; + vert->normcol[1]=this_vert[color_offset+1]; + vert->normcol[2]=this_vert[color_offset+2]; + vert->normcol[3]=this_vert[color_offset+3]; + } + if (luminance_offset != (-1)) { + /* move cooked llla values into place */ + vert->normcol[0]=this_vert[luminance_offset]; + vert->normcol[1]=this_vert[luminance_offset]; + vert->normcol[2]=this_vert[luminance_offset]; + vert->normcol[3]=this_vert[luminance_offset+1]; + } + /*}}} */ + + vix++; + if (vix==DPL_VERTICES_PER_BLOCK) { + vix=0; + vl=vl->next; + if (vl) vert=&vl->vertices[0]; + } + else vert++; + } +} +/*}}} */ +/*{{{ static int parse_vertices ( int32 length )*/ +static int parse_vertices ( int32 length, dpl_geo_type geotype ) +{ + int32 bytes=0, hdr, len, n_connections, total_connections, v_per_c, i; + dpl_CONNECTION *conn; + dpl_CONNECTION_LIST *cl; + pre_connect *pc; + int cix=0; + int pmesh_vert=0, pmesh_conn=0; + + while (b2z_ok && (bytes < length)) { + int32 f_per_vertex, n_floats, n_vertices, verts_per_poly; + + normal_offset = -1; + texture_2doffset = -1; + texture_3doffset = -1; + color_offset = -1; + luminance_offset = -1; + + bytes+=b2z_block(&hdr, &len ); + if (b2z_ok == 0) return 0; + + bytes+=len; + + n_floats=len>>2; + + switch ( hdr ) { + /*{{{ pmesh triangle connectivity*/ + case 0x0047 : + case 0x4047 : + case 0x8047 : + trprintf ( "PMESH TRIANGLE CONNECTIVITY len=%d\n", len ); + n_connections=len/12; + + pc=grow_connections(n_connections); + + for (i=0; iconnections[0]; + + cix=0; + + pc=pre_connections; + + for (i=0; in_verts=(*pc)[0]; + trprintf ( "n_verts=%d\n", conn->n_verts ); + + for (j=0; j<(*pc)[0]; j++ ) { + trprintf ( "vertex %d %d\n", j, (*pc)[j+1] ); + conn->indices[j]=(*pc)[j+1]; + } + + pc++; + cix++; + + if (cix==DPL_CONNECTIONS_PER_BLOCK) { + cix=0; + cl=cl->next; + if (cl) conn=&cl->connections[0]; + } + else conn++; + } + free(pre_connections); + pre_connections=NULL; + pre_connects=0; + + trprintf ("SETGEOMVERTICES WITH VERTEX AND CONNECTIONS\n" ); + dpl_SetGeometryVertices ( geometry, vertex_list, connection_list ); + trprintf ("did the setgeom thing\n" ); + } + } + else { + if (pmesh_vert) { + pmesh_vert=0; + dpl_SetGeometryVertices ( geometry, vertex_list, NULL ); + } + } + /* } */ + + return 1; +} +/*}}} */ +/*{{{ static int parse_patch ( int32 length )*/ +static int parse_patch ( int32 length ) +{ + int32 bytes=0, hdr, len; + char c, *name_p=patch_name; + int8 type; + + f_mtl=of_mtl; + b_mtl=ob_mtl; + + if (lod == NULL) { + lod=dpl_NewLod(); + dpl_AddLodToObject ( object, lod ); + } + + geogroup=dpl_NewGeogroup(); + dpl_AddGeogroupToLod ( lod, geogroup ); + geogroup->draw_mode=draw_mode; /* inherit from object */ + + while (b2z_ok && (bytes < length)) { + bytes+=b2z_block(&hdr, &len ); + if (b2z_ok == 0) return 0; + + bytes+=len; + + switch ( hdr ) { + /*{{{ name*/ + case 0x2008 : + trprintf ( "PATCH NAME\n" ); + while (c=get_char()) + *name_p++=c; + *name_p++=0x0; + trprintf ("patch %s\n", patch_name ); + break; + /*}}} */ + /*{{{ f_mtl*/ + case 0x2030 : + trprintf ( "FRONT MATERIAL\n" ); + type=get_int8(); + if (type == 0) + f_mtl=NULL; + else if (type == 2) + f_mtl=default_mtl; + else if (type==1) { + name_p=material_name; + while (c=get_char()) + *name_p++=c; + *name_p++=0x0; + trprintf ("material %s\n", material_name ); + + f_mtl = (dpl_MATERIAL *) look_up ( dpl_type_material, + material_name, + (node_create_function) dpl_NewMaterial ); + + name_p=patch_name; + } + + break; + /*}}} */ + /*{{{ b_mtl*/ + case 0x2031 : + trprintf ( "BACK MATERIAL\n" ); + type=get_int8(); + if (type == 0) + b_mtl=NULL; + else if (type == 2) + b_mtl=default_mtl; + else if (type==1) { + name_p=material_name; + while (c=get_char()) + *name_p++=c; + *name_p++=0x0; + trprintf ("material %s\n", material_name ); + + b_mtl = (dpl_MATERIAL *) look_up ( dpl_type_material, + material_name, + (node_create_function) dpl_NewMaterial ); + + name_p=patch_name; + } + else if (type == 3) + b_mtl=f_mtl; + break; + /*}}} */ + /*{{{ plane*/ + case 0x2032 : + trprintf ( "PLANE\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ draw_mode*/ + case 0x2033 : + trprintf ( "DRAW MODE\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ decal*/ + case 0x2034 : + trprintf ( "DECAL\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ facet*/ + case 0x2035 : + trprintf ( "FACET\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ vertex format*/ + case 0x2036 : + trprintf ( "VERTEX FORMAT\n" ); + geogroup->draw_mode=(int32) get_int8(); + trprintf ("vertex format 0x%x\n", geogroup->draw_mode ); + break; + /*}}} */ + /*{{{ voodoo*/ + case 0x2037 : + trprintf ( "SPECIAL VOODOO\n" ); + skip_block(len); + break; + /*}}} */ + + /*{{{ polygon*/ + case 0x4043 : + case 0x8043 : + case 0x0043 : + trprintf ( "POLYGON\n" ); + + parse_vertices ( len, dpl_g_type_polystrip ); + break; + /*}}} */ + /*{{{ tristrip*/ + case 0x4044 : + case 0x8044 : + case 0x0044 : + trprintf ( "TRISTRIP\n" ); + + parse_vertices ( len, dpl_g_type_tristrip ); + break; + /*}}} */ + /*{{{ polystrip*/ + case 0x4045 : + case 0x8045 : + case 0x0045 : + trprintf ( "POLYSTRIP\n" ); + + parse_vertices ( len, dpl_g_type_polystrip ); + break; + /*}}} */ + /*{{{ pmesh*/ + case 0x4046 : + case 0x8046 : + case 0x0046 : + trprintf ( "PMESH\n" ); + + parse_vertices ( len, dpl_g_type_pmesh ); + break; + + /*}}} */ + + /*{{{ sphere NOT IMPLEMENTED*/ + case 0x4048 : + case 0x8048 : + case 0x0048 : + trprintf ( "SPHERE\n" ); + skip_block(len); + break; + + /*}}} */ + /*{{{ line NOT IMPLEMENTED*/ + case 0x404a : + case 0x804a : + case 0x004a : + trprintf ( "LINE\n" ); + skip_block(len); + break; + + /*}}} */ + /*{{{ text NOT IMPLEMENTED*/ + case 0x404b : + case 0x804b : + case 0x004b : + trprintf ( "TEXT\n" ); + skip_block(len); + break; + + /*}}} */ + + default : + printf ( "Unexpected header 0x%x in patch\n", hdr ); + skip_block(len); + break; + } + } + geogroup->f_material=f_mtl; + geogroup->b_material=b_mtl; + + /* and the Bound flushes the geogroup */ + + dpl_BoundGeogroup ( geogroup ); + dpl_BoundLod ( lod ); + + return 1; +} +/*}}} */ +/*{{{ static int parse_lod ( int32 length )*/ +static int parse_lod ( int32 length ) +{ + lod=dpl_NewLod(); + dpl_AddLodToObject ( object, lod ); + + skip_block(length); + return 1; +} +/*}}} */ +/*{{{ static int parse_object ( int32 length )*/ +static int parse_object ( int32 length ) +{ + int32 bytes=0, hdr, len; + int8 type; + char c, *name_p=object_name; + + of_mtl=NULL; + ob_mtl=NULL; + + if (object == NULL) object=dpl_NewObject(); + *name_p=0x0; + + while (b2z_ok && (bytes < length)) { + bytes+=b2z_block(&hdr, &len ); + + if (b2z_ok == 0) return 0; + + switch ( hdr ) { + /*{{{ name*/ + case 0x2008 : + trprintf ( "OBJECT NAME\n" ); + while (c=get_char()) + *name_p++=c; + *name_p++=0x0; + trprintf ("object %s\n", object_name ); + break; + /*}}} */ + /*{{{ f_mtl*/ + case 0x2030 : + trprintf ( "FRONT MATERIAL\n" ); + type=get_int8(); + if (type == 0) + of_mtl=NULL; + else if (type == 2) + of_mtl=default_mtl; + else if (type==1) { + name_p=material_name; + while (c=get_char()) + *name_p++=c; + *name_p++=0x0; + trprintf ("material %s\n", material_name ); + + of_mtl = (dpl_MATERIAL *) look_up ( dpl_type_material, + material_name, + (node_create_function) dpl_NewMaterial ); + + name_p=object_name; + } + + break; + /*}}} */ + /*{{{ b_mtl*/ + case 0x2031 : + trprintf ( "BACK MATERIAL\n" ); + type=get_int8(); + if (type == 0) + ob_mtl=NULL; + else if (type == 2) + ob_mtl=default_mtl; + else if (type==1) { + name_p=material_name; + while (c=get_char()) + *name_p++=c; + *name_p++=0x0; + trprintf ("material %s\n", material_name ); + + ob_mtl = (dpl_MATERIAL *) look_up ( dpl_type_material, + material_name, + (node_create_function) dpl_NewMaterial ); + + name_p=object_name; + } + else if (type == 3) + ob_mtl=of_mtl; + break; + /*}}} */ + /*{{{ plane*/ + case 0x2032 : + trprintf ( "PLANE\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ draw mode*/ + case 0x2033 : + trprintf ( "DRAW MODE\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ decal*/ + case 0x2034 : + trprintf ( "DECAL\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ facet*/ + case 0x2035 : + trprintf ( "FACET\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ vertex format*/ + case 0x2036 : + trprintf ( "VERTEX FORMAT\n" ); + draw_mode=(int32) get_int8(); + break; + /*}}} */ + /*{{{ special*/ + case 0x2037 : + trprintf ( "SPECIAL VOODOO\n" ); + skip_block(len); + break; + + /*}}} */ + /*{{{ patch*/ + case 0x4042 : + case 0x8042 : + case 0x0042 : + trprintf ( "PATCH\n" ); + parse_patch(len); + break; + + /*}}} */ + /*{{{ lod*/ + case 0x4041 : + case 0x8041 : + case 0x0041 : + trprintf ( "LOD\n" ); + parse_lod(len); + break; + + /*}}} */ + /*{{{ comment*/ + case 0x0000 : + case 0x4000 : + case 0x8000 : + case 0x0001 : + case 0x4001 : + case 0x8001 : + case 0x2000 : + case 0x6000 : + case 0xa000 : + trprintf ( "COMMENT\n" ); + skip_block(len); + break; + + /*}}} */ + default : + printf ( "Unexpected header 0x%x in object, bytes=%d length=%d\n", + hdr, bytes, length ); + skip_block(len); + break; + } + + bytes+=len; + } + + return 1; +} +/*}}} */ +/*{{{ static int parse_texture ( int32 length )*/ +static int parse_texture ( int32 length ) +{ + /* + always create a new texture, but try to chain it to an + existing texmap + */ + dpl_TEXTURE *real_tex; + int32 bilinear, bytes=0, hdr, len; + int8 scope=0, minify=0, magnify=0, wrap_u=0, wrap_v=0, alpha=0, mode; + char c, *full_name, *name_p=texture_name; + + *name_p=0x0; + + while (b2z_ok && (bytes < length)) { + bytes+=b2z_block(&hdr, &len ); + + if (b2z_ok == 0) return 0; + + switch ( hdr ) { + /*{{{ name*/ + case 0x2008 : + trprintf ( "TEXTURE NAME\n" ); + name_p=texture_name; + while (c=get_char()) + *name_p++=c; + *name_p++=0x0; + trprintf ("texture %s\n", texture_name ); + break; + /*}}} */ + /*{{{ scope*/ + case 0x200a : + trprintf ( "SCOPE\n" ); + scope=get_int8(); + trprintf ( "SCOPE = 0x%x\n", scope ); + break; + /*}}} */ + /*{{{ file name*/ + case 0x0011 : + case 0x4011 : + case 0x8011 : + trprintf ( "TEXTURE FILE NAME\n" ); + name_p=texmap_name; + while (c=get_char()) + *name_p++=c; + *name_p++=0x0; + trprintf ("map name %s\n", texmap_name ); + break; + /*}}} */ + /*{{{ minify*/ + case 0x0012 : + trprintf ( "MINIFY\n" ); + minify=get_int8(); + trprintf ( "MODE = 0x%x\n", minify ); + break; + /*}}} */ + /*{{{ magnify*/ + case 0x0013 : + trprintf ( "MAGNIFY\n" ); + magnify=get_int8(); + trprintf ( "MODE = 0x%x\n", magnify ); + break; + /*}}} */ + /*{{{ alpha*/ + case 0x0014 : + trprintf ( "ALPHA\n" ); + alpha=get_int8(); + trprintf ( "MODE = 0x%x\n", alpha ); + break; + /*}}} */ + /*{{{ wrap u*/ + case 0x0015 : + trprintf ( "WRAP_U\n" ); + wrap_u=get_int8(); + trprintf ( "MODE = 0x%x\n", wrap_u ); + break; + /*}}} */ + /*{{{ wrap v*/ + case 0x0016 : + trprintf ( "WRAP_V\n" ); + wrap_v=get_int8(); + trprintf ( "MODE = 0x%x\n", wrap_v ); + break; + /*}}} */ + /*{{{ detail*/ + case 0x0017 : + trprintf ( "DETAIL\n" ); + mode=get_int8(); + trprintf ( "MODE = 0x%x\n", mode ); + skip_block(len-1); + break; + + /*}}} */ + default : + printf ( "Unexpected header 0x%x in texture, bytes=%d length=%d\n", + hdr, bytes, length ); + skip_block(len); + break; + } + bytes+=len; + } + + texmap = (dpl_TEXMAP *) dpl_FindNamedTypedNode ( dpl_type_texmap, + texmap_name ); + + if (texmap == NULL) { + /*{{{ load up the texture map*/ + /* first try to load the texture */ + if ((magnify == 1) || (magnify == 0)) + bilinear=0; + else + bilinear=1; + + texmap = dpl_NewTexMap (); + dpl_NameNode ( (dpl_node *) texmap, texmap_name ); + + texmap->bits_per_texel=32; /* sorry about this hack */ + + full_name=dpl_FindTextureFile ( texmap_name, NULL ); + + if (load_svt((dpl_node *) texmap, full_name )) { + int *map_texels=texmap->texels; + /* this is HORRIBLE - i will fix it later....... */ + texmap->texels=NULL; + + trprintf ("went load_svt, have u_size %d v_size %d\n", + texmap->u_size, texmap->v_size ); + + dpl_SetTexmapTexels ( texmap, + map_texels, + texmap->u_size, + texmap->v_size, + bilinear ); + + /* and the call to SetTexMapTexels has flushed the node */ + } + /*}}} */ + } + + real_tex = (dpl_TEXTURE *) look_up ( dpl_type_texture, + texture_name, + (node_create_function) dpl_NewTexture ); + + real_tex->minify = minify; + real_tex->magnify = magnify; + real_tex->alpha = alpha; + real_tex->wrap_u = wrap_u; + real_tex->wrap_v = wrap_v; + real_tex->texmap = texmap; + + dpl_Flush(real_tex); + + return 1; +} +/*}}} */ +/*{{{ static int parse_material ( int32 length )*/ + +static int parse_material ( int32 length ) +{ + dpl_MATERIAL local_mtl, *real_mtl; + dpl_TEXTURE *mtl_texture=NULL; + dpl_RAMP *mtl_ramp=NULL; + int32 bytes=0, hdr, len; + int8 scope, mode; + char c, *name_p=material_name; + float32 r, g, b, pse; + + *name_p=0x0; + + /*{{{ init local_material*/ + local_mtl.ambient[0]=1.0f; + local_mtl.ambient[1]=1.0f; + local_mtl.ambient[2]=1.0f; + + local_mtl.diffuse[0]=1.0f; + local_mtl.diffuse[1]=1.0f; + local_mtl.diffuse[2]=1.0f; + + local_mtl.emissive[0]=1.0f; + local_mtl.emissive[1]=1.0f; + local_mtl.emissive[2]=1.0f; + + local_mtl.specular[0]=0.0f; + local_mtl.specular[1]=0.0f; + local_mtl.specular[2]=0.0f; + local_mtl.specular[3]=0.0f; + + local_mtl.opacity[0]=1.0f; + local_mtl.opacity[1]=1.0f; + local_mtl.opacity[2]=1.0f; + + /*}}} */ + + while (b2z_ok && (bytes < length)) { + bytes+=b2z_block(&hdr, &len ); + + if (b2z_ok == 0) return 0; + + switch ( hdr ) { + /*{{{ name*/ + case 0x2008 : + trprintf ( "MATERIAL NAME\n" ); + while (c=get_char()) + *name_p++=c; + *name_p++=0x0; + trprintf ("material %s\n", material_name ); + break; + /*}}} */ + /*{{{ scope*/ + case 0x200a : + trprintf ( "SCOPE\n" ); + scope=get_int8(); + trprintf ( "SCOPE = 0x%x\n", scope ); + break; + /*}}} */ + /*{{{ texture*/ + case 0x0021 : + trprintf ( "TEXTURE TYPE NAME\n" ); + mode=get_int8(); + trprintf ( " MODE = 0x%x\n", mode ); + if (mode == 0) + texture=NULL; + else if (mode == 1) + texture=NULL; + else if (mode == 2) { + name_p=texture_name; + while (c=get_char()) + *name_p++=c; + *name_p++=0x0; + trprintf ("texture %s\n", texture_name ); + + mtl_texture = (dpl_TEXTURE *) look_up ( dpl_type_texture, + texture_name, + (node_create_function) dpl_NewTexture ); + name_p=material_name; + } + break; + /*}}} */ + /*{{{ environment*/ + case 0x0022 : + trprintf ( "ENVIRONMENT\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ ambient*/ + case 0x0023 : + trprintf ( "AMBIENT\n" ); + local_mtl.ambient[0]=get_float32(); + local_mtl.ambient[1]=get_float32(); + local_mtl.ambient[2]=get_float32(); + trprintf ( "is %f %f %f\n", + local_mtl.ambient[0], + local_mtl.ambient[1], + local_mtl.ambient[2] ); + break; + /*}}} */ + /*{{{ diffuse*/ + case 0x0024 : + trprintf ( "DIFFUSE\n" ); + local_mtl.diffuse[0]=get_float32(); + local_mtl.diffuse[1]=get_float32(); + local_mtl.diffuse[2]=get_float32(); + trprintf ( "is %f %f %f\n", + local_mtl.diffuse[0], + local_mtl.diffuse[1], + local_mtl.diffuse[2] ); + break; + /*}}} */ + /*{{{ specular*/ + case 0x0025 : + trprintf ( "SPECULAR\n" ); + local_mtl.specular[0]=get_float32(); + local_mtl.specular[1]=get_float32(); + local_mtl.specular[2]=get_float32(); + local_mtl.specular[3]=get_float32(); + trprintf ( "is %f %f %f\n", + local_mtl.specular[0], + local_mtl.specular[1], + local_mtl.specular[2] ); + break; + /*}}} */ + /*{{{ emissive*/ + case 0x0026 : + trprintf ( "EMISSIVE\n" ); + local_mtl.emissive[0]=get_float32(); + local_mtl.emissive[1]=get_float32(); + local_mtl.emissive[2]=get_float32(); + trprintf ( "is %f %f %f\n", + local_mtl.emissive[0], + local_mtl.emissive[1], + local_mtl.emissive[2] ); + break; + /*}}} */ + /*{{{ opacity*/ + case 0x0027 : + trprintf ( "OPACITY\n" ); + local_mtl.opacity[0]=get_float32(); + local_mtl.opacity[1]=get_float32(); + local_mtl.opacity[2]=get_float32(); + break; + + /*}}} */ + /*{{{ ramp*/ + case 0x0028 : + trprintf ( "RAMP\n" ); + name_p=ramp_name; + while (c=get_char()) + *name_p++=c; + *name_p++=0x0; + trprintf ("ramp %s\n", texture_name ); + + mtl_ramp = (dpl_RAMP *) look_up ( dpl_type_ramp, + ramp_name, + (node_create_function) dpl_NewRamp ); + name_p=material_name; + break; + /*}}} */ + default : + printf ( "Unexpected header 0x%x in material, bytes=%d length=%d\n", + hdr, bytes, length ); + skip_block(len); + break; + } + bytes+=len; + } + real_mtl = (dpl_MATERIAL *) look_up ( dpl_type_material, + material_name, + (node_create_function) dpl_NewMaterial ); + + memcpy ( real_mtl->emissive, local_mtl.emissive, 3*sizeof(float32)); + memcpy ( real_mtl->ambient, local_mtl.ambient, 3*sizeof(float32)); + memcpy ( real_mtl->diffuse, local_mtl.diffuse, 3*sizeof(float32)); + memcpy ( real_mtl->opacity, local_mtl.opacity, 3*sizeof(float32)); + memcpy ( real_mtl->specular, local_mtl.specular, 4*sizeof(float32)); + real_mtl->texture=mtl_texture; + real_mtl->ramp=mtl_ramp; + + dpl_Flush ( real_mtl ); + + return 1; +} +/*}}} */ +/*{{{ static int parse_ramp ( int32 length )*/ +static int parse_ramp ( int32 length ) +{ + dpl_RAMP local_ramp, *real_ramp; + dpl_node *node; + + int32 bytes=0, hdr, len; + int8 scope, mode; + float32 r, g, b; + char c, *name_p=material_name; + *name_p=0x0; + + while (b2z_ok && (bytes < length)) { + bytes+=b2z_block(&hdr, &len ); + + if (b2z_ok == 0) return 0; + + switch ( hdr ) { + /*{{{ name*/ + case 0x2008 : + trprintf ( "RAMP NAME\n" ); + while (c=get_char()) + *name_p++=c; + *name_p++=0x0; + trprintf ("ramp %s\n", ramp_name ); + break; + /*}}} */ + /*{{{ scope*/ + case 0x200a : + trprintf ( "SCOPE\n" ); + scope=get_int8(); + trprintf ( "SCOPE = 0x%x\n", scope ); + break; + /*}}} */ + /*{{{ ramp data*/ + case 0x0031 : + trprintf ( "RAMP DATA\n" ); + local_ramp.color0[0]=get_float32(); + local_ramp.color0[1]=get_float32(); + local_ramp.color0[2]=get_float32(); + + local_ramp.color1[0]=get_float32(); + local_ramp.color1[1]=get_float32(); + local_ramp.color1[2]=get_float32(); + + break; + + /*}}} */ + default : + printf ( "Unexpected header 0x%x in ramp, bytes=%d length=%d\n", + hdr, bytes, length ); + skip_block(len); + break; + } + bytes+=len; + } + real_ramp = (dpl_RAMP *) look_up ( dpl_type_ramp, + ramp_name, + (node_create_function) dpl_NewRamp ); + + memcpy ( real_ramp->color0, local_ramp.color0, 3*sizeof(float)); + memcpy ( real_ramp->color1, local_ramp.color1, 3*sizeof(float)); + + dpl_Flush ( real_ramp ); + + return 1; +} +/*}}} */ +/*{{{ static int parse_header ( int32 length )*/ +static int parse_header ( int32 length ) +{ + int32 bytes=0, hdr, len; + int8 precision, unit; + + while (b2z_ok && (bytes < length)) { + bytes+=b2z_block(&hdr, &len ); + if (b2z_ok == 0) return 0; + + bytes+=len; + switch ( hdr ) { + /*{{{ version*/ + case 0x2002 : + trprintf ( "VERSION\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ date*/ + case 0x2003 : + trprintf ( "DATE\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ time*/ + case 0x2004 : + trprintf ( "TIME\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ scale*/ + case 0x2005 : + trprintf ( "SCALE\n" ); + b2z_scale=get_float32(); + break; + /*}}} */ + /*{{{ precision*/ + case 0x2006 : + trprintf ( "PRECISION\n" ); + precision=get_int8(); + if (precision != 0) { + printf ("Error, cannot process double-precision files\n" ); + b2z_ok=0; + return 0; + } + break; + /*}}} */ + /*{{{ filetype*/ + case 0x2007 : + trprintf ( "FILETYPE\n" ); + skip_block(len); + break; + /*}}} */ + /*{{{ unit*/ + case 0x2009 : + trprintf ( "UNIT\n" ); + unit=get_int8(); + if (unit == 0x0) + b2z_units=1.0f; + else if (unit == 0x1) + b2z_units=1.0f / 25.4f; + else { + printf ("Error, unrecognized units 0x%x\n", unit ); + b2z_ok=0; + return 0; + } + break; + /*}}} */ + default : + printf ("Unrecognized block 0x%x in header\n", hdr ); + skip_block(len); + break; + } + } + trprintf ("Exiting parse_header\n" ); + + return 1; +} +/*}}} */ +/*{{{ static void forget_local_names ()*/ +static void forget_local_names () +{ + /* should walk down named_local_list and forget all the names */ + return; +} +/*}}} */ + +/*{{{ dpl_OBJECT *b2zread ( char *f )*/ +dpl_OBJECT * +b2zread ( dpl_OBJECT *param_object, char *f ) +{ + FILE *fp=fopen(f, "rb" ); + char biz_id[16]; + + /* determine endian-ness */ + little_endian=0; + *((char *) &little_endian)=0x1; + little_endian&=0x1; + + if (fp==NULL) { + printf ("Failed to open file %s\n", f ); + return NULL; + } + else { + /*{{{ do some inits*/ + b2z_open = 1; + + b2z_file = fp; + buff_p = NULL; + buffered_bytes = 0; + buffer_read_start= 0; + little_endian = 1; + b2z_ok = 1; + blocks_read = 0; + + texmap = NULL; + texture = NULL; + material = NULL; + object = param_object; + lod = NULL; + geogroup = NULL; + geometry = NULL; + vertex_list = NULL; + connection_list = NULL; + + named_local_list.tail=NULL; + named_local_list.head=NULL; + named_local_list.last_accessed=NULL; + /*}}} */ + + get_string ( biz_id, 8 ); + if (strcmp ( biz_id, "DIV-BIZ2") != 0) { + printf ( "Not a valid b2z file\n" ); + fclose(fp); + return NULL; + } + else { + int32 block_hdr; + int32 length; + + /*{{{ create a default material*/ + default_mtl = (dpl_MATERIAL *) look_up ( dpl_type_material, + "DEFAULT", + (node_create_function) dpl_NewMaterial ); + + defaultize ( default_mtl ); + /*}}} */ + + while (b2z_ok) { + b2z_block ( &block_hdr, &length ); + + switch (block_hdr & 0xfff) { + /*{{{ trailer*/ + case 0x005 : + trprintf ("Trailer, exiting\n" ); + fclose(fp); + if (b2z_ok) { + forget_local_names(); + return object; + } + else { + forget_local_names(); + return NULL; + } + break; + + /*}}} */ + /*{{{ header*/ + case 0x003 : + trprintf ("Header\n" ); + parse_header ( length ); + break; + + /*}}} */ + /*{{{ comment*/ + case 0x004 : + trprintf ("Comment\n" ); + skip_block ( length ); + break; + + /*}}} */ + /*{{{ texture*/ + case 0x010 : + trprintf ("Texture\n" ); + parse_texture ( length ); + break; + + /*}}} */ + /*{{{ material*/ + case 0x020 : + trprintf ("Material\n" ); + parse_material ( length ); + break; + + /*}}} */ + /*{{{ ramp*/ + case 0x030 : + trprintf ("Ramp\n" ); + parse_ramp ( length ); + break; + + /*}}} */ + /*{{{ object*/ + case 0x040 : + trprintf ("Object\n" ); + parse_object ( length ); + break; + + /*}}} */ + default : + skip_block ( length ); + printf ("ERROR : unexpected tag 0x%x\n", block_hdr ); + fclose(fp); + return NULL; + break; + } + } + if (b2z_ok) { + fclose(fp); + return object; + } + else + return NULL; + } + } +} +/*}}} */ + diff --git a/dpl3-revive/source-ref/BIZREAD.H b/dpl3-revive/source-ref/BIZREAD.H new file mode 100644 index 0000000..441ca49 --- /dev/null +++ b/dpl3-revive/source-ref/BIZREAD.H @@ -0,0 +1,26 @@ +#ifndef bizread_h +#define bizread_h + +/* ************************************************** + + Copyright DIVISION Limited (c) 1994 + All rights reserved + + + File : bizread.h + Project : dpl interface + Author : PJA + Date : 18/6/94 + + Function: declares b2zread + + + History : Rev 1.0, 18 / 06 / 1994 + +**************************** */ + +#include "dpltypes.h" + +extern dpl_OBJECT *b2zread ( char *f ); + +#endif diff --git a/dpl3-revive/source-ref/DPL.H b/dpl3-revive/source-ref/DPL.H new file mode 100644 index 0000000..4e1a713 --- /dev/null +++ b/dpl3-revive/source-ref/DPL.H @@ -0,0 +1,634 @@ +#ifndef dpl_h +#define dpl_h + +/* ************************************************** + + Copyright DIVISION Limited (c) 1994 + All rights reserved + + + File : dpl.h + Project : dpl interface + Author : PJA + Date : 18/6/94 + + Function: Introduces all the datatypes and functions + used by the dpl interface + + History : Rev 1.0, 18 / 06 / 1994 + +**************************** */ + +#include "dpltypes.h" + +/*{{{ DPL API ====> list management*/ +extern void +dpl_AddZoneToScene ( dpl_ZONE *z ); +extern void +dpl_AddViewToScene ( dpl_VIEW *v ); +extern void +dpl_AddZoneToView ( dpl_VIEW *v, dpl_ZONE *z ); +extern void +dpl_AddLightToLmodel ( dpl_LMODEL *lm, dpl_LIGHT *l ); +extern void +dpl_AddLodToObject ( dpl_OBJECT *o, dpl_LOD *lod ); +extern void +dpl_AddGeogroupToLod ( dpl_LOD *l, dpl_GEOGROUP *gg ); +extern void +dpl_AddGeometryToGeogroup ( dpl_GEOGROUP *gg, dpl_GEOMETRY *g ); +extern void +dpl_AddPathToFilepath ( dpl_FILEPATH *fp, dpl_PATHITEM *pi ); +extern void +dpl_AddExtToFilepath ( dpl_FILEPATH *fp, dpl_EXTNITEM *ei ); + +extern void +dpl_RemoveZoneFromScene ( dpl_ZONE *z ); +extern void +dpl_RemoveViewFromScene ( dpl_VIEW *v ); +extern void +dpl_RemoveZoneFromView ( dpl_VIEW *v, dpl_ZONE *z ); +extern void +dpl_RemoveLightFromLmodel ( dpl_LMODEL *lm, dpl_LIGHT *l ); +extern void +dpl_RemoveLodFromObject ( dpl_OBJECT *o, dpl_LOD *lod ); +extern void +dpl_RemoveGeogroupFromLod ( dpl_LOD *l, dpl_GEOGROUP *gg ); +extern void +dpl_RemoveGeometryFromGeogroup ( dpl_GEOGROUP *gg, dpl_GEOMETRY *g ); +extern void +dpl_RemovePathFromFilepath ( dpl_FILEPATH *fp, dpl_PATHITEM *pi ); +extern void +dpl_RemoveExtFromFilepath ( dpl_FILEPATH *fp, dpl_EXTNITEM *ei ); +/*}}} */ +/*{{{ DPL API ====> tree management*/ +extern void +dpl_NestDCS ( dpl_DCS *parent, dpl_DCS *child ); + +extern void +dpl_LinkDCS ( dpl_DCS *bro, dpl_DCS *sis ); + +extern void +dpl_PruneDCS ( dpl_DCS *node ); + +/*}}} */ + +/*{{{ DPL API ====> SCENE functions*/ + +extern void +dpl_DrawScene ( int32 double_buffered ); + +extern int32 +dpl_DrawSceneComplete ( void ); + +extern void +dpl_WaitSceneComplete ( void ); + +extern dpl_ZONE * +dpl_GetSceneFirstZone ( void ); + +extern dpl_ZONE * +dpl_GetSceneNextZone ( void ); + +/*}}} */ +/*{{{ DPL API ====> ZONE functions*/ + +extern dpl_ZONE * +dpl_NewZone ( void ); + +extern void +dpl_DeleteZone ( dpl_ZONE *z ); + +extern dpl_DCS * +dpl_GetZoneRootDcs ( dpl_ZONE *z ); + +extern void +dpl_SetZoneRootDcs ( dpl_ZONE *z, dpl_DCS *h ); + +extern dpl_LMODEL * +dpl_GetZoneLmodel ( dpl_ZONE *z ); + +extern void +dpl_SetZoneLmodel ( dpl_ZONE *z, dpl_LMODEL *lm ); + +extern int32 +dpl_GetZoneEnable ( dpl_ZONE *z ); + +extern void +dpl_SetZoneEnable ( dpl_ZONE *z, int32 i ); + +/*}}} */ +/*{{{ DPL API ====> DCS functions*/ + +extern dpl_DCS * +dpl_NewDcs ( void ); + +extern void +dpl_DeleteDcs ( dpl_DCS *h ); + +extern void +dpl_GetDcsLinkage ( dpl_DCS *node, dpl_DCS **parent, dpl_DCS **child, dpl_DCS **sibling ); + +extern void +dpl_SetDcsNode ( dpl_DCS *hnode, dpl_node *cnode ); + +#define dpl_SetDcsLight(a,b) dpl_SetDcsNode(a,&(b)->dplnode) +#define dpl_SetDcsInstance(a,b) dpl_SetDcsNode(a,&(b)->dplnode) + +extern dpl_node * +dpl_GetDcsNode ( dpl_DCS *hnode ); + +extern dpl_type +dpl_GetDcsNodeType ( dpl_DCS *hnode ); + +extern void +dpl_SetDcsMatrix ( dpl_DCS *hnode, dpl_MATRIX *m ); + +extern void +dpl_GetDcsMatrix ( dpl_DCS *hnode, dpl_MATRIX m ) ; + + +extern void +dpl_SetDcsNodeEnable ( dpl_DCS *hnode, int32 enable ); + +extern void +dpl_SetDcsSubtreeEnable ( dpl_DCS *hnode, int32 enable ); + +extern int32 +dpl_GetDcsNodeEnable ( dpl_DCS *hnode ); + +extern int32 +dpl_GetDcsSubtreeEnable ( dpl_DCS *hnode ); + +/*}}} */ +/*{{{ DPL API ====> INSTANCE functions*/ + +extern dpl_INSTANCE * +dpl_NewInstance ( void ); + +extern void +dpl_DeleteInstance ( dpl_INSTANCE *i ); + +extern void +dpl_SetInstanceObject ( dpl_INSTANCE *i, dpl_OBJECT *o ); + +extern dpl_OBJECT * +dpl_GetInstanceObject ( dpl_INSTANCE *i ); + +extern void +dpl_SetInstanceFrontMaterial ( dpl_INSTANCE * i, dpl_MATERIAL *m ); + +extern dpl_MATERIAL * +dpl_GetInstanceFrontMaterial ( dpl_INSTANCE *i ); + +extern void +dpl_SetInstanceBackMaterial ( dpl_INSTANCE *i, dpl_MATERIAL *m ); + +extern dpl_MATERIAL * +dpl_GetInstanceBackMaterial ( dpl_INSTANCE *i ); + +extern void +dpl_SetInstanceFrontTexture ( dpl_INSTANCE *i, dpl_TEXTURE *t ); + +extern dpl_TEXTURE * +dpl_GetInstanceFrontTexture ( dpl_INSTANCE *i ); + +extern void +dpl_SetInstanceBackTexture ( dpl_INSTANCE *i, dpl_TEXTURE *t ); + +extern dpl_TEXTURE * +dpl_GetInstanceBackTexture ( dpl_INSTANCE *i ); + +extern void +dpl_SetInstanceForceLOD ( dpl_INSTANCE *i, dpl_LOD *l ); + +extern dpl_LOD * +dpl_GetInstanceForceLOD ( dpl_INSTANCE *i ); + +extern void +dpl_SetInstanceBillboard ( dpl_INSTANCE *i, int32 b ); + +extern int32 +dpl_GetInstanceBillboard ( dpl_INSTANCE *i ); + +extern void +dpl_SetInstanceIntersect ( dpl_INSTANCE *i, dpl_isect_mode m ); + +extern dpl_isect_mode +dpl_GetInstanceIntersect ( dpl_INSTANCE *i ); + +/*}}} */ +/*{{{ DPL API ====> LIGHT functions*/ + +extern dpl_LMODEL * +dpl_NewLmodel ( void ); + +extern void +dpl_DeleteLmodel ( dpl_LMODEL *lm ); + +extern dpl_LIGHT * +dpl_GetLmodelFirstLight ( dpl_LMODEL *lm ); + +extern dpl_LIGHT * +dpl_GetLmodelNextLight ( dpl_LMODEL *lm ); + +extern dpl_LIGHT * +dpl_NewLight ( void ); + +extern void +dpl_DeleteLight ( dpl_LIGHT *l ); + +extern void +dpl_SetLightType ( dpl_LIGHT *l, dpl_light_type t ); + +extern dpl_light_type +dpl_GetLightType ( dpl_LIGHT *l ); + +extern void +dpl_SetLightColor ( dpl_LIGHT *l, float32 r, float32 g, float32 b ); + +extern void +dpl_GetLightColor ( dpl_LIGHT *l, float32 *r, float32 *g, float32 *b ); + +extern void +dpl_SetLightRadii ( dpl_LIGHT *l, float32 r0, float32 r1 ); + +extern void +dpl_GetLightRadii ( dpl_LIGHT *l, float32 *r0,float32 *r1 ); + +extern void +dpl_SetLightUmbra ( dpl_LIGHT *l, float32 th1, float32 th2 ); + +extern void +dpl_GetLightUmbra ( dpl_LIGHT *l, float32 *th1, float32 *th2 ); + +/*}}} */ +/*{{{ DPL API ====> VIEW functions*/ + +extern dpl_VIEW * +dpl_NewView ( void ); + +extern void +dpl_DeleteView ( dpl_VIEW *v ); + +extern void +dpl_SetViewMatrix ( dpl_VIEW *v, dpl_MATRIX m ); + +extern void +dpl_GetViewMatrix ( dpl_VIEW *v, dpl_MATRIX m ); + +extern void +dpl_SetViewClipPlanes ( dpl_VIEW *v, float32 hither, float32 yon ); + +extern void +dpl_GetViewClipPlanes ( dpl_VIEW *v, float32 *hither, float32 *yon ); + +extern void +dpl_SetViewBackGround ( dpl_VIEW *v, float32 r, float32 g, float32 b ); + +extern void +dpl_GetViewBackGround ( dpl_VIEW *v, float32 *r, float32 *g, float32 *b ); + +extern void +dpl_SetViewFog ( dpl_VIEW *v, int32 enable, + float32 r, float32 g, float32 b, + float32 neer, float32 phar ); + +extern void +dpl_GetViewFog ( dpl_VIEW *v, int32 *enable, + float32 *r, float32 *g, float32 *b, + float32 *neer, float32 *phar ); + +extern void +dpl_SetViewProjection ( dpl_VIEW *v, + float32 x_size, float32 y_size, + float32 x0, float32 y0, + float32 x1, float32 y1, + float32 zeye ); + +extern void +dpl_GetViewProjection ( dpl_VIEW *v, + float32 *x_size, float32 *y_size, + float32 *x0, float32 *y0, + float32 *x1, float32 *y1, + float32 *zeye ); + +/*}}} */ +/*{{{ DPL API ====> MATERIAL functions*/ + +extern dpl_MATERIAL * +dpl_NewMaterial ( void ); + +extern void +dpl_DeleteMaterial ( dpl_MATERIAL *m ); + +extern void +dpl_SetMaterialAmbient ( dpl_MATERIAL *m, float32 r, float32 g, float32 b ); + +extern void +dpl_GetMaterialAmbient ( dpl_MATERIAL *m, float32 *r, float32 *g, float32 *b ); + +extern void +dpl_SetMaterialEmissive ( dpl_MATERIAL *m, float32 r, float32 g, float32 b ); + +extern void +dpl_GetMaterialEmissive ( dpl_MATERIAL *m, float32 *r, float32 *g, float32 *b ); + +extern void +dpl_SetMaterialDiffuse ( dpl_MATERIAL *m, float32 r, float32 g, float32 b ); + +extern void +dpl_GetMaterialDiffuse ( dpl_MATERIAL *m, float32 *r, float32 *g, float32 *b ); + +extern void +dpl_SetMaterialSpecular ( dpl_MATERIAL *m, float32 r, float32 g, float32 b, float32 shininess ); + +extern void +dpl_GetMaterialSpecular ( dpl_MATERIAL *m, float32 *r, float32 *g, float32 *b, float32 *shininess ); + +extern void +dpl_SetMaterialOpacity ( dpl_MATERIAL *m, float32 r, float32 g, float32 b ); + +extern void +dpl_GetMaterialOpacity ( dpl_MATERIAL *m, float32 *r, float32 *g, float32 *b ); + +extern void +dpl_SetMaterialTexture ( dpl_MATERIAL *m, dpl_TEXTURE *t ); + +extern dpl_TEXTURE * +dpl_GetMaterialTexture ( dpl_MATERIAL *m ); + +/*}}} */ +/*{{{ DPL API ====> TEXTURE functions*/ + +extern dpl_TEXMAP * +dpl_NewTexMap ( void ); + +extern void +dpl_DeleteTexMap ( dpl_TEXMAP *tm ); + +extern void +dpl_SetTexMapEdgeSize ( dpl_TEXMAP *tm, int32 u, int32 v ); + +extern void +dpl_GetTexMapEdgeSize ( dpl_TEXMAP *tm, int32 *u, int32 *v ); + +extern void +dpl_SetTexMapTexelSize ( dpl_TEXMAP *tm, int32 bytes_per_pixel ); + +extern int32 +dpl_GetTexMapTexelSize ( dpl_TEXMAP *tm ); + +extern void +dpl_SetTexMapTexels ( dpl_TEXMAP *tm, + int32 *texels, + int32 u_size, + int32 v_size, + int32 mode ); + +extern dpl_TEXTURE * +dpl_NewTexture ( void ); + +extern void +dpl_DeleteTexture ( dpl_TEXTURE *t ); + +extern void +dpl_SetTextureMode ( dpl_TEXTURE *t, int32 m ); + +extern int32 +dpl_GetTextureMode ( dpl_TEXTURE *t ); + +extern int32 +dpl_LoadTexmap ( dpl_TEXMAP *t, char *fname ); +/*}}} */ +/*{{{ DPL API ====> RAMP functions*/ +extern dpl_RAMP * +dpl_NewRamp ( void ); + +extern void +dpl_DeleteRamp ( dpl_RAMP *m ); + +extern void +dpl_SetRampColors ( dpl_RAMP *m, + float r0, float g0, float b0, + float r1, float g1, float b1 ); + +extern void +dpl_GetRampColors ( dpl_RAMP *m, float *r0, float *g0, float *b0, + float *r1, float *g1, float *b1 ); +/*}}} */ +/*{{{ DPL API ====> FILEPATH CONTROL functions*/ + +extern dpl_FILEPATH * +dpl_NewFilePath ( void ); + +extern dpl_PATHITEM * +dpl_NewPathItem ( void ); + +extern dpl_EXTNITEM * +dpl_NewExtnItem ( void ); + +extern void +dpl_DeleteFilePath ( dpl_FILEPATH *fp ); + +extern void +dpl_DeletePathItem ( dpl_PATHITEM *fp ); + +extern void +dpl_DeleteExtnItem ( dpl_PATHITEM *fp ); + +extern void +dpl_SetPathItemPath ( dpl_PATHITEM *i, char *s ); + +extern char * +dpl_GetPathItemPath ( dpl_PATHITEM *p ); + +extern void +dpl_SetExtnItemExtn ( dpl_EXTNITEM *p, char *s ); + +extern char * +dpl_GetExtnItemExtn ( dpl_EXTNITEM *p ); + +extern void +dpl_SetExtnItemLoadFunc ( dpl_EXTNITEM *p, load_function l ); + +extern load_function +dpl_GetExtnItemLoadFunc ( dpl_EXTNITEM *p ); + +extern void +dpl_SetGeometryFilePath ( dpl_FILEPATH *fp ); + +extern dpl_FILEPATH * +dpl_GetGeometryFilePath ( void ); + +extern void +dpl_SetTextureFilePath ( dpl_FILEPATH *fp ); + +extern dpl_FILEPATH * +dpl_GetTextureFilePath ( void ); + +extern char * +dpl_FindFile ( dpl_FILEPATH *fp, char *root_name, load_function *ext_func ); + +extern char * +dpl_FindGeometryFile ( char *root_name, load_function *ext_func ); + +extern char * +dpl_FindTextureFile ( char *root_name, load_function *ext_func ); +/*}}} */ +/*{{{ DPL API ====> GEOMETRY functions*/ + +extern dpl_OBJECT * +dpl_NewObject ( void ); + +extern dpl_LOD * +dpl_NewLod ( void ); + +extern dpl_GEOGROUP * +dpl_NewGeogroup ( void ); + +extern dpl_GEOMETRY * +dpl_NewGeometry ( void ); + +extern dpl_LOD * +dpl_GetObjectFirstLod ( dpl_OBJECT *o ); + +extern dpl_LOD * +dpl_GetObjectNextLod ( dpl_OBJECT *o ); + +extern void +dpl_DeleteObject ( dpl_OBJECT *o ); + +extern void +dpl_DeleteLod ( dpl_LOD *o ); + +extern void +dpl_DeleteGeogroup ( dpl_GEOGROUP *o ); + +extern void +dpl_DeleteGeometry ( dpl_GEOMETRY *o ); + +extern dpl_geo_type +dpl_GetGeometryGeotype ( dpl_GEOMETRY *g ); + +extern void +dpl_SetGeometryGeotype ( dpl_GEOMETRY *g, dpl_geo_type gt ); + +extern int32 +dpl_SetGeometryVertices ( dpl_GEOMETRY *g, + dpl_VERTEX_LIST *v, + dpl_CONNECTION_LIST *con ); + +extern int32 +dpl_GetGeometryNumVertices ( dpl_GEOMETRY *g ); + +extern dpl_VERTEX * +dpl_GetGeometryVertex ( dpl_GEOMETRY *g, int vertex_index ); + +extern int32 +dpl_GetGeometryNumConnections ( dpl_GEOMETRY *g ); + +extern dpl_CONNECTION * +dpl_GetGeometryConnection ( dpl_GEOMETRY *g, int vertex_index ); + +extern dpl_VERTEX_LIST * +dpl_NewVertices ( int n_vertices ); + +extern dpl_CONNECTION_LIST * +dpl_NewConnections ( int n_connections ); + +extern void +dpl_BoundLod ( dpl_LOD *lod ); + +extern void +dpl_BoundGeogroup ( dpl_GEOGROUP *gg ); + +extern int32 +dpl_LoadObject ( dpl_OBJECT *o, char *fname ); + +extern void +dpl_MorphObject ( dpl_OBJECT *morphed, + dpl_OBJECT *a, + dpl_OBJECT *b, float32 alpha ); + +/*}}} */ +/*{{{ DPL API ====> NAMETABLE ACCESS functions*/ +extern dpl_NAME * +dpl_NewName ( void ); + +extern void +dpl_DeleteName ( dpl_NAME *n ); + +extern void +dpl_SetNameString ( dpl_NAME *n, char * s ); + +extern void +dpl_SetNameNode ( dpl_NAME *n, dpl_node *dn ); + +extern void +dpl_AddNameToNametable ( dpl_NAME *n ); + +extern void +dpl_RemoveNameFromNametable ( dpl_NAME *n ); + +extern void +dpl_NameNode ( dpl_node *n, char *s ); + +extern dpl_node * +dpl_FindNamedNode ( char *str ); + +extern dpl_node * +dpl_FindNamedTypedNode ( dpl_type t, char *str ); + +extern void +dpl_ClearNameTable ( void ); + +extern char * +dpl_TypeToString ( dpl_type t ); +/*}}} */ +/*{{{ DPL API ====> GENERAL CONTROL functions*/ +extern void +dpl_SetWarningLevel ( int32 w ); + +extern int32 +dpl_GetWarningLevel ( void ); + +extern int32 +dpl_Init ( char *dpl_arg ); + +extern void +dpl_Exit ( int32 exit_code ); + +extern void +dpl_SetCacheMode ( dpl_cache_mode m ); + +extern dpl_cache_mode +dpl_GetCacheMode (); + +extern dpl_STATISTICS +dpl_Statistics ( void ); + +extern dpl_VERSION +dpl_Version ( void ); + +extern char * +dpl_Status ( void ); + +extern void +dpl_ReadFrameStore ( int32 *pixels, int32 x, int32 y, int32 n_pixels ); + +extern dpl_INSTANCE * +dpl_SectVector ( dpl_POINT sect, + float32 x0, float32 y0, float32 z0, + float32 x1, float32 y1, float32 z1 ); + +extern dpl_INSTANCE * +dpl_SectPixel ( dpl_POINT sect, float32 x, float32 y ); + + +/*}}} */ + +#define dpl_Flush(n) (((dpl_node *)(n))->flush)((dpl_node *)(n)) +#define dpl_Hide(n) (((dpl_node *)(n))->hide)((dpl_node *)(n)) +#define dpl_Unhide(n) (((dpl_node *)(n))->unhide)((dpl_node *)(n)) + +#endif diff --git a/dpl3-revive/source-ref/DPLTYPES.H b/dpl3-revive/source-ref/DPLTYPES.H new file mode 100644 index 0000000..17c1cae --- /dev/null +++ b/dpl3-revive/source-ref/DPLTYPES.H @@ -0,0 +1,501 @@ +#ifndef dpltyp_h +#define dpltyp_h + +/* ************************************************** + + Copyright DIVISION Limited (c) 1994 + All rights reserved + + + File : dpltypes.h + Project : dpl interface + Author : PJA + Date : 18/6/94 + + Function: Introduces all the datatypes + + History : Rev 1.0, 18 / 06 / 1994 + 1.1, 22 / 06 / 1994 fixed unimplementable lists by + introducing dpl_superlist + 1.2, 27 / 06 / 1994 dislocated heirarchy from instances + and dropped back to matrix-only DCS + + **************************** */ + +#include "basictyp.h" + +typedef enum dpl_type { + dpl_type_error, + dpl_type_scene, + dpl_type_zone, + dpl_type_view, + dpl_type_instance, + dpl_type_dcs, + dpl_type_lmodel, + dpl_type_light, + dpl_type_object, + dpl_type_lod, + dpl_type_geogroup, + dpl_type_geometry, + dpl_type_material, + dpl_type_texmap, + dpl_type_texture, + dpl_type_ramp, + dpl_type_filepath, + dpl_type_pathitem, + dpl_type_extnitem, + dpl_type_nameitem +} dpl_type; + +typedef enum dpl_light_type { + dpl_light_type_error, + dpl_light_type_disable, + dpl_light_type_ambient, + dpl_light_type_directional, + dpl_light_type_point, + dpl_light_type_conical +} dpl_light_type; + +typedef enum dpl_cache_mode { + dpl_cache_mode_error, + dpl_cache_mode_write_back, + dpl_cache_mode_write_thru +} dpl_cache_mode; + +typedef enum dpl_geo_type { + dpl_g_type_error, + dpl_g_type_empty, + dpl_g_type_tristrip, + dpl_g_type_polystrip, + dpl_g_type_pmesh, + dpl_g_type_spheres, + dpl_g_type_string, + dpl_g_type_lines, + dpl_g_type_polyline +} dpl_geo_type; + +typedef enum dpl_isect_mode { + dpl_isect_mode_error, + dpl_isect_mode_none, + dpl_isect_mode_obj, + dpl_isect_mode_geogroup, + dpl_isect_mode_geometry +} dpl_isect_mode; + +/* forward declare everything - it gets circular down there */ + +#define dpl_arg_sep '~' + +#define dcs_node_enable 0x1 +#define dcs_subtree_enable 0x2 + +#define dpl_draw_faceted 0x00 +#define dpl_draw_smooth 0x01 +#define dpl_draw_colored 0x02 +#define dpl_draw_luminance 0x04 +#define dpl_draw_textured 0x08 +#define dpl_draw_3dtextured 0x10 +#define dpl_draw_front 0x200 +#define dpl_draw_back 0x400 + +#define dpl_X 0 +#define dpl_Y 1 +#define dpl_Z 2 +#define dpl_W 3 + +#ifdef MS_DOS +#define dpl_path_separator '\\' +#else +#define dpl_path_separator '/' +#endif + +/* + NOTE in rpc implementations, the rather large overhead + of the dpl_node dplnode is never passed over to the + remote renderer, its just in the host space for management +*/ + +typedef int32 (*node_function)( struct s_dplnode *); + +typedef struct s_dplnode { + void *app_specific; + int32 hidden; + node_function ghost_create; + node_function ghost_delete; + node_function flush; + node_function hide; + node_function unhide; + int32 ref_count; + void *remote; + dpl_type type_check; +} dpl_node; + +typedef dpl_node* (*node_create_function)( void ); + +typedef struct s_dplremote_node { + void *remote; + dpl_type type_check; +} dpl_remote_node; + +#ifdef REMOTE +#define dpl_node dpl_remote_node +#endif + +typedef int32 (*load_function)( dpl_node *, char * ); + +typedef struct s_dpllist { + dpl_node *item; + dpl_type type_check; + struct s_dpllist *next; + struct s_dpllist *prev; +} dpl_list; + +/* + the dpl_superlist is used as the head node for + dpl_lists +*/ + +typedef struct s_dplsuperlist { + dpl_node *dplnode; + + dpl_list *last_accessed; + dpl_list *head; + dpl_list *tail; +} dpl_superlist; + +typedef struct s_dplscene { + dpl_node dplnode; + + dpl_superlist zone_list; + dpl_superlist view_list; +} dpl_SCENE; + +/* MATRICES are embedded in VIEWs and in HNODEs */ + +typedef float32 dpl_POINT [4]; +typedef float32 dpl_MATRIX[4][4]; + +/* **************************** + the HNODE is a heirarchy node, a general binary + tree contruction node. It contains links up, down and + along the tree, plus a local data pointer, the dpl_node* + + the dpl_node* in here may be NULL, in which case + the HNODE is operating as an articulation point, + it may be a dpl_INSTANCE, or it may be a dpl_LIGHT. + any other node type in an HNODE is illegal, and + strictly enforceable + + The renderer's cull phase must traverse the whole tree, + in order that lights embedded in the tree have their location / + direction cosine correctly set. We may still cull the rendering + of sub-trees by clearing the sub-tree enable bit of the HNODE. + +*/ + +typedef struct s_dpldcs { + dpl_node dplnode; + + dpl_node *node; + dpl_MATRIX matrix; + int32 enable; + int32 identity; /* a traversal optimization */ + struct s_dpldcs *parent; + struct s_dpldcs *sibling; + struct s_dpldcs *child; +} dpl_DCS; + +typedef struct s_dplview { + dpl_node dplnode; + + dpl_MATRIX matrix; + int32 enable; + float32 x0, y0, x1, y1, zeye; + float32 x_size, y_size; + float32 hither_clip; + float32 yon_clip; + float32 back_color[3]; + int32 fog_enable; + float32 fog[5]; /* near, phar, r, g, b */ + dpl_superlist zone_list; +} dpl_VIEW; + + +typedef struct s_dpllmodel { + dpl_node dplnode; + + dpl_superlist light_list; +} dpl_LMODEL; + +typedef struct s_dpllight { + dpl_node dplnode; + + dpl_DCS *dcs; + int32 light_type; + float32 color [3]; + float32 radius [2]; /* for radial lights */ + float32 umbra [2]; /* for conical lights */ +#ifdef REMOTE + float32 position[3]; +#endif +} dpl_LIGHT; + +typedef struct s_dplzone { + dpl_node dplnode; + + dpl_DCS *root; + dpl_LMODEL *lmodel; + int32 enable; +} dpl_ZONE; + +typedef struct s_dpltexture { + dpl_node dplnode; + + struct s_dpltexmap *texmap; + + int32 minify; + int32 magnify; + int32 alpha; + int32 wrap_u; + int32 wrap_v; + int32 detail; + float32 u0; + float32 v0; + float32 du; + float32 dv; + float32 animate_time; + int32 animate_behaviour; +} dpl_TEXTURE; + +typedef struct s_dpltexmap { + dpl_node dplnode; + + int32 *texels; + int32 u_size; + int32 v_size; + int32 bits_per_texel; +#if REMOTE + int32 hwareSize; + int32 hwareOffs; + int32 bilinear; +#endif +} dpl_TEXMAP; + + +typedef struct s_dplramp { + dpl_node dplnode; + + float32 color0[3]; + float32 color1[3]; +} dpl_RAMP; + +typedef struct s_dplmtl { + dpl_node dplnode; + + struct s_dpltexture *texture; + float32 emissive [3]; + float32 ambient [3]; + float32 diffuse [3]; + float32 opacity [3]; + float32 specular [4]; + dpl_RAMP *ramp; +} dpl_MATERIAL; + + +typedef struct s_dplinstance { + dpl_node dplnode; + + dpl_DCS *dcs; + int32 billboard; + dpl_isect_mode intersectmode; + struct s_dplobject *object; + struct s_dpllod *forcelod; + struct s_dplmtl *f_material; + struct s_dplmtl *b_material; + struct s_dpltexture *f_texture; + struct s_dpltexture *b_texture; +#if REMOTE + int32 frame_count; + int32 last_LOD_index; +#endif +} dpl_INSTANCE; + +typedef struct s_dplobject { + dpl_node dplnode; + + dpl_superlist lod_list; +} dpl_OBJECT; + +typedef struct s_dpllod { + dpl_node dplnode; + + float32 bounds[2][4]; + dpl_OBJECT *parent; + dpl_superlist geogroup_list; + float32 switch_in; + float32 switch_out; +} dpl_LOD; + +typedef struct s_dplgeogroup { + dpl_node dplnode; + + float32 bounds[2][4]; + dpl_LOD *parent; + int32 draw_mode; + dpl_superlist geometry_list; + dpl_MATERIAL *f_material; + dpl_MATERIAL *b_material; +} dpl_GEOGROUP; + +/* NBB these geometry primitives are NOT supersets of dpl_node */ + +typedef struct s_vert { + dpl_POINT position; /* x, y, z, w */ + dpl_POINT normcol; /* r, g, b alpha */ + float32 texcoords [3]; + struct s_vert *next; +} dpl_HOST_VERTEX; + +typedef struct s_rvert { + dpl_POINT position; /* 0 */ + dpl_POINT normcol; /* 16 */ + float32 texcoords [3]; /* 32 */ + struct s_rvert *next; /* 44 */ + dpl_POINT xform_posn; /* 48 */ + float32 rendered_color[3]; /* 64 */ + int32 touched; /* 76 */ +} dpl_REMOTE_VERTEX; + +typedef struct s_rconn { + struct s_rconn *next; + int32 n_verts; + int32 indices[6]; /* allow up to hexagons, make structure 8 words long */ +} dpl_HOST_CONNECTION; + +typedef struct s_conn { + struct s_conn *next; /* 0 */ + int32 n_verts; /* 4 */ + dpl_REMOTE_VERTEX* indices[6]; /* 8 */ + dpl_POINT planeEqn; /* 32 */ + float32 rendered_color[3]; /* 48 */ + int32 touched; /* 60 */ +} dpl_REMOTE_CONNECTION; + +#define DPL_VERTICES_PER_BLOCK 8 +#define DPL_CONNECTIONS_PER_BLOCK 8 + +typedef struct s_vert_list { + struct s_vert_list *next; + int32 n_vertices; + dpl_HOST_VERTEX vertices[DPL_VERTICES_PER_BLOCK]; +} dpl_HOST_VERTEX_LIST; + +typedef struct s_conn_list { + struct s_conn_list *next; + int32 n_connections; + dpl_HOST_CONNECTION connections[DPL_CONNECTIONS_PER_BLOCK]; +} dpl_HOST_CONNECTION_LIST; + +typedef struct s_rvert_list { + struct s_rvert_list *next; + int32 n_vertices; + dpl_REMOTE_VERTEX vertices[DPL_VERTICES_PER_BLOCK]; +} dpl_REMOTE_VERTEX_LIST; + +typedef struct s_rconn_list { + struct s_rconn_list *next; + int32 n_connections; + dpl_REMOTE_CONNECTION connections[DPL_CONNECTIONS_PER_BLOCK]; +} dpl_REMOTE_CONNECTION_LIST; + +#if REMOTE +#define dpl_VERTEX dpl_REMOTE_VERTEX +#define dpl_VERTEX_LIST dpl_REMOTE_VERTEX_LIST +#define dpl_CONNECTION dpl_REMOTE_CONNECTION +#define dpl_CONNECTION_LIST dpl_REMOTE_CONNECTION_LIST +#else +#define dpl_VERTEX dpl_HOST_VERTEX +#define dpl_VERTEX_LIST dpl_HOST_VERTEX_LIST +#define dpl_CONNECTION dpl_HOST_CONNECTION +#define dpl_CONNECTION_LIST dpl_HOST_CONNECTION_LIST +#endif + + +typedef struct s_dplgeometry { + dpl_node dplnode; + + dpl_geo_type geometry_type; + + dpl_GEOGROUP *parent; + dpl_CONNECTION_LIST *connections; + dpl_VERTEX_LIST *vertices; +#if REMOTE + int32 variable_alpha; +#endif + +} dpl_GEOMETRY; + +typedef struct s_dplnameitem { + dpl_node dplnode; + + char *name; + dpl_node *item; +} dpl_NAME; + +typedef struct s_dplpathitem { + dpl_node dplnode; + + char *path; +} dpl_PATHITEM; + +typedef struct s_dplextnitem { + dpl_node dplnode; + + char *extn; + load_function load_ext; +} dpl_EXTNITEM; + +typedef struct s_dpl_filepath { + dpl_node dplnode; + + dpl_superlist path_list; + dpl_superlist extn_list; +} dpl_FILEPATH; + +/* ****************************************** + + BEWARE ! ! ! ! ! + + FOR REASONS OF STORAGE AND APPLICATIONS EFFICIENCY, THE FOLLOWING + DATA TYPES ARE N O T SUPERSETS OF DPL_NODE + + BE CAREFUL DEALING WITH VERTICES + + */ + + + +/* typedefs for tracing performance */ + +typedef struct s_dpl_statistics { + float32 polys_in_last_frame; + float32 last_edit_time; + float32 last_cull_time; + float32 last_frame_time; + float32 last_frame_time_sb; + float32 polys_per_second; + float32 polys_per_second_sb; +} dpl_STATISTICS; + +typedef struct s_dpl_version { + int32 host; + int32 major_version; + int32 minor_version; + int32 firmware_major_version; + int32 firmware_minor_version; + char detailed[256]; +} dpl_VERSION; + +typedef void (*vfptr)(); + +#endif diff --git a/dpl3-revive/source-ref/DPL_LOAD.C b/dpl3-revive/source-ref/DPL_LOAD.C new file mode 100644 index 0000000..552f134 --- /dev/null +++ b/dpl3-revive/source-ref/DPL_LOAD.C @@ -0,0 +1,259 @@ +#include +#include +#include +#include +#include +#include + +/*{{{ banner*/ +/* ************************************************** + + Copyright DIVISION Limited (c) 1994 + All rights reserved + + + File : dpl_load.c + Project : dpl interface object loading support + host / platform independent section + Author : PJA + Date : 22/06/94 + + Function: Implements the host section of the dpl + b2z object loader + + History : Rev 1.1, 22 / 06 / 1994 + +**************************** */ +/*}}} */ + +/*{{{ load_b2z ( char *full_name )*/ +int32 +load_b2z ( dpl_node *head_node, char *full_name ) +{ + extern dpl_OBJECT * + b2zread ( dpl_OBJECT *param_object, char *f ); + + return (b2zread ((dpl_OBJECT *) head_node, full_name ) != NULL); +} +/*}}} */ + +/*{{{ load_svt ( char *full_name )*/ +static char *worst_case_texels=NULL; + +int32 +load_svt ( dpl_node *head_node, char *full_name ) +{ + #if 0 + /*{{{ static int parse_texture ( int32 length )*/ + static int parse_texture ( int32 length ) + { + /* + always create a new texture, but try to chain it to an + existing texmap + */ + dpl_TEXTURE *real_tex; + int32 bilinear, bytes=0, hdr, len; + int8 scope=0, minify=0, magnify=0, wrap_u=0, wrap_v=0, alpha=0, mode; + char c, *name_p=texture_name; + + *name_p=0x0; + + while (b2z_ok && (bytes < length)) { + bytes+=b2z_block(&hdr, &len ); + + if (b2z_ok == 0) return 0; + + switch ( hdr ) { + /*{{{ name*/ + case 0x2008 : + trprintf ( "TEXTURE NAME\n" ); + name_p=texture_name; + while (c=get_char()) + *name_p++=c; + trprintf ("texture %s\n", texture_name ); + break; + /*}}} */ + /*{{{ scope*/ + case 0x200a : + trprintf ( "SCOPE\n" ); + scope=get_int8(); + trprintf ( "SCOPE = 0x%x\n", scope ); + break; + /*}}} */ + /*{{{ file name*/ + case 0x0011 : + case 0x4011 : + case 0x8011 : + trprintf ( "TEXTURE FILE NAME\n" ); + name_p=texmap_name; + while (c=get_char()) + *name_p++=c; + trprintf ("map name %s\n", texmap_name ); + break; + /*}}} */ + /*{{{ minify*/ + case 0x0012 : + trprintf ( "MINIFY\n" ); + minify=get_int8(); + trprintf ( "MODE = 0x%x\n", mode ); + break; + /*}}} */ + /*{{{ magnify*/ + case 0x0013 : + trprintf ( "MAGNIFY\n" ); + magnify=get_int8(); + trprintf ( "MODE = 0x%x\n", mode ); + break; + /*}}} */ + /*{{{ alpha*/ + case 0x0014 : + trprintf ( "ALPHA\n" ); + alpha=get_int8(); + trprintf ( "MODE = 0x%x\n", mode ); + break; + /*}}} */ + /*{{{ wrap u*/ + case 0x0015 : + trprintf ( "WRAP_U\n" ); + wrap_u=get_int8(); + trprintf ( "MODE = 0x%x\n", mode ); + break; + /*}}} */ + /*{{{ wrap v*/ + case 0x0016 : + trprintf ( "WRAP_V\n" ); + wrap_v=get_int8(); + trprintf ( "MODE = 0x%x\n", mode ); + break; + /*}}} */ + /*{{{ detail*/ + case 0x0017 : + trprintf ( "DETAIL\n" ); + mode=get_int8(); + trprintf ( "MODE = 0x%x\n", mode ); + skip_block(len-1); + break; + + /*}}} */ + default : + printf ( "Unexpected header 0x%x in texture, bytes=%d length=%d\n", + hdr, bytes, length ); + skip_block(len); + break; + } + bytes+=len; + } + + texmap = (dpl_TEXMAP *) dpl_FindNamedTypedNode ( dpl_type_texmap, + texmap_name ); + + if (texmap == NULL) { + /*{{{ load up the texture map*/ + /* first try to load the texture */ + if ((magnify == 1) || (magnify == 0)) + bilinear=0; + else + bilinear=1; + + texmap = dpl_NewTexMap (); + dpl_NameNode ( (dpl_node *) texmap, texmap_name ); + + texmap->bits_per_texel=32; /* sorry about this hack */ + + if (load_svt((dpl_node *) texmap, texmap_name)) { + int *map_texels=texmap->texels; + /* this is HORRIBLE - i will fix it later....... */ + texmap->texels=NULL; + + trprintf ("went load_svt, have u_size %d v_size %d\n", + texmap->u_size, texmap->v_size ); + + dpl_SetTexmapTexels ( texmap, + map_texels, + texmap->u_size, + texmap->v_size, + bilinear ); + + /* and the call to SetTexMapTexels has flushed the node */ + /* HACK TO SAVE MEMORY */ + + free(texmap->texels); + texmap->texels=NULL; + } + /*}}} */ + } + + real_tex = (dpl_TEXTURE *) look_up ( dpl_type_texture, + texture_name, + (node_create_function) dpl_NewTexture ); + + real_tex->minify = minify; + real_tex->magnify = magnify; + real_tex->alpha = alpha; + real_tex->wrap_u = wrap_u; + real_tex->wrap_v = wrap_v; + real_tex->texmap = texmap; + + dpl_Flush(real_tex); + + return 1; + } + /*}}} */ + #endif + dpl_TEXMAP *map=(dpl_TEXMAP *) head_node; + FILE *fp=fopen(full_name, "rb" ); + long l_edge, texels; + int32 edge; + + if (fp==NULL) { + printf ("failed to open file %s\n", full_name ); + return 0; + } + else { + fseek ( fp, 0L, SEEK_END ); + l_edge=ftell(fp); + fseek ( fp, 0L, SEEK_SET ); + + edge=128; + + if (l_edge == 16384L) edge=64; + else if (l_edge == 65536L) edge=128; + else if (l_edge == 262144L) edge=256; + else { + printf("Bad texture file size 0x%lx\n",l_edge); + fclose(fp); + return 0; + } + } + texels=l_edge; + if (worst_case_texels == NULL) { + worst_case_texels=malloc(256*256*sizeof(int)); + } + map->texels=worst_case_texels; + + if (map->texels) { + char *texelp=(char *) map->texels; + + while (texels) { + int i; + fread ( texelp, 1, 8192, fp ); + + texels-=8192; + texelp+=8192; + } + fclose(fp); + + map->u_size=edge; + map->v_size=edge; + + return 1; + } + else { + printf ("Failed to malloc space for texels in map %s\n", full_name ); + fclose (fp); + + return 0; + } +} +/*}}} */ + diff --git a/dpl3-revive/source-ref/DPL_LOAD.H b/dpl3-revive/source-ref/DPL_LOAD.H new file mode 100644 index 0000000..abb9781 --- /dev/null +++ b/dpl3-revive/source-ref/DPL_LOAD.H @@ -0,0 +1,38 @@ +#ifndef dpl_load_h +#define dpl_load_h + +#include +#include +#include +#include +#include "dpl.h" +#include "dpl_host.h" + +/*{{{ banner*/ +/* ************************************************** + + Copyright DIVISION Limited (c) 1994 + All rights reserved + + + File : dpl_load.c + Project : dpl interface object loading support + host / platform independent section + Author : PJA + Date : 22/06/94 + + Function: Implements the host section of the dpl + b2z object loader + + History : Rev 1.1, 22 / 06 / 1994 + +**************************** */ +/*}}} */ + +extern int32 +load_b2z ( dpl_node *node, char *fname ); + +extern int32 +load_svt ( dpl_node *node, char *fname ); + +#endif diff --git a/dpl3-revive/spec/B2Z_FORMAT.md b/dpl3-revive/spec/B2Z_FORMAT.md new file mode 100644 index 0000000..53c0bb8 --- /dev/null +++ b/dpl3-revive/spec/B2Z_FORMAT.md @@ -0,0 +1,188 @@ +# DIV-BIZ2 (`.B2Z` / `.V2Z`) binary geometry format + +> **The format family is wider than its extensions.** The game-side pipeline uses +> the same two encodings under different names: +> `.BGF` = binary geometry (magic `DIV-BIZ2`), `.VGF` = its text twin (`DIV-VIZ2`), +> `.BMF` = binary **material library** (a BIZ2 file with only material/texture/ramp +> blocks, no geometry), `.VMF` = its text twin. Everything below applies to all of +> them. Game-side material references are **namespaced**: `stships:eblu4_mtl` means +> material `eblu4_mtl` in library `STSHIPS.VMF`/`.BMF`. +> +> **Texture sub-block `0x018` (1996+): BITSLICE** — u8 plane index into a `.BSL` +> bit-slice texture pack (see `SVT_FORMAT.md`). Absent = slice 0. + +Reverse-engineered from `DPL3/BIZREAD.C` (© DIVISION Ltd 1994, author PJA) and +validated against real models on drive SDA4. This is the object/geometry format +consumed by the DPL3 renderer used in Virtual World Entertainment's BattleTech / +Red Planet pods. `.B2Z` and `.V2Z` share this format. + +All multi-byte values are **little-endian**. Floats are IEEE-754 single precision +(the header rejects double-precision files). There is no compression — the stream +is a flat sequence of length-delimited, tagged blocks. + +## 1. File layout + +``` +"DIV-BIZ2" 8-byte ASCII magic +* sequence of top-level blocks +``` +Parsing stops at the **trailer** block (type `0x005`). + +## 2. Block header + +Every block begins with a 16-bit header, followed by a length field whose **width +is encoded in the top nibble of the header**: + +``` +hdr = u16 +sel = (hdr >> 12) & 0xC + sel == 0x0 -> length = u8 (1 byte) top nibble 0,1,2,3 + sel == 0x4 -> length = u16 (2 bytes) top nibble 4,5,6,7 + sel == 0x8 -> length = u32 (4 bytes) top nibble 8,9,A,B +``` + +`length` counts the block **body** only (everything after the length field). The +record **type** is `hdr & 0xFFF`; the top nibble is purely the length-encoding +flag. That is why the source lists each type three times (`0x0NNN` / `0x4NNN` / +`0x8NNN`) — same record, three body sizes. + +## 3. Top-level block types (`hdr & 0xFFF`) + +| Type | Meaning | Body | +|-------|----------|------| +| 0x003 | Header | sub-blocks (§4) | +| 0x004 | Comment | opaque, skip | +| 0x005 | Trailer | end of file | +| 0x010 | Texture | sub-blocks (§7) | +| 0x020 | Material | sub-blocks (§7) | +| 0x030 | Ramp | sub-blocks (§7) | +| 0x040 | Object | sub-blocks (§5) | + +## 4. Header sub-blocks + +| Type | Name | Body | +|-------|-----------|------| +| 0x002 | version | skip | +| 0x003 | date | skip | +| 0x004 | time | skip | +| 0x005 | scale | f32 | +| 0x006 | precision | u8 (must be 0 = single) | +| 0x007 | filetype | skip | +| 0x009 | unit | u8: 0 = model units, 1 = inches (scale by 1/25.4) | + +## 5. Object sub-blocks + +| Type | Name | Body | +|-------|----------------|------| +| 0x008 | object name | NUL-terminated string | +| 0x030 | front material | material-ref (§6) | +| 0x031 | back material | material-ref (§6) | +| 0x036 | vertex format | u8 draw_mode (inherited by patches) | +| 0x041 | LOD | payload skipped by original loader | +| 0x042 | patch | sub-blocks (§8) — one geogroup | +| 0x032/33/34/35/37 | plane / draw-mode / decal / facet / voodoo | skip | +| 0x000/001 (+comment tags) | comment | skip | + +## 6. Material reference (used by 0x030 / 0x031) + +``` +type = u8 + 0 -> none (NULL) + 1 -> named: NUL-terminated material name follows + 2 -> DEFAULT material + 3 -> (back only) same as front +``` + +## 7. Material / Texture / Ramp sub-blocks + +**Material** (top-level 0x020): + +| Type | Name | Body | +|-------|----------|------| +| 0x008 | name | string | +| 0x00a | scope | u8 | +| 0x021 | texture | u8 mode; if mode==2, NUL-terminated texture name | +| 0x023 | ambient | 3×f32 | +| 0x024 | diffuse | 3×f32 | +| 0x025 | specular | 4×f32 | +| 0x026 | emissive | 3×f32 | +| 0x027 | opacity | 3×f32 | +| 0x028 | ramp | NUL-terminated ramp name | + +**Texture** (top-level 0x010): 0x008 name, 0x00a scope, 0x011 map filename +(string), 0x012 minify, 0x013 magnify, 0x014 alpha, 0x015 wrap_u, 0x016 wrap_v, +0x017 detail — the mode fields are each a u8. The bitmap itself lives in an +external `.SVT` file named by 0x011. 0x037 is the SPECIAL hook, a NUL-terminated +free-text string mirroring the text source's `SPECIAL="..."` header field — in +practice `" SCROLL u0 v0 du dv "` UV-drift rates (see SCN_FORMAT, Scrolling +textures). + +**Ramp** (top-level 0x030): 0x008 name, 0x00a scope, 0x031 ramp data +(color0 = 3×f32, color1 = 3×f32). + +## 8. Patch (geogroup) sub-blocks + +| Type | Name | Body | +|-------|----------------|------| +| 0x008 | patch name | string | +| 0x030 | front material | material-ref (§6) | +| 0x031 | back material | material-ref (§6) | +| 0x036 | vertex format | u8 draw_mode | +| 0x043 | polygon | vertex-list (§9), type = polystrip | +| 0x044 | tristrip | vertex-list (§9), type = tristrip | +| 0x045 | polystrip | vertex-list (§9), type = polystrip | +| 0x046 | pmesh | vertex-list (§9), type = pmesh | +| 0x048/04a/04b | sphere / line / text | not implemented in original; skip | +| 0x032/33/34/35/37 | plane / draw-mode / decal / facet / voodoo | skip | + +`draw_mode` bits (from `dpltypes.h`): 0x01 smooth, 0x02 colored, 0x04 luminance, +0x08 textured, 0x10 3d-textured, 0x200 front, 0x400 back. + +## 9. Vertex-list sub-blocks + +A primitive's body is itself a sequence of blocks: zero or more **connectivity** +blocks (pmesh only) plus exactly one **vertex** block. A vertex block's `length` +gives `n_floats = length / 4`; `n_vertices = n_floats / floats_per_vertex`. +Each vertex is `floats_per_vertex` consecutive f32s; components live at fixed +offsets by format: + +| Type | f/vert | position | normal | rgba | lum(la) | uv(2) | uvw(3) | +|-------|:-----:|:--------:|:------:|:----:|:-------:|:-----:|:------:| +| 0x080 | 3 | 0 | – | – | – | – | – | +| 0x081 | 6 | 0 | 3 | – | – | – | – | +| 0x082 | 7 | 0 | – | 3 | – | – | – | +| 0x083 | 10 | 0 | 3 | 6 | – | – | – | +| 0x084 | 5 | 0 | – | – | 3 | – | – | +| 0x085 | 8 | 0 | 3 | – | 6 | – | – | +| 0x088 | 5 | 0 | – | – | – | 3 | – | +| 0x089 | 8 | 0 | 3 | – | – | 6 | – | +| 0x08a | 9 | 0 | – | 3 | – | 7 | – | +| 0x08c | 7 | 0 | – | – | 3 | 5 | – | +| 0x090 | 6 | 0 | – | – | – | – | 3 | +| 0x091 | 9 | 0 | 3 | – | – | – | 6 | +| 0x092 | 10 | 0 | – | 3 | – | – | 7 | +| 0x094 | 8 | 0 | – | – | 3 | – | 5 | + +**Connectivity** (pmesh only): +- `0x047` triangle connectivity — `length/12` triangles, each = 3× i32 vertex indices. +- `0x04d` polygon connectivity — u8 `verts_per_poly`, then `(length-1)/(vpp*4)` + polygons, each = `vpp`× i32 indices. + +**Triangulation for export:** +- `tristrip`: triangles `(i, i+1, i+2)`, flipping winding on odd `i`. +- `pmesh`: fan each face `(f0, fk, fk+1)`. +- `polygon` / `polystrip`: current parser fans the block's vertices — adequate for + simple patches; a faithful polystrip walk is a known refinement (see README). + +## 10. Validation + +`parser/b2z.py` decodes the samples with no unrecognized blocks and clean +trailers. Semantic checks that confirm correctness: + +| File | Result | +|------|--------| +| PT_RED.B2Z | material diffuse = (1,0,0) → red; no geometry (a colored point) | +| PT_WHITE.B2Z | material diffuse = (1,1,1) → white | +| BALL.B2Z | 50 verts / 40 tris, bounds ≈ unit sphere at origin, has normals | +| TESTBOX.B2Z | 5 pmesh parts (blue/red/black/green/yellow), 202 tris, boxy bounds | +| CANDA_S1.B2Z | Red Planet terrain tile: redrock/water materials, inch units, UV coords | diff --git a/dpl3-revive/spec/MAP_FORMAT.md b/dpl3-revive/spec/MAP_FORMAT.md new file mode 100644 index 0000000..b6e7542 --- /dev/null +++ b/dpl3-revive/spec/MAP_FORMAT.md @@ -0,0 +1,108 @@ +# `.MAP` game maps and `.MOD` game objects + +The **actual playable game worlds** — BattleTech arenas and Red Planet tracks — +live in the game trees' `MAPS/` directories (`BTDAVE`, `BTLIVE`, `BTRAVINE`, +`RPDAVE`, `RPLIVE`), not in `.SCN` files. Reverse-engineered from the files +themselves (INI-style text; ~50 maps, ~13,000 instance placements archive-wide). + +## `.MAP` + +Sections declare assets; a `type=map` section places them: + +```ini +[hall2] ; asset declaration +type=model +modelfile=hall2.mod +[one] +type=dropzone ; pod spawn ring +dropzone= x y z rx ry rz ; rotations in RADIANS +[arena1] ; the map itself (usually named like the file) +type=map +instance=hall2 x y z rx ry rz +instancedropzone=one x y z rx ry rz +[inc] +type=include ; merge another .MAP's sections +file=other.map +``` + +- `instance=` may reference a **model or another map section** — maps nest, with + transform composition (row-vector, `Rz·Rx·Ry·T`, unit scale — the SCN transform + with radians instead of degrees). +- The root map section is the one named like the file (fallback: the map-typed + section with the most instances). +- Comments: `//`. A leading `[LAB_ONLY]` marks unreleased content. + +## `.MOD` (game object) + +```ini +[video] +object=rb3.bgf ; render geometry (may carry a node name after it) +[collision] +name=rb3_cv.sld ; collision volume (not needed for rendering) +[gamedata] +class=UnscalableTerrainClassID +; plus physics (mass, drag, friction), DamageZones, Explosion*, animation=, +; skeleton=, Subsystems= ... the full game-object layer (MechBld output) +``` + +For rendering, only `[video] object=` matters — take the first token, resolve the +`.BGF` (DIV-BIZ2 geometry, see B2Z_FORMAT.md). + +## LOD container (format evolution, 1996) + +Game-side BGFs nest their patches **inside** the LOD block (`0x041`), which the +1994 loader treated as an empty marker and skipped: + +``` +0x040 object + 0x036 vertex format + 0x041 LOD ; now a CONTAINER + 0x046 len=8 ; switch in/out distances (2 × f32) + 0x042 patch ... ; the actual geometry +``` + +A parser following the 1994 spec reads these files as *empty*. Parse the first +(highest-detail) LOD's contents; later `0x041` blocks are lower-poly alternates +and would double-draw. This unlocked ~850 game models that previously decoded to +zero geometry. + +## Animation & effects + +- A MOD's `[video] object=` list may carry **several objects** — e.g. the Red + Planet door `DRW.MOD`: `drw.bgf dr1.bgf dr2.bgf` = frame + two sliding leaves + ("number and order of objects is significant!"), plus low-poly LOD twins + (`l`, dropped for rendering). The wellhead `WH1.MOD` carries + `wh1.bgf … lazdril.bgf` — the **laser-drill beam mesh**. +- **Door motion is data**, in the `.SUB` subsystems file: + + ```ini + [door1] + Type=DoorClassID + TravelTime=13.0 ; seconds to open + DeadTime=3.0 ; dwell at each end + MotionExtent=-25.0 0.0 0.0 ; local slide vector + Collision=dr1_cv.sld ; links the subsystem to video object "dr1" + ``` + + The viewer plays this as a ping-pong slide (extent rotated into world space per + instance). The laser drill's firing is engine-driven (no data), so the viewer + gives `laz*` objects an approximate glow pulse — flagged as approximation. +- Scrolling textures play in maps too: any surface whose texture entity carries + a `SPECIAL=" SCROLL u0 v0 du dv"` hook in its material library (smoke stacks, + sky domes, beam effects) drifts its UVs — see SCN_FORMAT, Scrolling textures. +- Other animation carriers, not yet implemented: `.ANI` (INI skeletal keyframes: + framecount/framerate/skeletonfile + `.SKL` skeletons — the mech walk cycles) + and `SPECIALFX` particle-emitter definitions. + +## Asset resolution + +Prefer assets from the **same game tree** as the map (`/MODELS/*.MOD`, +`/VIDEO/**.BGF`) before the archive-wide basename index — BTDAVE, BTLIVE and +BTRAVINE each carry their own versions. + +## Validation + +`ARENA1.MAP` → the arena hall + 8 dropzones. `CITY1.MAP` → 77 placed buildings. +`BLADE.MAP` (Red Planet) → 205 placements / 24k triangles; a render from its first +dropzone shows the drop-station, canyon walls and track — the pod's-eye view. +Dropzones are drawn as small red pyramids in the archive viewer. diff --git a/dpl3-revive/spec/SCN_FORMAT.md b/dpl3-revive/spec/SCN_FORMAT.md new file mode 100644 index 0000000..afa3265 --- /dev/null +++ b/dpl3-revive/spec/SCN_FORMAT.md @@ -0,0 +1,146 @@ +# `.SCN` scene format + +Reverse-engineered from `DPL3/EXAMPLES/FLYK.C` (the flythrough viewer) and +`DPL3/MATRIX.C`. `.SCN` is **plain text**, one command per line, whitespace- +delimited. Comments start with `//` or `#`. It assembles a world by *referencing* +models (`.B2Z`) and textures (`.SVT`) by name — it embeds no geometry itself. + +## Core commands (the FLYK subset) + +These cover the overwhelming majority of every scene on the disk (`STATIC` alone +is ~5,000 of ~9,000 command lines): + +| Command | Arguments | Meaning | +|---------|-----------|---------| +| `FOG` | `z0 z1 r g b` | linear fog: start/end distance + colour | +| `BACKGND` | `r g b` | background / clear colour | +| `AMBIENT` | `r g b` | ambient light | +| `LIGHT` | `r g b ax ay az` | directional light: colour + orientation angles (deg) | +| `STATIC` | `name scale x y z ax ay az` | place a model instance | +| `DYNAMIC` | `name scale splinefile t0 dt` | model that follows a camera-path spline | +| `SSTATIC` | (as `STATIC`) | static variant | +| `SCROLL` | `texname u0 v0 du dv` | UV drift on a TEXTURE entity (see below) | + +Colours are floats in 0..1. Distances/positions are world units. Angles are in +**degrees**. `name` is resolved to a geometry file by search path (see Resolution). + +## Instance transform + +From `FLYK.C` + `MATRIX.C`. DPL uses a **row-vector** convention — a point is a row +and transforms as `p' = p · M`, with translation in the **bottom row** of the +matrix and each op post-multiplied (`M = M · op`). `STATIC` builds: + +``` +M = Rz(az) · Rx(ax) · Ry(ay) · Scale(s) · Translate(x, y, z) +p_world = [x y z 1] · M +``` + +so a point is rotated (Z, then X, then Y), scaled, then translated into place. +Rotation matrices (row-vector form, `c=cos`, `s=sin`): + +``` +Rz = [ c s 0 0] Rx = [1 0 0 0] Ry = [c 0 -s 0] + [-s c 0 0] [0 c s 0] [0 1 0 0] + [ 0 0 1 0] [0 -s c 0] [s 0 c 0] + [ 0 0 0 1] [0 0 0 1] [0 0 0 1] +``` + +`LIGHT`'s three angles orient a directional light the same way (`Rz·Rx·Ry`). + +## Asset resolution + +- **Models**: `name` → `.B2Z` (also `.BIZ`/`.V2Z`), found on a search path + (originally the `GEOMETRY` env var; here: index of the archive by basename). +- **Materials & textures (authoritative)**: a geogroup carries a *material name* + (e.g. `redrock_mtl`). The real definition lives in the model's **`.V2Z`** source + (the verbose text twin of the binary `.B2Z` — magic `DIV-VIZ2`), which inlines + its material library: + + ``` + TEXTURE(NAME=redrock_tex) { MAP {"rocknoiz"} ... } + MATERIAL(NAME=redrock_mtl) { TEXTURE {redrock_tex} DIFFUSE {0.743, 0.216, 0.202} } + ``` + + So a material = a **texture (→ `.SVT`) modulated by a DIFFUSE colour**. The + final surface colour is `texel × diffuse × lighting`. Material names are **scoped + per model** (the same name can differ between themes), so each model's own `.V2Z` + is the source of truth — `parser`/`bundle` parse it per model. + - Two kinds of texture, handled differently: + - **Full-colour painted skins** (vehicle textures like `VTVCAB`) are shown + **as-is** (tint = white); their colour lives in the texels. + - **Greyscale / intensity "noise" maps** (`CANAL`, `ROCKNOIZ`) are **tinted by + the material diffuse** — `techy` (white) × CANAL = grey concrete, `rednoise` + (red) × CANAL = red pipes. Some (e.g. `CANAL`, `VTVALL`) have a dead colour + channel and are rebuilt as luminance first. The loader auto-classifies by + saturation. + - Prefer the material's **per-part skin** `.SVT` (e.g. `vtvcab_mtl` → + `VTVCAB.SVT`) over any atlas the material names (`vtvall`) — the per-part skins + match the model UVs; the atlas tinted by diffuse does not. + - **Pair geometry and materials from the same source directory.** The same asset + exists in several theme folders (`CONVERT/THOR/`, `CONVERT/CANALS/`, …) and the + *same material name is defined with different colours* in each (THOR + `rednoise` = vivid red `0.83,0.006,0.006`; CANALS = salmon `0.69,0.41,0.40`). + Loading `.B2Z` from one folder but `.V2Z` from another mixes them and yields the + wrong colour. `bundle.v2z_for()` resolves the sibling `.V2Z` first. + - *Fallback:* only when a model has no `.V2Z` do we fall back to the old + stem/keyword heuristics. + +### Output gamma (the Division DAC) + +Material/diffuse values are **linear**. The Division i860/pxpl5 card output through a +**10-bit DAC with a gamma-1.7 encode** — from `DPL3/GAMMA.C`, which builds a +256→1023 table `out = 1023 · (i/255)^(1/1.7)`. This brightens midtones and gives the +characteristic bright/pastel look. The viewer reproduces it by applying +`colour^(1/1.7)` as the final fragment step (and to the background clear). Without +it, linear output on an sRGB display looks too dark and over-saturated. + +### Surface merging (a bug worth noting) + +When baking a scene, geometry is merged into surfaces to cut draw calls. Surfaces +**must be keyed on `(texture, diffuse-colour)`, not texture alone** — otherwise two +materials that share a texture but differ in tint (grey columns vs red pipes, both +on `CANAL.SVT`) merge into one and the second colour is lost. This was the cause of +"the pipes render grey". + +## Scrolling textures (`SCROLL`) + + SCROLL u0 v0 du dv e.g. SCROLL btfx:firesmoke1_scr_tex 0 0 0.05 -0.331 + +Animates a texture's UV coordinates — flowing water, drifting smoke, rising +fire, laser-beam flow (287 uses across the disk). `texname` addresses a +**TEXTURE entity** (not a bitmap): either `lib:name` in a named material +library (`.VMF`/`.BMF`) or a plain name from the model's own `.V2Z`/`.VGF` +source. `u0 v0` is the initial offset, `du dv` the drift rate — the viewer +plays it as UV/second (rate units are inferred; the engine source for this +path is not on the disk). + +The scene command is actually an **override**: the same data is baked into the +texture entity itself as a `SPECIAL` hook in the material source — + + TEXTURE(NAME="firesmoke1_scr_tex";SPECIAL=" SCROLL 0 0 0.05 -0.331 \0x00") + { MAP {"bintA"} } + +so a texture scrolls even in scenes/maps with no `SCROLL` line, and several +entities can share one bitmap (`bintA`) at different rates. In the binary +`.BMF` form the SPECIAL string is texture sub-block tag `0x037` (see +B2Z_FORMAT §7). Scroll is a per-texture-entity property, so the baker keys +animated surfaces on the entity, not the bitmap. + +## Extended commands (game-tool scenes) + +Larger scenes authored by the game tools use more keywords. The current loader +**recognises and skips** these; they are documented here for completeness: + +`MORPH` (animated geometry) · `SPECIALFX` (particle/effect hooks) · +`TEXTURE` / `GEOMETRY` / `MATERIAL` / `RAMP` +(explicit asset declarations) · `ZONE` / `START` / `END` (zone blocks) · `CLIP` +(near/far clip) · `VIEWANGLE` (fov) · `TRACKER` / `RETRACE` / `KEYRATE` / +`GEOMETRIZE` / `IMMUNE` / `DRAWLAST` / `NOVIEWMATRIX` / `BILLBOARD`. + +## Validation + +`parser/scn.py` parses `CANAL.SCN` → fog, background, ambient, 1 light, 47 +instances (5 unique models, all resolved). `viewer/bundle.py` assembles it into +world space and renders it: a Red Planet canal — 22 red-rock cliff segments +lining a channel, 22 struts, a VTV "mule" vehicle, rock and tech-junk — with +reddish fog. See `viewer/preview_scene.png`. diff --git a/dpl3-revive/spec/SPL_FORMAT.md b/dpl3-revive/spec/SPL_FORMAT.md new file mode 100644 index 0000000..e4aa5a7 --- /dev/null +++ b/dpl3-revive/spec/SPL_FORMAT.md @@ -0,0 +1,63 @@ +# `.SPL` camera-path spline format + +Reverse-engineered from `DPL3/EXAMPLES/SPLINE.C`. A `.SPL` drives a camera (or a +`DYNAMIC` object) along a smooth path. It is **plain text**: + +``` +N number of control points +x y z ax ay az x N -- position + euler angles (degrees) +``` + +`ax ay az` are rotations about X, Y, Z (same convention as `STATIC`/`LIGHT` in the +scene format). The path is a **closed loop** (the last point links back to the +first). + +## Interpolation + +Each of the 6 channels (x, y, z, and the three angles) is splined independently as +a **cubic Hermite** curve with **Catmull-Rom tangents**: + +``` +tangent at knot i (position): vel = (pos[i+1] - pos[i-1]) / 2 +tangent at knot i (angle): rot = wrap(ang[i+1] - ang[i]) / 2 +``` + +Per segment `i -> i+1`, with endpoint values `v0,v1` and tangents `d0,d1`, the +cubic `v(t) = a t^3 + b t^2 + c t + d` (`t` in 0..1) is: + +``` +d = v0 +c = d0 +a = d1 + d0 - 2*v1 + 2*v0 +b = v1 - v0 - d0 - a +``` + +(Angle segments additionally subtract 360 from a tangent > 360 before solving, as +in the original `solve_rot_cubic`.) `walk_spline` advances `t` by a step, rolling +over to the next/previous knot at the 0..1 boundaries — a constant-`dt` walk, so +speed follows the control-point spacing (closely spaced points = slower). + +## From a path sample to a camera + +At parameter `t` the sampled `(pos, ang)` becomes a camera basis. Build the DPL +rotation `R = Rz(az) . Rx(ax) . Ry(ay)` (row-vector; see `SCN_FORMAT.md`), then: + +``` +forward = (0,0,-1) . R # the camera looks down its local -Z +up = (0,1,0) . R +eye = pos +center = pos + forward +``` + +The `-Z` local-forward is confirmed by `CAMERA.SPL`: it starts at `(0,310,-500)` +with `ay = 180`, which rotates local `-Z` to world `+Z` — i.e. the camera looks +into the scene, which extends toward `+Z`. + +## Validation + +`parser/spl.py` evaluates `CAMERA.SPL` (20 control points) into a smooth 240-frame +loop, eye path `X[-260,230] Y[-15,842] Z[-759,58519]` — matching both the file's +control points and the `RAPTOR.SCN` extent (`Z[-30000,75000]`). Rendered frames +from the path (`viewer/flythru_*.png`) show a coherent first-person flight down the +Raptor canal, banking as the euler angles interpolate. In the viewer, the fly- +through animates the full loop; dragging drops back to manual orbit. diff --git a/dpl3-revive/spec/SVT_FORMAT.md b/dpl3-revive/spec/SVT_FORMAT.md new file mode 100644 index 0000000..02f6dde --- /dev/null +++ b/dpl3-revive/spec/SVT_FORMAT.md @@ -0,0 +1,127 @@ +# `.SVT` texture format + +Reverse-engineered from `DPL3/DPL_LOAD.C : load_svt()` and validated by decoding +real textures to PNG. `.SVT` is the texture-map format referenced by `.B2Z` +materials (via the texture "map filename", block 0x011 — see `B2Z_FORMAT.md` §7). + +## Layout + +**There is no header.** The entire file is a raw, row-major array of 32-bit +texels. The image is always **square**, and the edge length is inferred solely +from the file size (exactly as the original loader does): + +| File size (bytes) | Dimensions | +|-------------------|------------| +| 16384 | 64 × 64 | +| 65536 | 128 × 128 | +| 262144 | 256 × 256 | + +Any other size is rejected. `texel_count = edge²`, `file_size = edge² × 4`. + +## Texel + +Each texel is 4 bytes in file order: + +``` +byte0 = pad (always 0x00, unused) +byte1 = Blue +byte2 = Green +byte3 = Red +``` + +i.e. a `0BGR` layout (as a little-endian 32-bit word: `0xRRGGBB00`). There is no +usable alpha channel stored in the texel — byte0 is a pad byte, not opacity. The +loader hard-codes `bits_per_texel = 32`. Per-material transparency comes from the +material/texture `alpha` mode, not from the texels. + +The channel order is not stated in the source (the i860/pxpl5 hardware consumed +the word directly); it was determined empirically — see Validation. + +## V coordinate convention + +**Do not flip V.** The renderer indexed texture rows directly: `v = 0` samples SVT +row 0, which is the image **top**. Verified empirically by A/B renders — the Mad Cat +cockpit canopy and missile-pod tube/vent faces, the MADCOL "08" insignia, and the +shark's back/belly shading are all correct with V passed through unchanged and all +invert if V is flipped (`1-v`). WebGL's `texImage2D` also places the first uploaded +row at `t = 0`, so no correction is needed anywhere in the modern pipeline. +(An earlier revision of this toolchain flipped V; that produced the "upside-down +cockpit / reversed pods" artefacts on the mechs.) + +## GLOMM-packed ("ALL") textures + +`GLOMM.EXE` (source: `DPL3/GLOMM.C`) packs several textures into the **bit-planes of +one 32-bit SVT** — a texture-memory-saving trick for the pxpl5 board. From the mode +switch in `load_svt()`: + +| `/m:` mode | Format | Bits | +|---|---|---| +| 0 | 4-bit mono | 20–23 | +| 1 | 4-bit mono | 16–19 | +| 2 | 4-bit mono | 12–15 | +| 3 | 4-bit mono | 8–11 | +| 4 | 8-bit mono | 8–15 | +| 6 | 8-bit colour (3-3-2: `r \| g<<3 \| b<<6`) | 0–7 | +| 7 | 12-bit colour (4-4-4: `r \| g<<4 \| b<<8`) | 0–11 | + +`save_svt()` **byte-reverses each 32-bit word** before writing. Files named +`*ALL.SVT` (`MADALL`, `VTVALL`, `GENALL`, …) are such packs (e.g. `GLOMMAD.BAT`: +`glomm madall.svt /m:0 mad1 /m:1 mad2 /m:2 mad3 /m:3 mad4 /m:6 madcol`) and decode +as coloured noise if read as plain RGBA. For modern rendering, **use the pre-glom +per-part source textures** (`MAD1..4.SVT`, `VTVCAB.SVT`, …), which sit alongside +them on the disk as ordinary full-colour SVTs. + +## `.BSL` bit-slice packs (DIV-BSL2) + +The formalised successor of the GLOMM packing, used by the game-side pipeline +(1996+). Header-ed, unlike `.SVT`: + +``` +"DIV-BSL2" 8-byte magic +u32 version (0x100), u32 (1) +u32 bits_per_slice (4) +u32 dir_bytes +u32 n_slices +n x { u32 type (10 = 4-bit mono), u32 slice_index, cstr name } +raw 32-bit texel words (edge inferred from remaining size, as .SVT) +``` + +Slice *k* occupies nibble **`(k+2) ^ 1`** of each little-endian 32-bit word: +slices fill bits 8–31 in ascending order, **pair-swapped within each byte** by the +writer's 32-bit byte reversal (the same `save_svt` reversal as GLOMM), leaving the +low byte (nibbles 0–1) for an optional 8-bit colour plane. So slices 0–5 land at +nibbles 3, 2, 5, 4, 7, 6. Materials select a slice via texture sub-block `0x018` +(binary) / `BITSLICE {k}` (text); the 4-bit mono plane is rendered as luminance +and coloured by the material's DIFFUSE. + +*Verification:* the pre-pack source TGAs survive in `STDAVE/VIDEO/BUILD/` +(`STEH1–6.TGA`, `KLNG1–6.TGA`). Correlating each TGA against all eight nibble +planes of its pack gives r ≥ 0.98 for this mapping on **all twelve** slices, and +near-zero for every other nibble. (A first-guess mapping of `7−k` scrambled the +planes — e.g. the starship's deflector-dish glow landed on the saucer hull.) + +## Notes + +- Textures are addressed by name from the `.B2Z` material; the loader resolves the + name to a file via a search path (`dpl_FindTextureFile`). Files on the disk carry + the `.SVT` extension. +- `minify` / `magnify` modes in the material select point vs. bilinear sampling + (`magnify` 0 or 1 → point; else bilinear). `wrap_u` / `wrap_v` select clamp vs. + repeat. These live in the `.B2Z`, not the `.SVT`. +- A 1-byte `.SVT` appears on the disk as a placeholder/stub (e.g. `WPNMECH.SVT`); + treat non-conforming sizes as "no texture". + +## Validation + +`parser/svt.py png ` decodes with the `0BGR` order and produced correct, +recognizable images: + +| File | Dims | Result | +|------|------|--------| +| FLAME64.SVT | 64² | orange/red flame (blue under the wrong order — the decisive test) | +| GROUPER.SVT | 128² | coral grouper: red body, blue spots | +| SHARK.SVT | 256² | great white shark on a blue ground | +| TANK.SVT | 128² | vehicle skin | + +The flame is the disambiguator: warm colors only make sense under `0BGR`; the +alternative `0RGB` renders it blue. diff --git a/dpl3-revive/spec/VELOCIRENDER_PROTOCOL.md b/dpl3-revive/spec/VELOCIRENDER_PROTOCOL.md new file mode 100644 index 0000000..29cc63b --- /dev/null +++ b/dpl3-revive/spec/VELOCIRENDER_PROTOCOL.md @@ -0,0 +1,914 @@ +# VelociRender wire protocol (Path A — run the original binary unmodified) + +Reverse-engineered from the original DIVISION source on the archive drive +(`sda4/DPL3/`), **not** from disassembly — both ends of the link are present in C: + +| Role | File | What it is | +|------|------|-----------| +| Host framing | `sda4/DPL3/VR_COMMS.C` / `VR_COMMS.H` | length-prefixed messaging, byte-sex, boot | +| Host link driver | `sda4/DPL3/LINKIO.C` / `LINKIO.H` | INMOS C012 link-adapter port I/O | +| Host command builder | `sda4/DPL3/DPL_HOST.C` | turns DPL nodes into `vr_action` messages | +| Protocol enum | `sda4/DPL3/VR_PROT.H` | the 24 `vr_action` command tags | +| **Board reference impl** | `sda4/DPL3/VRENDER/VR_REMOT.C` | the i860 message loop we are replacing | +| Board raster/texture | `sda4/DPL3/VRENDER/DNC.C`, `PAZPL5.C` | PixelPlanes-5 backend | + +Author throughout: Phil Atkin (PJA), DIVISION Ltd, 1994. Node struct layouts are in +`source-ref/DPLTYPES.H` (already used by the `dpl3-revive` parsers). + +**Goal of Path A:** stand up a *virtual VelociRender board* that speaks this protocol, +so `FLYK.EXE` (and the other HPDAVE/DPL3 binaries) run **unmodified** under DOSBox-X. +The board's rendering guts are the pipeline `dpl3-revive` already built. + +--- + +## 1. Transport — the link adapter (what DOSBox-X must emulate) + +The host talks to the board through an **INMOS C012 link adapter, IMS-B004 register +layout**, at I/O base `0x150` (default from `getXputer()` / `TRANSPUTER` env var). +`LINKIO.C` `setLA()` fixes the map: + +| Port | Register | Access | Semantics | +|------|----------|--------|-----------| +| `0x150` | Input Data | R | next received byte | +| `0x151` | Output Data | W | byte to transmit | +| `0x152` | Input Status | R | **bit0 = 1** → a byte is available to read | +| `0x153` | Output Status | R | **bit0 = 1** → ready to accept a byte | +| `0x160` | Reset / fifo-ok | W / R | reset (write); `fifo_ok_status` reads bit0 | +| `0x161` | Analyse | W | transputer analyse strobe | + +Byte primitives (`LINKIO.C`): + +- **out byte:** spin until `Output Status bit0`, then write `Output Data`. +- **in byte:** spin until `Input Status bit0`, then read `Input Data`. +- `altRecord()` / `inputReady()` = read `Input Status bit0` (non-blocking poll). +- `reset()` = analyse=0, reset=0, delay, reset=1, delay, clear both status, reset=0. + +**Emulation contract for the DOSBox-X device:** +- Output Status bit0 → always ready (we consume host bytes into a message assembler). +- Input Status bit0 → set whenever we have reply bytes queued; host drains via Input Data. +- On `reset` strobe, clear both directions and re-arm the boot handshake. + +That is the *entire* hardware surface: six ports, no DMA, no IRQ used by the polled +DOS path. No i860, no transputer, no PixelPlanes silicon is emulated. + +--- + +## 2. Envelope — framing on the wire + +Every message, both directions (`velocirender_transmit` / `receive_protocol`), is: + +``` ++----------------+----------------+---------------------------+ +| length_word:4 | action:4 | payload : N bytes | ++----------------+----------------+---------------------------+ + LE LE (little-endian structs) +``` + +- `length_word = flags | (id << 16) | count`, little-endian. + - **bit31 set** → *iserver* control message (C-runtime service; see §4). **clear** → render command. + - **bit30 (`0x40000000`)** → set on **every** length word by the shipped Aug-1995 HPDAVE + build (`FLYK.EXE`), on both vr_net and iserver messages (iserver ⇒ `0xC0000000`). It is + **absent from the 1994 `VR_COMMS.C` source** — a shipped-vs-snapshot delta found by + static analysis (`patha/verify_framing.py`: `or ebp,0x40000000` in the transmit path, + plus a global initialized `mov [..],0x40000000`). Exact receive-side meaning is still + TBD (routing/epoch flag?); confirm in the dynamic capture. **We mirror it** on replies + (`vrboard.LW_FLAG`) so our board is byte-identical to the real one; our decoder ignores + it (masks low-16 for count, tests bit31 for iserver). + - bits 16–23 = node id; `0xff` = broadcast (host→board render commands always use `0xff`). + - low 16 bits (`count`) = number of bytes that **follow the length word** = `4 (action) + N`. + Max 1024; the host caps payloads and fragments (see `velocirender_packetize`, 508-byte chunks). +- `action` = a `vr_action` enum value (§3), int32 LE. +- `payload` = command-specific bytes (§3). Multi-byte fields are LE on the wire; + the `dpl_little_endian` path sends host memory verbatim (`endian_fix` only matters + on big-endian hosts, which we are not). + +**Reply envelope is identical.** The board handler returns `ret` bytes; `reply()` +re-frames them with the **echoed action** as the first int32. The host +(`velocirender_receive`) reads `action`, then `count-4` payload bytes, and matches +the action to know the reply is for the command it sent. + +Synchronous vs. async: +- Most commands that expect data back (`create`, `delete`, `set_geom_verts`, + `set_texmap_texels`, `init`) do **transmit → receive** and block for the reply. +- `draw_scene` is **async**: the board replies with a bare `vr_draw_scene_action` + frame-ack later; the host tracks `dpl_frame_replied` and picks it up in + `velocirender_frameack` / `wait_draw_scene_complete`. + +--- + +## 3. Command set (`vr_action`, VR_PROT.H) and payloads + +Enum ordinals are positional (0-based, in declaration order). Payload columns are the +bytes **after** the 4-byte action. "Reply" is what the board sends back (0 = none). + +| # | action | payload (host→board) | reply | source | +|--:|--------|----------------------|-------|--------| +| 0 | `vr_init` | init arg string (leftover `DPLARG` opts joined by `\|`, NUL-term) | `int32 = 1` | DPL_HOST.C `ghost_dpl_init` | +| 1 | `vr_create` | `int32 type_check` | `int32 remote` (board handle) | `velocirender_create` | +| 2 | `vr_delete` | `int32 remote, int32 type` | echo | `velocirender_delete` | +| 3 | `vr_flush` | **the node struct body** (see §5) | none | `velocirender_flush` | +| 4 | `vr_sect_pixel` | `dpl_POINT + float x,y` | inst handle (stubbed here) | `ghost_dpl_sectpixel` | +| 5 | `vr_sect_vector` | `dpl_POINT + float x0..z1` | inst handle (stubbed here) | `ghost_dpl_sectvector` | +| 6 | `vr_dcs_nest` | `int32 parent_remote, int32 node_remote` | none | `ghost_dpl_nest_dcs` | +| 7 | `vr_dcs_link` | `int32 bro_remote, int32 sis_remote` | none | `ghost_dpl_link_dcs` | +| 8 | `vr_dcs_prune` | `int32 parent_remote, int32 child_remote` | none | `ghost_dpl_prune_dcs` | +| 9 | `vr_draw_scene` | `int32 double_buffered` | **async** `vr_draw_scene` frame-ack | `ghost_dpl_draw_scene` | +| 10 | `vr_draw_scene_complete` | (poll only, via input status) | — | host-side only | +| 11 | `vr_list_add` | `int32 head_remote, int32 node_remote` | none | `ghost_dpl_add_list_item` | +| 12 | `vr_list_remove` | `int32 head_remote, int32 node_remote` | none | `ghost_dpl_remove_list_item` | +| 13 | `vr_morph` | `int32 morphed,a,b_remote, float32 alpha` | (stub) | `ghost_dpl_morph_object` | +| 14 | `vr_version` | — | version fields (host fills locally) | `ghost_dpl_version` | +| 15 | `vr_statistics` | — | stats record | `velocirender_statistics` | +| 16 | `vr_readpixels` | `x, y, n_pixels` | **pixel data** (see §6) | see §6 | +| 17 | `vr_hspcode` | link addr + 128-byte HSP blocks | — | boot only | +| 18 | `vr_860code` | i860 .text (7-word header + segment) | — | boot only | +| 19 | `vr_860data` | i860 .data segment | — | boot only | +| 20 | `vr_860bss` | i860 .bss size | — | boot only | +| 21 | `vr_860args` | i860 argv string | — | boot only | +| 22 | `vr_set_geom_verts` | header + vertex/connection blocks (§5.2) | `int32` ack | `ghost_dpl_set_geometry_vertices` | +| 23 | `vr_set_texmap_texels` | header + 64-int32 texel blocks (§5.3) | `int32` ack | `ghost_dpl_set_texmap_texels` | + +Board dispatch that a virtual board mirrors: `VR_REMOT.C:remote_velocirender()` — +receive message, `action = data[0]`, `param_data = &data[1]`, switch on action, and +if the handler returns `ret>0`, `reply(ret,...)`. + +--- + +## 4. Boot sequence (must be satisfied before any rendering) + +`start_velocirender()` (VR_COMMS.C) runs, in order: + +1. `setLA(0x150); initLA();` +2. `reset()` — the analyse/reset strobe sequence on ports 0x160/0x161. +3. `boot_xputer()` — streams the transputer `.BTL` file raw in 2 KB blocks out the link. +4. **3-transaction iserver handshake per i860** (`startup_handshake`): the (real) i860 + C-runtime boots and issues 3 iserver requests (getenv-style); the host services them + in `iserver_action`. Comment in source: *"relies on the knowledge that the C run-time + boots up and does 3 iserver transactions before closing down."* +5. `boot_HSP()` — 128-byte HSP microcode blocks via `vr_hspcode`. +6. `boot_860()` — reads the `.MNG`: a 7×int32 header (csize, dsize, bsize, cstart, + dstart, bstart, entry) sent via `vr_860code`, then code/data/bss segments via + `vr_860code/860data/860bss`, plus argv via `vr_860args`. +7. `vr_init` with the init arg string; waits for the reply. + +**Virtual-board behaviour:** accept and **discard** all boot payloads (BTL/HSP/MNG); +we don't run that firmware. The only thing we must *reproduce* is the handshake shape +so the host proceeds: emit the 3 iserver transactions the host services in step 4, and +reply to `vr_init`. The `iserver_action` cases the host implements (and therefore the +kinds of request it expects from us) are: 40=commandline, 42=version(1.50), +32=getenv, 24=fputblock, 13=fwrite, 16=fflush. For getenv we can reply with empty +strings; the goal is only to advance past the 3-transaction gate. + +--- + +## 5. The node / handle / flush model (the heart of the renderer contract) + +DPL is a scene graph of typed nodes (`DPLTYPES.H`): `dpl_DCS` (transform), +`dpl_GEOMETRY`, `dpl_GEOGROUP`, `dpl_LOD`, `dpl_OBJECT`, `dpl_INSTANCE`, `dpl_MATERIAL`, +`dpl_TEXTURE`, `dpl_TEXMAP`, `dpl_LIGHT`, `dpl_LMODEL`, `dpl_RAMP`, `dpl_VIEW`, +`dpl_ZONE`. + +**Handles.** Each node has a `remote` field = its board-side identity. `vr_create` +sends the node type; the board allocates the object and returns a handle; the host +stores it in `remote`. All later references use handles, not host pointers. *A virtual +board just hands out unique 32-bit ids and keeps an id→object table.* + +**Pointer→handle rewrite (`remotize`).** Before flushing a node, the host swaps every +embedded child pointer for that child's `remote` handle (`DPL_HOST.C` macro +`remotize`), flushes, then restores local pointers. So **on the wire, all inter-node +links are handles** — directly usable by our board's object table. + +### 5.1 `vr_flush` — node body upload +`velocirender_flush` sends the node struct starting at the `remote` field for +`sizeof(struct) - delta` bytes (`delta = sizeof(dpl_node) - sizeof(dpl_remote_node)`, +i.e. it skips the local-only header). The board `memcpy`s it over its copy. This is how +transforms (the DCS 4×4 matrix), material colours, LOD ranges, instance refs, view +params, light params, etc. reach the board. **Field layouts come straight from +`DPLTYPES.H`** — the same structs `dpl3-revive/parser` already reads. + +### 5.2 `vr_set_geom_verts` — geometry +Header record (5×int32 used): `remote, vert_blox, conn_blox, vertices, connections`, +then `vert_blox` × `sizeof(dpl_VERTEX_LIST)` blocks, then `conn_blox` × +`sizeof(dpl_CONNECTION_LIST)` blocks (each a linked-list node sent whole). Board +reassembles into its geometry. Reply = int32 ack. + +### 5.3 `vr_set_texmap_texels` — textures +Header (5×int32 used of 8 sent): `remote, n_texels(=u*v), u_size, v_size, mode`, then +texels streamed **64 int32 at a time** (`endian_fix=0`, raw). `mode`: `1`=8-bit +(bilinear path, `dN_pxpl5_texture8`), `2`=24-bit FX (`dN_pxpl5_fx_texture24`), +else 24-bit (`dN_pxpl5_texture24`). Texel word format matches the SVT `0BGR` +convention already decoded in `spec/SVT_FORMAT.md`. Reply = int32 ack. + +--- + +## 6. Display path — the one item to pin against `FLYK.EXE` + +In **this DPL3 source snapshot**, `velocirender_readpixels`, `sectpixel`, `sectvector`, +and `morph` all `return 0` (stubs): the board rendered to its own PixelPlanes video +output (NTSC/SVGA off the card), so the host never read pixels back. + +But the **shipped `FLYK.EXE`** contains active pixel-readback machinery — strings +`_rframebuffer/_gframebuffer/_bframebuffer`, `dump_frame_buffer`, `vr_read_pixels`, +`velocirender received only %d bytes`, `VelociRender flush on 2d display 0x%x`. So the +HPDAVE build is a **later/derived** library where readback *is* implemented and the host +paints its own SVGA/VESA framebuffer from the returned pixels. + +This is the ideal seam for Path A and must be confirmed against the binary: + +- **If FLYK uses readback (expected):** our virtual board renders a frame (GPU or the + existing CPU rasterizer) and returns it as `vr_readpixels` payload; the host's own + SVGA path displays it. We never emulate a video encoder — the host shows the pixels. +- **If any content instead relied on board-direct video:** the virtual board opens its + own output window rather than returning pixels. (Applies at most to binaries lacking + the readback strings.) + +**Action:** trace `vr_readpixels` in `FLYK.EXE` (its `dpl_readpixels` → the +`DPL.C:3804 ghost_dpl_readpixels` call site, reimplemented in the shipped lib) to nail +the exact request args (`x, y, n_pixels`) and the reply pixel packing (word order, +chunking — likely the same 64-word fragmentation as texels). This is the only payload +whose shipped form isn't fully fixed by the snapshot. + +--- + +## 7. Virtual-board architecture + +``` + FLYK.EXE (unmodified, 32-bit Watcom/DOS4G) + │ port I/O 0x150-0x161 (polled C012) + ▼ + DOSBox-X custom device "virtual C012 link adapter" ← NEW, small + │ byte stream ⇄ message assembler (§1,§2) + ▼ + VelociRender protocol server (§3-§6) ← NEW, the real work + │ id→object table, flush→struct decode, scene graph + ▼ + Renderer = dpl3-revive pipeline (transform/light/cull + raster/GPU) ← REUSE + │ rendered RGBA frame + ▼ + vr_readpixels reply ─────────────────────────────────► host SVGA display +``` + +Work split: +- **New, mechanical:** the DOSBox-X C012 device (six ports + a byte FIFO) and the + message-envelope assembler. Bounded and testable in isolation. +- **New, substantive:** the protocol server — handle table, the `vr_flush` struct + decoders (mapping `DPLTYPES.H` structs to our scene objects), and boot-handshake + emulation. `VR_REMOT.C` is the line-by-line reference. +- **Reused:** geometry/texture ingestion, the DPL instance transform, materials + + gamma, and rasterization already exist in `dpl3-revive/parser` + `viewer`. The + protocol server feeds them the *same* data the file parsers produce — it arrives over + the wire instead of from `.BGF`/`.SVT` files. + +### Why Path A is now cheap +The two things that usually make "run the original binary" expensive — reversing an +undocumented protocol, and rebuilding the renderer — are both already solved here: the +protocol is in source (this doc), and the renderer is the project we've been building. +Path A is mostly *plumbing the existing renderer behind a wire protocol*, plus a tiny +emulated link card. + +### Relationship to the reimplementation track +Roadmap items 6–7 (port the geometry pipeline; GPU rasterizer) reimplement DPL3 as a +standalone viewer and discard the EXE. Path A instead keeps the **original host binary** +(its scene scripting, `.EVT` event timing, game logic, input, frame pacing) and +substitutes only the renderer. The two share the same rendering core; Path A adds the +link device + protocol server on top. + +--- + +## 8. Open items / next steps + +1. **Confirm the readback display path in `FLYK.EXE`** (§6) — decides whether the board + returns pixels or self-displays. Highest priority; everything else assumes readback. +2. **Pull the exact `vr_readpixels` request/reply layout** from the shipped lib + (`DPL.C` readpixels path) — the only payload not fully fixed by the snapshot. +3. **Lock struct sizes** for `vr_flush` bodies and the vertex/connection blocks against + `DPLTYPES.H` on this exact build (watch Watcom struct packing/alignment). +4. **Prototype the DOSBox-X device** headless first: log the boot handshake + + `vr_init` from a real `FLYK.EXE` run, verify our assembler frames the messages, then + add rendering. +5. Decide host: DOSBox-X custom device (C++ in the emulator) vs. a shim DLL — DOSBox-X + is the right home because the app does **direct port I/O**, with no driver hook point. + +--- + +## 9. Validation status + +**Static (patha/verify_framing.py, vs the shipped `FLYK.EXE` code object).** Found, as +literals in the transmit/link code: the `0x00FF0000` broadcast mask, the `0x80000000` +iserver bit, the `0x1FC` (508) packetize chunk, the `0x150` link base, and C012 port I/O. +Disassembly of the length-word construction confirmed the structure — and revealed the +shipped-build **bit30 `0x40000000`** flag absent from the 1994 source (§2), now mirrored. + +**Dynamic (real `FLYK.EXE` under stock DOSBox-X 2026.07.02, no responder).** FLYK boots, +drives the link at 0x150, streams the transputer boot, then blocks in the handshake +awaiting the board. The unmapped port reads `0xFFFFFFFF`, and FLYK prints: + +``` +Protocol error : length 65535 too big, length_word 0xffffffff ++++ERROR : rcv_protocol fail during iserver handling, xmits=0 +``` + +This is exactly `receive_protocol` (`nb = length_word & 0xffff` = 65535 > 1040 → reject) +called from `handle_iserver_stuff` during `startup_handshake` — confirming, against the +real binary, that the length word is 4 bytes with the count in the low 16 bits, and that +the *only* thing missing is a board that answers. Two more benign shipped-vs-snapshot +deltas surfaced: the shipped error strings additionally print `length_word` and `xmits` +(same framing). A responder that satisfies the 3-transaction handshake (§4) will let FLYK +proceed into the framed HSP/860/init/scene traffic. + +**Live (custom DOSBox-X build + our C012 bridge + real FLYK.EXE).** A custom DOSBox-X was +built with `src/hardware/vrlink.cpp` (the C012 bridge, ports 0x150–0x161 → TCP 127.0.0.1:8620, +enabled by env `VRLINK=1`; init hooked next to `GLIDE_Init` in `sdlmain.cpp`). Running real +FLYK against a **quiet capture board** (patha/vrcapture.py) captured 80,909 bytes: +`00 52 53 45 54` ×3 (FLYK's `reset()` strobing port 0x160 three times) + the **full 80,894-byte +transputer boot stream** (`f0 b4 d1 d1 d1 24 …` = VRENDMON.BTL verbatim). FLYK then printed: + +``` +timeout in inRecord +velocirender_receive timed out - sends_wo_rcv=0 +velocirender_input failed in start_velocirender +``` + +— a *clean* timeout (not the stock garbage-read), i.e. the bridge faithfully carries the +protocol and FLYK is now waiting for the board to answer. **The whole path is proven: +custom build → C012 ports → socket → Python board.** Iteration from here needs **no rebuild** +(board logic is pure Python). + +**Next blocker (precisely located): the post-BTL boot handshake.** After the boot download the +shipped `start_velocirender` calls `velocirender_input` expecting the board to send the first +message (the transputer/i860 C-runtime startup handshake — the 1994 source's "3 iserver +transactions", but the shipped flow differs and uses `velocirender_input`/`sends_wo_rcv`). An +unknown iserver tag makes the host `exit(666)`, so the exact expected bytes should be nailed by +disassembling shipped FLYK's `velocirender_input` path (or careful empirical bring-up), not +guessed. Once the handshake passes, FLYK proceeds to the framed `vr_860*` MNG download → `vr_init` +→ scene traffic → `draw_scene`/`readpixels`. + +--- + +## 10. END-TO-END ACHIEVED — FLYK.EXE runs its full render loop through the board + +Custom DOSBox-X (`src/hardware/vrlink.cpp`, env `VRLINK=1`) + `patha/vrrun.py` board + +real `FLYK.EXE`. FLYK now runs unmodified through the entire protocol into its main loop, +with **no errors**: + +1. Transputer `.BTL` boot streamed (byte writes to 0x151); board sends one vr_net frame to + satisfy `velocirender_input` (§4). +2. i860 `.MNG` firmware downloaded as framed `vr_860code/data/bss/args` (833 msgs) — discarded. +3. `vr_init` (real argv) → board replies. +4. **Full SHARKS scene graph built** (~69 nodes): `create` (fire-and-forget, host-assigned + handle in payload word 2 — not the 1994 reply model), `flush` (struct bodies), `dcs_link`, + `list_add`; loads geometry/texture/material search paths, sfx, fog. +5. **Main loop**: `draw_scene` per frame (board async-acks) + per-frame `0x1d` (56B). `0x2a` + (8B) is the **velocirender_sync barrier** (echo-ack it); `0x1c`/`0x1d` are fire-and-forget. + +### C012 FIFO wire protocol (definitive, from vrlink port logging) +Framed message = two channels, **body first**: +- **Body** (action+payload) → **port 0x154**, 16-bit `rep outsw` words (padded to even; the + length word's `count` is the true unpadded length). +- **Trailing length word** (`0x40ff00NN`, 4 bytes) → **port 0x151**, byte writes (commits it). +- Replies read **byte-wise from port 0x150** as serial `[length_word][body]`. +- Boot `.BTL` streams byte-wise to 0x151 (no body pending → raw passthrough). +`vrlink.cpp` demuxes and re-emits length-first `[length_word][body(count)]` to the board. + +### Shipped-vs-1994 deltas +- Length word always sets **bit30 (0x40000000)**. +- **FIFO transport** (0x154 word body + 0x151 trailing length) vs source's serial byte path. +- `create` **fire-and-forget**, **host-assigned handle** (payload word 2), no reply. +- New actions: **0x2a** = `velocirender_sync` (echo-ack), **0x1c/0x1d** = fire-and-forget + data / per-frame ops (geometry/camera — not yet decoded). + +### Remaining = RENDERING (not protocol) +FLYK spins `draw_scene` with no wait (instant ack) and issues **no `vr_readpixels`** in this +scene. To see the demo: decode the geometry/material carried by `flush` bodies + `0x1c/0x1d`, +feed the dpl3-revive renderer, present frames (return via `vr_readpixels` if FLYK pulls them, +else a board-side window), and pace `draw_scene` acks to ~60 Hz for real-time animation. + +--- + +## 11. Rendering phase — live scene-graph decode (patha/analyze_scene.py) + +What FLYK streams (decoded from the framed capture of a SHARKS.SCN run): + +| Data | Carrier | Notes | +|------|---------|-------| +| Camera | `flush` body, type 3 (view) | matrix + 832×512 screen + clip + fog + back_color(0.4,0.6,0.9) | +| Transforms | `flush` bodies, type 5 (dcs) ×26 | 4×4 `dpl_MATRIX` each | +| Placements | `flush` bodies, type 4 (instance) ×22 | `dcs`,`object`,`f_material`… handles (`dpl_INSTANCE`) | +| Materials | `flush` body, type 12 ×1 | emissive/ambient/diffuse/opacity/specular (`dpl_MATERIAL`) | +| **Per-frame animation** | **action 0x1d** (2×/frame, 56B) | transform/camera update — drives the flythrough | +| **SPECIALFX particles** | **action 0x1c** (80B) | matches SHARKS.SCN SPECIALFX params (11.0,360,0.9,-19,0.99…) | +| Geometry meshes | **referenced by name → files** | NOT on the wire — no `set_geom_verts`/object/geometry creates; loaded board-side from the geometry/texture search paths. dpl3-revive already parses these (`.BGF`/`.SVT`). | + +Flush body layout = `[remote:4][type_check:4][struct fields after dpl_node]` (flush starts at +`&node->remote`). Struct field offsets per `source-ref/DPLTYPES.H` (mind the `#if REMOTE` fields). + +**Key finding:** the whole *scene graph and animation* come over the wire; only the *geometry* +is by-reference (file-loaded). And FLYK issues **no `vr_readpixels`** — the real board drove +video directly — so frames must be presented board-side, not returned to FLYK. + +### Rendering approaches (design fork) +- **A — Hybrid (fastest to visible):** take the live camera + per-frame `0x1d` animation from the + protocol; render the SHARKS scene's geometry/placements from files via the existing dpl3-revive + pipeline; present in a board-side window. FLYK drives camera/timing; sidesteps the + instance→object→file handle mapping (which isn't on the wire). +- **B — Faithful (purist):** additionally decode instance→object references and reproduce exact + placements/materials/sfx from the wire, using files only for the raw meshes. More decode work + (resolve how object handles map to geometry files board-side). + +Either way the renderer core is the dpl3-revive pipeline; pace `draw_scene` acks to ~60 Hz so +FLYK animates at real speed instead of spinning. + +### Hybrid render achieved (patha/hybrid_render.py -> patha/flyk_render.png) +First visible frame: the SHARKS.SCN reef FLYK.EXE is running, assembled from files by the +dpl3-revive pipeline and viewed from an in-scene (diver) camera — a textured shark in the reef. +The fish cluster (-220,225,-350) matches FLYK's live 0x1d translations (y~230, z~-200), +confirming the wire animation targets the same content. Camera *projection/fog/back_color* are +taken from the decoded VIEW body (§11); the *pose* is a DCS (animated by 0x1d). + +--- + +## 12. FULL PATH B ACHIEVED — geometry on the wire, live board-side window (2026-07-04, session 2) + +**The §11 "geometry is file-loaded board-side" conclusion was wrong — it was an artifact of a +broken run.** Root cause chain, proven by live iteration: + +1. `HPDAVE\SHARKS.SCN` searches `..\geometry`, `..\sharks`, `..\texture` — none of which exist + beside `sda4\HPDAVE`. FLYK **fails object loads silently** (the "Failed to load object" print + only fires when `dpl_NewObject` returns NULL, not when the file is missing) and builds an + empty world: every instance flushed with `object=NULL`, no geometry/texture traffic. That was + the capture §11 analyzed. +2. The shipped FLYK registers object-load extensions `.bgf .bmf .bsl .tga .vtx` (strings in the + EXE) — **not** `.b2z/.svt`. But its `dpl_bgfRead` validates the same `DIV-BIZ2` container the + `.B2Z` files hold ("`dpl_bgfRead "%s" is not a valid biz file`"). +3. **Fix = staging** (`patha/stage_assets.py` → `c:\temp\flykc`, mounted as C: by + `patha/flyk_vr.conf`): copy `HPDAVE`, copy `DPL3\GEOMETRY` exposing `*.B2Z` as `*.BGF`, decode + `DPL3\TEXTURE\*.SVT` (raw xBGR 32-bit) to `*.TGA`. With that, FLYK loads the reef and + **uploads everything over the link**. (`fishes`, `banner`, and the `sharks:*` textures have no + surviving source files anywhere on the drive — those entities stay object-less; the i860 + firmware has no file I/O, so nothing was ever board-loaded.) + +### Shipped wire protocol, geometry phase (all decoded live) +- **Shipped node type ids differ from 1994 from 7 up**: 2=zone 3=view 4=instance 5=dcs 6=lmodel + **7=object 8=lod 9=geogroup 0xa=geometry 0xb=material 0xc=texmap** (1994 order was + object=8…material=12). +- **Action 0x17** (1994 `set_texmap_texels`) = **bulk vertex upload**. Header 36B = + `[geom_handle][0][n_verts][stride_words][n_conns][vert_type][geo_type][conn_sz][GEOMSCALE:f32]`, + then framed data fragments (≤508B); **one ack** (echo action, int32 1) after + `n_verts*stride*4` bytes. Vertex strides: `vtype 0x13` = `[x y z nx ny nz u v]`, + `vtype 0x15` = `[x y z r g b a u v]`. `gtype 5` = pmesh, `gtype 2` = implicit single polygon + (no connectivity follows). **Wire positions are divided by GEOMSCALE; the header's float + restores model units** (verified against raw .B2Z bounds). +- **Action 0x19** = **add connections**. Header 16B = `[geom_handle][n_conns][indices_per_conn] + [flags]`, then `n_conns*per` int32 indices; one ack. (Ocean quad = `[1][4]` + `0,1,3,2`; + fish pmesh = `[70][3]` + 70 triangles.) +- **Graph refs in flush bodies** (wire-payload word indices, where word0=remote): + instance word6 = object handle; geogroup words16/17 = f/b material; DCS matrix words4–19 + (4×4 row-major, translation in row 3), word20 = parent DCS (instance DCSs → world DCS 0x3; + camera tree uses explicit `dcs_link` 0x1→0x2, 0x1→0x3 instead). object→lod→geogroup→geometry + attach via `list_add`. Repeated STATICs share objects (host object file cache). +- **VIEW body** (104B, projection only, no matrix): wire float idx 6..17 = + `x0 y0 x1 y1 zeye x_size y_size hither yon back_rgb[3]`, idx18 = fog_enable, + idx19..23 = fog `[near far r g b]`. SHARKS: ±1 × ±0.615 image plane at zeye=1.3, 832×512, + clip 8..4500, back (0.4,0.6,0.9), fog 500..4000 (0.05,0.1,0.12). +- **Action 0x1d** = per-frame DCS transform (shipped `flush_artics`): + `[dcs_type=1 (matrix)][dcs_handle][3×3][tx ty tz]`, fire-and-forget, 2/frame in + SHARKS: handle 0x1 (root, constant identity+t(0,5,5) when idle) and the DYNAMIC shark's DCS + (spline path from SHARK1.SPL, scale 8 baked into the 3×3). **The 0x1d 3×3 is row-vector + convention, same as the flush matrices** — proven by `patha/heading_test.py`: across ~7000 + records, model −Z tracks the swim velocity (dot ≈ −1, 95% of samples); a transpose mirrors + the yaw (user-visible as reversed turning). Corollary: **the spline animation is built for + nose-along−Z models.** The original `..\sharks` shark (lost) was authored that way; the + surviving stand-in `DPL3\GEOMETRY\SHARK.B2Z` is nose-along-+X, so the renderer applies a + +90° yaw model correction (X→−Z) to dynamically animated instances only. +- **Camera** = inverse of (M_dcs0x2 · M_dcs0x1); look −Z, row-vector convention (p′ = p·M), + child→parent composition. 0x1d on 0x1 = head motion (joystick when a pilot is present). + +### Board-side real-time window (Path A/B complete) +`patha/vrview.py` (numpy rasterizer + pygame, ~18–30 fps at 640×400) renders straight from the +live board state: wire geometry, DCS graph, VIEW projection + fog + back color, 0x1d animation. +`python vrrun.py --view` presents each `draw_scene` and paces the ack (≤60 Hz), so the +unmodified FLYK.EXE runs its demo in real time against the virtual board — swimming shark, fish +cluster, kelp, ocean planes, all from the wire. Offline proof/debug: `patha/test_view.py` +(replay a capture → PNG; `flyk_live_render2.png` = the shark mid-swim), `patha/census.py`, +`patha/decode_anim.py`. + +### Run recipe (session 2) +1. `python patha/stage_assets.py` (once; rebuilds `c:\temp\flykc`) +2. `python patha/vrrun.py cap --view` +3. `$env:VRLINK='1'; G:\DOSBox-X\dosbox-x-vrlink.exe -conf patha\flyk_vr.conf` + +### SDEMO findings (2026-07-04, session 2 continued — the full Hull Pressure demo) +Nearly all HPDAVE scenes point their GEOMETRY path at `.\video`, which **survives intact** +(201 files) — SHARKS.SCN was the worst-provisioned scene on the drive. `stage_assets.py` now +stages `HPDAVE\VIDEO`; `run_demo.py ` selects scene/exe via `C:\RUNSCN.BAT`. SDEMO +(144 entities, 41 objects, 12 splines) runs live end-to-end and decoded more of the protocol: + +- **Action 0x1a = set_texmap_texels** (1994 layout): header 32B + `[texmap][n_texels][u][v][mode][3 junk words]`, then 64-int32 (256B) chunks; ONE ack. + Texel word = `[pad,B,G,R]` (confirmed against DNC.C `dN_pxpl5_fx_texels`). +- **mode 1** = 8-bit intensity (grayscale replicated across B,G,R) — decodes clean. +- **mode 0** = **BSL bit-slice pack**: the page holds up to eight 4-bit monochrome textures, + one per nibble plane of the 32-bit texel word (verified: planes of SDEMO's texmap 0x3e are + distinct coherent sub-interior textures; a 256×256 page holds the VW logo + a mural in + different byte lanes). Each dpl_TEXTURE node sharing the texmap carries a small-int slice + selector late in its flush body (values 0x14–0x1a observed) — exact value→nibble mapping + not yet pinned. Matches the b2z `bitslice` tag (0x018) and the `.BSL` files in VIDEO. +- **More vertex formats** on 0x17 uploads: vtype 0x01 stride 3 `[xyz]`, vtype 0x11 stride 5 + `[xyz uv]`, vtype 0x41 stride 4 (gtype 0xa, spheres/points?), in addition to 0x13/0x15. +- **Offset vertex updates**: 0x17 header word1 = first-vertex index, word7 = total vertex + count (word1+count ≤ word7) — `dpl_UpdateGeometryVertices`, used for vertex animation. + The board must merge uploads into the full array, not replace. +- **Geometry flush body references its texmap directly** (stored word 3), simpler than the + geogroup→material→texture→texmap chain. +- **Corrected type ids: 0xc = texture, 0xd = texmap** (earlier §12 guess had them swapped). +- Per-frame traffic: heavy **vr_morph** (action 13 — fish-school vertex morphing, .V2Z), + **0x1b** (72B, periodic), **0x22** (8B), 0x1c SPECIALFX — all currently discarded. + +### Cross-product runs (session 2 continued): KLNGVID (Star Trek), BLADE (Red Planet) +Other products stage at the mount root (their scenes use absolute `\STDAVE\VIDEO`-style +paths) with the HPDAVE FLYK runner copied in (`stage_assets.py stage_product`). Decoded: + +- **Material flush body** (0xb, wire words): texture@2, **emissive@5-7, ambient@8-10, + diffuse@11-13, opacity@14-16, specular@17-20** (stored-body idx −1). Applied in vrview. + **Exact ambient=(1,0,0) AND diffuse=(1,0,0) is the shipped build's UNSET-material + marker, not a colour** — every .B2Z-default material (all of SHARKS) and the Trek star + materials carry it; render as white. (Found as a red-reef regression during the SHARKS + re-validation run.) +- **dpl_TEXTURE flush body word 14 (stored 13) = BSL bit-slice selector** (0x14…0x1a). +- **0x1d COMPOSES with the flushed DCS matrix** (M_flush · M_anim, row-vector): the DCS + holds model scale (KLNGVID klngn 0.1), the spline pose rides on top. SHARKS' identity + base matrix had made replace-vs-compose indistinguishable. +- **Action 13 (vr_morph)** = `[morphed][geomA][geomB][alpha:f][flags]` — board-side vertex + blend a→b into the morphed geometry (SDEMO fish schools). Implemented. +- **Action 0x1b** = `[zone][dcs][4×4]` occasional zone-root matrix refresh; identity 3×3 + + uninitialized row-3 junk when the host DCS is identity-flagged — ignore those. +- **SCHILD/DCHILD effect attachments** (Trek shield bubbles, invisible-until-hit) carry a + nonzero hierarchy pointer at wire word 21 of their DCS body — vrview skips them. +- **Shipped FLYK quirks**: KLNGVID's `START x y z` parses z wrong (camera got y duplicated + into z — authentic shipped behavior); `TRACKER KEYBOARD` input path not yet working under + DOSBox (keys don't move the camera — investigate how the tracker reads the keyboard). +- **MAPS\*.SCN files are a different key=value dialect** (`viewangle=60.0`) that hangs + FLYK's parser — stage SCENES\ versions, never MAPS. +- **BLADE.SCN runs**: 770 geometry uploads, 29 texture pages, 200+ instances — the Mars + canal city renders fully textured (RPDAVE pages are full-color, no BSL packing). + +### Completeness batch (session 2 close-out) — all five scenes regression-PASS +Implemented and validated by `patha/regress.py` (replays every scene capture, renders, +reports; zero handler errors across SHARKS/SDEMO/KLNGVID/BLADE/FXTEST): + +- **Wire lighting**: lmodel (0x6) / light (0xe) bodies = `[dcs@2][light_type@3][r g b @4..6]` + (stored words); type 2 = ambient, type 3 = directional with aim = the light-DCS's Z row + (SHARKS: sun (0,1,0) from rx=−90 ✓). Renderer uses scene ambient + suns, double-sided. +- **LOD range selection**: lod body stored words 15/16 = switch_in/switch_out; renderer + picks by eye distance (fallback LOD 0). +- **SCROLL**: board-side autonomous texture animation from the texture body's + u0/v0/du/dv (stored floats 8..11), du/dv per second. +- **BILLBOARD**: instance stored word 3 == 2 (vs 3 normal) — camera-facing spherical + billboard (FXTEST `flamebig`). +- **Sphere lists** (vtype 0x41, `[x y z r]`): shaded screen-space discs. +- **sect_vector**: real Möller–Trumbore picking over live world triangles; reply = + int32 instance handle. Self-test: an upward underwater ray correctly hits the ocean + surface quad. (sect_pixel still needs camera unprojection of board 832×512 coords.) +- **vr_readpixels**: experimental reply — `[pad,B,G,R]` pixel words from the last + rendered frame, 508B fragments; no shipped binary has exercised it yet. +- **Action 0x22** = name binding `[handle][0x8000xxxx name id]` (board createName; + fire-and-forget; stored in board.names — GEOMETRIZE 0x8000005a-style refs resolve here). +- **BSL slice mapping**: rank-based (sorted texture-node selectors → content nibble + planes in order). No more rainbow; exact selector→nibble encoding still unproven. + +### SPECIALFX (action 0x1c = install_sfx) — decoded + ambient sim implemented +Payload (80B): `[code][texture ref][type][velocity][size][off_y][bias][cook_r g b] +[variance][gravity][o_cool][cool][frags][rpt][4 host ptrs]` — exactly the .SCN +SPECIALFX columns. Defs are *installed* at scene load; two trigger models: +- **Ambient** (SHARKS bubbles/marine snow): the board steps installed defs + autonomously. Implemented in vrview `_particles`: per-def pools (frags×rpt), + spawned in a camera-centred volume at bias/off_y height, rise at `velocity` + with `variance` jitter and `gravity`, fade by `o_cool^age`, lifetime 1/`cool`; + soft-blended discs, no z-write. Regression stays green on all five scenes. +- **Event-triggered** (`PSFX \fx\*.pfx` lines in .EVT scripts, MORPH_OBJ/MORPH_MTL + events): confirmed in FXTEST that *nothing* fires without the input path — not + even the t=0.2 morph — so runtime FX triggering is **blocked on the keyboard + tracker**, not on the protocol. The .PFX emitter format is self-documented in + the files (position/velocity/radius/colour-in-out/duration + variances). + +### Input SOLVED — joystick flight via the game-port (THRUSTMASTER) tracker +FLYK's `TRACKER KEYBOARD` is inert under DOSBox (no key ever moved the camera), but +`TRACKER THRUSTMASTER ` reads the DOS game port, which DOSBox maps from +the physical joystick — **verified live: BLADE flown with a Logitech Extreme 3D Pro**, +camera 0x1d translations tracking the stick on the wire. `run_demo.py` now rewrites any +scene's TRACKER line to `THRUSTMASTER 4.0 0.3` before launch (`--no-joystick` to keep +the original). FLYK's input layer is joystick-first throughout (`joystick_read`, +DIVISION game-port calibration, `joystick.cal`). + +### Fire control (action 0x23) + calibration notes +- **Stale joystick.cal = constant-velocity camera drift** (stick center offset reads as + permanent deflection). Fix: delete the staged cal; FLYK runs its RE-CALIBRATION flow + (wiggle to full scale) and writes a fresh one matched to the DOSBox-mapped stick. +- **Action 0x23** = fire/pick event on joystick trigger: `[u:f][v:f][host ptr][-1]`, + (0.5, 0.5) = crosshair center. **CONFIRMED fire-and-forget** — replying desyncs the + host stream (tested both ways: unanswered 0x23s are harmless; one reply stalled the + run). The board still resolves the pick internally (pick_screen: VIEW-frustum + unproject → Möller–Trumbore) for telemetry; FLYK's own hit/FX consequences must be + computed host-side or armed by a mechanism still undecoded (the .EVT `PSFX` chain + never fired in any FXTEST run). + +### Protocol edges closed (session 3) +- **Action 0x18 = get_geom_vertices** — found by static hunt (LE fixup-table parse + + capstone over shipped FLYK: `mov eax,0x18` into velocirender_transmit, 0x28-byte + request, reply must echo action 0x18 into a 0x100-byte receive buffer). Board replies + with the geometry's merged current vertex floats (request word0 = handle, words 1/2 + honored as vertex0/count when sane) in ≤240B echoed-action frames. Regression: + byte-exact round-trip of an uploaded quad. `patha/hunt_actions.py` is the reusable + hunt tool. `get_geom_numconns` exists only in the game binaries (RPL4OPT etc.), which + are Borland **PE** images (32RTM DPMI), not Watcom LE — needs a PE-based hunt, filed + under the MUNGA campaign. +- **sect_pixel** — wired to pick_screen (auto-normalizes board-pixel vs 0..1 coords); + crosshair self-test picks a live instance. +- **statistics** — 1994 reply (int32 1) per VR_REMOT.C. **version** confirmed + board-silent (host-local; not in the board dispatch switch). +- **readpixels** — self-tested: correct echoed-action fragmentation (100 px → 1 frame, + 400B). Still awaiting a real caller for final validation. +- Regression suite extended with all of the above; 5 scenes + 6 protocol self-tests PASS. + +### Rendering fidelity batch (session 3) +- **BSL slice encoding DECODED EXACTLY** (`patha/bsl_pair.py`): wire selector 0 = file + bitslice 0 (unsliced), selector 0x13+b = file bitslice b (1..8) — proven by the GENS + pack whose nine wire selectors {0,0x14..0x18,0x1a,0x1b,+dup 0} match its file + bitslices {0,1,2,3,4,5,0,7,8} one-for-one, duplicate included. **Nibble plane = + bitslice + 2** (pad byte owns planes 0-1; GENH's six textures = exactly content + planes 2..7). b ≥ 6 clamps to plane 7 (only the 9-texture packs; residual). On a + sliced pack, selector-0 textures read plane 2; on plain pages selector 0 = the + full-colour texture. b2z tag 0x018 (`bitslice`) is the authoritative file-side value. +- **Transparency**: texture-body alpha flag (stored word 4) → near-black texel cutout; + material opacity < 1 (`DITHER n` = n%) → 2×2 ordered screen-door. +- **Specular** from material specular[4] (rgb + exponent), per-sun highlight. +- **NOVIEWMATRIX / HUD zones**: instance DCS whose parent word references a ZONE node + renders in camera space (view matrix skipped) — SHARKS banner, SDEMO fcard. +- **RETRACE n** honored: run_demo parses the scene divider → board paces draw_scene + acks at 60/n Hz (SHARKS 30 Hz, FXTEST 20 Hz — authentic animation speed). +- Regression: 5 scenes + 6 protocol self-tests all PASS; SDEMO interior now renders + as coherent monochrome sub panels under scene lighting. + +### DCS topology corrected — camera-motion cancellation bug (session 3) +`vr_dcs_link` is a **SIBLING** link (1994 payload `[bro, sis]`) and the DCS flush-body +word at stored offset 76 is the **sibling pointer**, not a parent: FLYK's top-level +DCSs (head 0x1, view-holder 0x2, and every instance DCS) form a flat sibling ring. +Treating either as parentage had chained the whole world through the tracker-animated +head DCS, so camera motion cancelled exactly (title-bar pose moved, view didn't — +also the fxjoy "frozen screen while drifting to (−1036,605)" symptom). Correct model: +instance chains follow **vr_dcs_nest edges only** (flat roots in all scenes so far); +camera pose = M(view's DCS) · M(head 0x1). Verified: the drift capture now renders +dramatically different frames along the flight path. +Ambient SPECIALFX sim is now **opt-in** (`run_demo --sfx` / VRVIEW_SFX=1): most scenes +install their defs for event use only — simulating them ambiently put bubbles in +scenes that never show them. Launchers enable it for the underwater HPDAVE set. + +### Software-rasterizer optimization pass (session 3) +Vectorized pre-culling: whole-mesh early-out (nothing in front / screen bbox miss), +per-triangle batch rejection (clip, off-screen, degenerate) before the Python loop, +and **far-plane (yon) culling** — the big one for open scenes (BLADE's yon is 600 in a +6 km canal). Measured: BLADE 769→368 ms, SDEMO 883→466 ms (~2×), KLNGVID 249→224, +small scenes unchanged. `VRVIEW_SIZE=WxH` env (e.g. 384x240) trades resolution for +another ~1.7×. The remaining cost is per-visible-triangle Python overhead + numpy +fill — the endpoint is the **moderngl GPU backend** (mesh cache maps directly to +static VBOs; only matrices change per frame), targeted at 832×512 @ 60 Hz. + +### GPU backend (session 4, 2026-07-04) — moderngl, 832×512 @ 60 Hz ACHIEVED +`patha/vrview_gl.py` = `GLRenderer(vrview.Renderer)`: SceneCache, DCS chain math, +picking, and view/material/SCROLL parsing are inherited; only the draw path is +replaced with moderngl (GL 3.3 core). The software rasterizer stays intact as the +debugging reference — `vrrun --view --soft` / `run_demo --soft` / `VRVIEW_SOFT=1` +select it; any GL failure auto-falls-back with a message. + +Architecture facts: +- **SceneCache._mesh() results = static VBOs.** The VBO cache is keyed on the mesh + dict's *identity* — `_mesh` already version-caches per handle (uploads, offset + vertex updates, morph alpha), so a new dict means re-upload, same dict means zero + per-frame work. Interleaved layout `[pos3 nrm3 col3 uv2]` + uint32 index buffer + (fan-triangulated connections, unchanged). Morphing geometries re-upload per + alpha change (small meshes; negligible). +- **Row-vector convention needs no transposes**: numpy row-major mat4 bytes are + read column-major by GLSL, i.e. the shader sees Mᵀ, so `u_m * v` in GLSL IS + `p @ M`. Projection is a row-vector frustum built from the VIEW body + (x0/x1/y0/y1 at zeye, near=hither, far=yon·1.05) matching the software mapping + exactly; instance matrices (chain, FIX yaw-90, billboard, HUD/NOVIEWMATRIX, + LOD select by ‖T‖) are computed CPU-side identically to the software path. +- **Shaders replicate the software formulas 1:1**: per-vertex lighting (wire + ambient + Σ|n·L|·suncol double-sided, per-sun specular rgb+exp, 0.7/sun + no-normal fallback, 0.35/0.65 default rig when no wire lights), vertex-color + modulate, clamp, texture×(base·1.275) (= software's texel·base/200), SCROLL + uv offset (u0+du·t from the texture body, per frame), alpha cutout + (texel sum ≤ 24/255 discard), DITHER 2×2 screen-door discard vs opacity, + per-vertex linear fog mixed after texturing, sphere lists + SPECIALFX pools as + depth-tested point sprites (particles: blend (ONE, 1−SRC_ALPHA) with + rgb=cook·0.75·fade, a=0.6·fade ≡ the software soft blend; no depth write). +- **Linear FBO + gamma present pass**: the scene renders linear into an offscreen + 832×512 FBO; a fullscreen present pass applies the Division DAC gamma + (out = in^(1/1.25)) — same single-application point as the software path. + `last_frame` (readpixels replies) reads the FBO back post-gamma on demand, + cached per rendered frame. Headless (`SDL_VIDEODRIVER=dummy`, regress) uses a + standalone WGL context — no window needed. +- Textures: nearest + repeat (matches the software point sampler); uploaded once + per SceneCache texture array (BSL slices included), cached by array identity. + +Validation (RTX 3080 Ti): +- `regress.py` now runs both backends: software 45–447 ms/frame vs **GL 0.6–8.7 + ms/frame at 832×512** (SHARKS 1509 fps, SDEMO 272, KLNGVID 446, BLADE 115, + FXTEST 1431) — 60 Hz with ≥2× headroom on the heaviest scene. PNGs + `regress_gl_*.png`; mean-abs-diff vs the software reference: BLADE 4.5/255, + KLNGVID 6.5/255 (near-identical). SHARKS/SDEMO/FXTEST diff higher for a + KNOWN benign reason: the software pre-cull drops any triangle with a vertex + behind hither/beyond yon, which kills huge ground/ocean planes; **GL clips + properly and renders them** (FXTEST arena floor, SHARKS sea floor + surface + horizon appear only in GL — GL is the more correct image). +- Live (real FLYK.EXE through DOSBox): SDEMO ran 3300+ frames (zero errors, + RETRACE 2 → 30 Hz pacing honored, wire rate ~37 fps burst); **BLADE ran live + at 65.7 fps** (900 frames / 13.7 s, 2400+ frames total, zero errors, a + joystick-trigger 0x23 fire resolved a pick against the live scene) — full + 60 Hz on the heaviest scene. Windowed replay paces at 60.0 fps with 9 ms + render. Adaptive frame-skip now never engages (skip stays 1 below + 50 ms/frame) — as predicted, it's vestigial on GPU. + +## 13. MUNGA GAME BRING-UP — the real Red Planet runs (2026-07-05, session 5) + +**`rpl4opt.exe -egg test.egg` (Red Planet v1.2.0.1) runs its full engine loop +against the virtual board**: 892 geometry uploads, 22 texture pages, 308 +instances, 63 objects, 4 lights — the whole Mars canal city + the player's +starting hangar — with the attract-mode flight rendering live at ~90 fps in the +GL window. Staging: `stage_assets.py --game RPDAVE` (adds MAPS/MODELS/SOLIDS/ +CAMERAS/GAUGE/SCENES + `32RTM.EXE` **and `DPMI32VM.OVL`** borrowed from BTDAVE +— 32rtm refuses to start without its DPMI overlay). Launcher: `run_game.py` +(mirrors RP.BAT: `SETENV.BAT t s n n` → `32rtm -x` → exe → `32rtm -u`). +`hunt_pe.py` = the PE-format static hunt (Borland PE sections + capstone), +counterpart of the LE hunt_actions.py. + +### The MUNGA wire dialect (all proven live + by disasm) +- **Dialect detection: the first vr_sync action id.** The init argv is BYTE- + IDENTICAL between shipped FLYK and rpl4opt (`/device~0x150~/video~svga~/pipes + ~1~/qual~0x14~/system_tex~0~`) — sniffing it misclassified FLYK and yawed + every demo camera (caught by regress diffs; the numbers, not the PASS flag). + 0x2d = MUNGA, 0x2a = FLYK; the flag flips on the first 0x2d. +- **vr_sync = action 0x2d** (FLYK's 0x2a moved). Disasm of rpl4opt's vr_sync + (0x497080): the reply ACTION is never checked; the reply PAYLOAD word0 must + echo the request's word0 (a cookie; mismatch → "unexpected action %d returned + in velocirender_sync" printing the unrelated action + exit 9). FLYK's 0x2a + cookie is 0, so the old zero-payload echo was accidentally correct. +- **The sync cookie is the action id being synchronized** (create → cookie 1, + texel upload → cookie 0x1a): **MUNGA bulk ops are ACK-LESS** — 0x17/0x19/0x1a + completions must NOT be acked (the sync echo is the ack; an extra upload ack + desyncs the receive stream by one frame). FLYK keeps its per-upload acks. +- **create stays fire-and-forget** (host-assigned handle in payload word2; the + 1994 "unexpected action %d in velocirender_create" strings are absent from + the PE — an experimental create-ack desynced the stream). +- **Action 0x1f = batched flush_artics**, replacing 0x1d entirely: + `[n_records]` then n × `[handle][payload]` where a DCS handle carries + 3×3 row-major + t (12 floats) and non-DCS handles (HUD gauge nodes) carry 5 + floats (sin/cos-like; skipped). **Poses are ABSOLUTE** — the first record + duplicates the flushed matrix exactly; composing (the FLYK 0x1d rule) doubles + the transform. Stored in `board.anim_abs` (replace) vs `anim` (compose). +- **Camera rig via dcs_link parentage** (MUNGA ONLY): vehicle DCS —link→ + cockpit —link→ head DCS, view re-`list_add`ed onto the head (0xb5c); world + pose lives in the 0x1f-animated vehicle DCS (0xaa1). The camera chain follows + link edges in MUNGA; FLYK's links are flat sibling rings (following them + relocated every demo camera). Instance chains stay nest-only in both. +- **MUNGA vehicles fly nose-along-+Z** — straight-fast-flight samples of the + RAW vehicle 0x1f records put local velocity 3.8° off +Z (459 samples). An + earlier composed-chain heading test read +X (+0.79) — that was BIAS from the + attract head-yaw swinging ±54° in the cockpit DCS; measure the vehicle DCS + alone. The renderer applies a camera-only yaw(180) (`cam_matrix`) mapping + render-forward (−Z) onto vehicle +Z (the raw chain rendered 180° backward; + the interim yaw(−90) guess showed as "flying sideways", user-confirmed). +- **Material body is 88B (FLYK 84B): every stored offset shifts +1** + (emissive@5-7, ambient@8-10, diffuse@11-13, opacity@14-16, specular@17-20 + + exponent). Misparse rendered the whole world green with a global DITHER + screen-door (opacity read 0.815). lmodel (0x6) light bodies parse with the + FLYK layout unchanged; the four type-0xe 32B bodies (vehicle lamps?) fail the + sanity filter and are ignored. +- **Fire-and-forget unknowns** (don't block anything): 0x29 (8B, one word ≈ a + high node handle) and 0x2b (140B, `[handle][1][2][0x1d]...` + floats — HUD/ + gauge displaylist-shaped). Undecoded; game runs without replies. +- First-boot trap: `32rtm error: Can't find DPMI32VM.OVL` — the overlay must + sit beside 32RTM.EXE on the path. + +Regression: all five FLYK scenes byte-identical to baseline after the dialect +fix (36.8/33.6/6.5/4.5/89.5 diff signature); MUNGA runs live: hangar + canal +render in correct Mars tans/reds, camera tracks the attract flight. + +### BattleTech (BTL4OPT v1.1.0.4) bring-up — session 5 continued +Runs the same MUNGA dialect end-to-end (arena + mechs on the board, 1800+ +frames, ~100-130 fps GL). Asset chain findings: +- **BTDAVE's VIDEO tree is empty of the video\geo|mat|tex sets** its BTDPL.INI + searches — GETVID.BAT fetched video.zip from a dead resource server. Symptom: + "Unable to cache tsphere.bgf", "couldn't find texture btfx:...", then + "L4VIDEO.cpp couldn't load object plit.bgf" → NULL instance → exit. +- **BTLIVE** (the pod's live snapshot) has the complete 1585-file VIDEO + (GEO/MAT/TEX subdirs, .BSL packs) but its BTL4.RES is REJECTED by its own + exe ("btl4.res v1.0.1.2 is obsolete!"). Working combination = **BTDAVE build + + BTLIVE VIDEO graft** (stage_assets.py --game BTDAVE does this; --game also + now copies product root files + VIDEO recursively; ANIMS staged: 634 files). +- Keyboard/controls: same keyjoy.map Thrustmaster synthesis applies. + +**0x1f record structure CORRECTED (BT broke the fixed-size guess):** +`[n_records]` then n × `[dcs handle][payload]` where payload = 12f full pose +(absolute 3×3+t) OR a **joint angle record [sin, cos]** — 2 floats in BTL4 +v1.1, 5 floats (sin,cos,0,0,0) in RPL4 v1.2. Version-dependent sizes → the +board parses by BACKTRACKING (each record starts with a valid DCS handle; +float bit patterns don't collide with small handle ints; misparse symptom was +det=0 matrices corrupting the camera chain — "landscape rendered, then flipped +to garbage" when per-frame updates began). Joint records = the mech +articulation stream (torso/leg joints, e.g. BT camera chain runs 11 DCS deep +through the mech rig: torso 0xdf1 world pose → ... → cockpit 0xe4a). Joint +sin/cos → local axis semantics still undecoded; flushed matrices stand, so +mech limbs don't articulate yet. + +New fire-and-forget unknowns in BT: 0x24 (frequent), 0x20, 0x26 (once each). + +Rendering findings from the BT bring-up (apply to all products): +- **Per-geometry texture refs**: MUNGA geometry bodies reference their TEXTURE + node directly (word 2; some also their material) — resolving only through + the geogroup→material chain assigned one shared page (every BT terrain tile + got the cloud texture). SceneCache now prefers per-geometry texture/material + refs, geogroup chain as fallback. Side effect: SDEMO's interior sub-panels + now resolve their own BSL slices (visibly richer, wood/teal panels). +- **vtype 0x15 field 6 is NOT alpha**: values run −3.75..1.0 across products + (a blend experiment made SHARKS' kelp ghostly). Stored as mesh['alpha'] data; + semantics undecoded. How the BT cloud/sky layers blended on the real board + is an open question (their 64×64 gray cloud page + SCROLL dv=−1.3 confirmed). +- **BT's parked view is fog-dominated**: its VIEW fog is near=150 far=1250 + color (0.32,0.30,0.65) — the purple "walls" are ground tiles (Y=75, camera + 11 above) and the 4000×4000 sky deck (Y=150) fogging to saturation. The + view-body layout is IDENTICAL across FLYK/RP/BT (fog_enable word17=0x5). +- **"Waiting for translocation!"** — BTL4OPT parks the mech at the map edge in + a LOBBY state until the pod network translocates it into the arena + (NETNUB.EXE / BTNET.BAT are the network hub). This is BT's mission-start + path; the RP equivalent is still undecoded. The arena itself (snow/rock + textured terrain, mountains, structures) renders correctly — verified with + the debug chase camera. +- **VRVIEW_CHASE=1|2** (env): debug chase camera in vrview.cam_matrix — follows + the most-traveled 0x1f-animated DCS from behind-above; =1 turns with the + target's nose, =2 keeps a world-locked offset (no spin). Use to see the + world when the cockpit is fog-bound or a rig is undecoded. +- **MUNGA texture-body floats 10/11 are a STATIC uv offset**, not FLYK's + du/dv-per-second SCROLL — animating them drifted the BT terrain texture + across its mesh "like clouds" (user-observed). Board renders them as a + fixed offset in the MUNGA dialect (semantics inference; FLYK SCROLL + unchanged and regression-validated). +- **Instance stored word 3 = DISPLAY MODE: 3 normal, 2 billboard, 1/0 HIDDEN + until armed.** Census: RP's exactly-4 w3=1 instances are the vehicle's + effect attachments (rendered, they block the cockpit lens); BT's 11 are the + PLAYER MECH awaiting translocation; SHARKS' single w3=0 is the null-object + 'fishes'. The renderer skips w3 ∉ {2,3} in all dialects (regression green; + SDEMO −1 instance). Corollary: the mech is SUPPOSED to be invisible until + translocation re-arms it — expect a re-flush with w3=3 as part of the + translocation sequence (a decode hook for game-start). +- **MUNGA instance chains follow dcs_link parentage** (like the camera): mech/ + vehicle part instances hang off the 0x1f-animated rig via link edges — + nest-only chains rendered them at world origin. FLYK stays nest-only. +- **SCHILD skip is FLYK-only**: MUNGA rig DCSs carry host pointers in the same + stored-word-20 slot (skipping on it hid legit vehicle parts); MUNGA hiding + is governed by instance w3 instead. +- **Texture filtering: the board POINT-SAMPLED** — proven in the i860 source: + the texture inner loop (VRENDER/AS860/SCANLINE.SS, .txinner_loop) issues + exactly one `fld.l texbase(reg)` per pixel from the interpolated u,v; no + four-tap fetch exists. The GL backend's NEAREST default is authentic; + VRVIEW_FILTER=linear opts into bilinear for modern taste. + +### Translocation / mission-start (session 5 close) — network stack UP, needs a peer +BattleTech's "Waiting for translocation!" is a **multi-node network gate**, not a +local flag. The full 1996 pod LAN now runs inside DOSBox-X (run_game.py --netnub): +- `[ne2000] ne2000=true nicbase=300 nicirq=3 backend=auto` in flyk_vr.conf. +- `Z:\SYSTEM\NE2000.COM 0x60 3 0x300` — DOSBox-X's embedded Crynwr packet driver + (registered under Z:\SYSTEM\ only when ne2000=true). The drive's era ODI stack + (LSL+NE2000 MLID+ODIPKT) does NOT work — "An MLID could not be found" — use the + built-in one. WATTCP.CFG (my_ip=200.0.0.86, from the egg) beside the exe. +- BTNET.BAT wrapper: `NETNUB.EXE -f btl4opt.exe -a -egg .egg` (netnub -f runs + the game, -a forwards args). Netnub hosts wattcp, launches the game with + `-net `, prints "Communicating through interrupt vector 0x61". +- Result: game becomes a live network client (further than standalone ever got) + but still draws 0 frames — no peer sends the translocation message. + +**Egg format decoded** (TEST.EGG = plain INI): `[mission] adventure/map=polar3/ +scenario=freeforall/time/weather/temperature/length`, `[pilots] pilot=`, +`[] hostType=0 vehicle=thor dropzone=one name=Milo bitmapindex badge...`, +`[ordinals]`/`[largebitmap]` = HUD digit + pilot-portrait bitmaps (hex rows). +hostType 1/2/3 variants tested standalone — all still park (translocation is the +gate, not hostType). BTL4OPT strings confirm the ladder from AUDIO/SYMBOLS.SCP: +**BTPlayer states DropZoneAcquired(1) → VehicleTranslocated(2) → MissionStarting(3)**. +Translocation is the network event moving the mech from staging into the arena +(POVTranslocateRenderable) — expect the hidden w3=1 mech instances to re-flush +w3=3 when it fires. + +Two paths forward (user decision): (A) reconstruct the mission HOST — BTL4D2S.EXE +(spooling/server build) or `netnub -t` master mode, two networked DOSBox nodes; +(B) hunt_pe.py the state check in BTL4OPT and patch past DropZoneAcquired → +MissionStarting (single-node bypass). RP's mission start is the analogous +undecoded gate. hunt_pe.py located the wait at 0x45d1e0 (state byte at [ebx+0x88], +values 2/9 branch to the print). + +### Remaining to "100%" +- ramp colorization when a scene uses one (unseen on wire so far). +- MUNGA: joint-record axis semantics (mech articulation!), 0x29/0x2b HUD + gauges, 0x24/0x20/0x26, get_geom_numconns (hunt_pe.py ready), mission start + (translocation — host node or binary patch; see above). +- SDEMO VIEWSPLINE camera attach; .EVT arming; .SKL mech articulation test; + validation vs RENDERS/pod video. diff --git a/dpl3-revive/viewer/archive.html b/dpl3-revive/viewer/archive.html new file mode 100644 index 0000000..a198511 --- /dev/null +++ b/dpl3-revive/viewer/archive.html @@ -0,0 +1,320 @@ + + + + + +DPL3 Archive — Virtual World Entertainment, c.1994 + + + + +
+ +

+
+ + + + +
+
drag look around · scroll zoom/dolly · WASD fly · Q/E down/up · auto-rotate returns to orbit
+
select an item from the archive →
+
+ + + diff --git a/dpl3-revive/viewer/archive.py b/dpl3-revive/viewer/archive.py new file mode 100644 index 0000000..a340d9c --- /dev/null +++ b/dpl3-revive/viewer/archive.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +""" +archive.py -- batch-build EVERY resolvable DPL3 scene into the historical archive. + +Writes one JSON per unique scene to viewer/data/ plus a manifest, for the lazy- +loading archive viewer (archive.html + serve.py). Reuses bundle.build_scene. + + python viewer/archive.py [--limit N] +""" +import glob +import json +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +sys.path.insert(0, os.path.join(os.path.dirname(HERE), "parser")) +import bundle +import scn + +DATA = os.path.join(HERE, "data") + + +def unique_scenes(): + """One canonical .SCN per basename, >=60% of its models resolvable.""" + idx = bundle.archive_index() + seen = {} + for s in sorted(glob.glob(bundle.ARCHIVE + "/**/*.SCN", recursive=True)): + name = os.path.basename(s)[:-4].lower() + if name in seen: + continue + try: + sc = scn.load(s) + except Exception: + continue + if not sc.instances: + continue + uniq = set(i.name.lower() for i in sc.instances) + res = sum(1 for n in uniq if n in idx["geo"]) + if uniq and res / len(uniq) >= 0.6: + seen[name] = s + return seen + + +def build_scenes(limit=None): + scenes = unique_scenes() + items = sorted(scenes.items()) + if limit: + items = items[:limit] + print("building %d scenes -> data/" % len(items)) + manifest = [] + for i, (name, path) in enumerate(items): + rel = os.path.relpath(path, bundle.ARCHIVE).replace("\\", "/") + try: + md = bundle.build_scene({"name": name.upper(), "scene": rel, "up": "y"}) + except Exception as e: + print(" SKIP scene %-14s %s" % (name, e)); continue + out = os.path.join(DATA, name + ".json") + with open(out, "w", encoding="utf-8") as fp: + json.dump(md, fp, separators=(",", ":")) + manifest.append({"id": name, "tris": md["tris"], "groups": len(md["groups"]), + "textures": len(md["textures"]), "kb": round(os.path.getsize(out) / 1024)}) + manifest.sort(key=lambda m: -m["tris"]) + return manifest + + +def unique_maps(): + """One canonical .MAP per basename from the game MAPS dirs (skip EDITBACK).""" + seen = {} + for f in sorted(glob.glob(bundle.ARCHIVE + "/**/MAPS/*.MAP", recursive=True)): + if "EDITBACK" in f.upper(): + continue + name = os.path.basename(f)[:-4].lower() + seen.setdefault(name, f) + return seen + + +def build_maps(limit=None): + maps = unique_maps() + mdir = os.path.join(DATA, "maps") + os.makedirs(mdir, exist_ok=True) + items = sorted(maps.items()) + if limit: + items = items[:limit] + print("building %d maps -> data/maps/" % len(items)) + manifest = [] + for name, path in items: + rel = os.path.relpath(path, bundle.ARCHIVE).replace("\\", "/") + try: + md = bundle.build_map({"name": name.upper(), "map": rel}) + if not md["groups"]: + continue + except Exception as e: + print(" SKIP map %-14s %s" % (name, e)); continue + out = os.path.join(mdir, name + ".json") + with open(out, "w", encoding="utf-8") as fp: + json.dump(md, fp, separators=(",", ":")) + manifest.append({"id": name, "tris": md["tris"], "groups": len(md["groups"]), + "textures": len(md["textures"]), "kb": round(os.path.getsize(out) / 1024)}) + manifest.sort(key=lambda m: -m["tris"]) + return manifest + + +def build_models(limit=None): + idx = bundle.archive_index() + mdir = os.path.join(DATA, "models") + os.makedirs(mdir, exist_ok=True) + names = sorted(n for n, p in idx["geo"].items() + if p.lower().endswith((".b2z", ".biz", ".bgf"))) + if limit: + names = names[:limit] + print("building %d models -> data/models/" % len(names)) + manifest = [] + for i, name in enumerate(names): + try: + md = bundle.build_model_archive(idx["geo"][name], name) + if not md["groups"]: + continue + except Exception as e: + print(" SKIP model %-14s %s" % (name, e)); continue + out = os.path.join(mdir, name + ".json") + with open(out, "w", encoding="utf-8") as fp: + json.dump(md, fp, separators=(",", ":")) + manifest.append({"id": name, "tris": md["tris"], "groups": len(md["groups"]), + "textures": len(md["textures"]), "kb": round(os.path.getsize(out) / 1024)}) + if (i + 1) % 50 == 0: + print(" ...%d/%d models" % (i + 1, len(names))) + manifest.sort(key=lambda m: -m["tris"]) + return manifest + + +def classify(id, path): + """Map a scene/model to its VWE product line. Directory is the strong signal + for the themed CONVERT/ and project folders; object name fills the gaps for the + generic geometry/example dirs.""" + rel = os.path.relpath(path, bundle.ARCHIVE).replace(os.sep, "/").lower() + n = id.lower() + if "/btech" in rel or rel[:2] == "bt": + return "BattleTech" + if "hpdave" in rel: + return "Hull Pressure" + if "stdave" in rel: + return "Star Trek" + if any(d in rel for d in ("/canals", "/canyons", "/raptor", "/rocky", "/thor", "/grg")) \ + or rel[:2] == "rp": + return "Red Planet" + if "/mileston" in rel: + return "Milestone" + if "spheres" in rel: + return "Engine / Demo" + # name-driven for generic geometry / example dirs + if any(k in n for k in ("shark", "grouper", "angel", "butter", "golden", "fish", + "ocean", "plant", "coral", "kelp", "atlant", "sea", "reef")): + return "Hull Pressure" + if any(k in n for k in ("trek", "klngn", "klingon", "vulcan", "divcal", "field")): + return "Star Trek" + if any(k in n for k in ("madcat", "vulture", "mech", "loki", "warhawk", "thor", "gauss")): + return "BattleTech" + if any(k in n for k in ("canal", "canyon", "cliff", "rock", "strut", "tcliff", + "redrock", "mars", "raptor", "rapids", "river", "mine")): + return "Red Planet" + return "Engine / Demo" + + +def reclassify(): + """Add/refresh the `product` tag on every manifest entry WITHOUT rebuilding the + (heavy) per-item JSON. Uses the source paths to classify.""" + mpath = os.path.join(DATA, "manifest.json") + man = json.load(open(mpath, encoding="utf-8")) + idx = bundle.archive_index() + scene_paths = unique_scenes() + for s in man.get("scenes", []): + p = scene_paths.get(s["id"]) + s["product"] = classify(s["id"], p) if p else "Engine / Demo" + for m in man.get("models", []): + p = idx["geo"].get(m["id"]) + m["product"] = classify(m["id"], p) if p else "Engine / Demo" + map_paths = unique_maps() + for m in man.get("maps", []): + p = map_paths.get(m["id"]) + m["product"] = classify(m["id"], p) if p else "Engine / Demo" + json.dump(man, open(mpath, "w", encoding="utf-8"), separators=(",", ":")) + from collections import Counter + for key in ("scenes", "models", "maps"): + c = Counter(x["product"] for x in man.get(key, [])) + print("%-8s by product: %s" % (key, dict(c))) + + +def main(): + if "--reclassify" in sys.argv: + reclassify() + return + limit = None + if "--limit" in sys.argv: + limit = int(sys.argv[sys.argv.index("--limit") + 1]) + os.makedirs(DATA, exist_ok=True) + only = [a for a in sys.argv if a.endswith("-only")] + def want(kind): + return not only or ("--%s-only" % kind) in only + scenes = build_scenes(limit) if want("scenes") else _load("scenes") + models = build_models(limit) if want("models") else _load("models") + maps = build_maps(limit) if want("maps") else _load("maps") + with open(os.path.join(DATA, "manifest.json"), "w", encoding="utf-8") as fp: + json.dump({"scenes": scenes, "models": models, "maps": maps}, + fp, separators=(",", ":")) + sm = sum(m["kb"] for m in scenes) / 1024 + mm = sum(m["kb"] for m in models) / 1024 + pm = sum(m["kb"] for m in maps) / 1024 + print("done: %d scenes (%.1f MB), %d models (%.1f MB), %d maps (%.1f MB)" + % (len(scenes), sm, len(models), mm, len(maps), pm)) + + +def _load(key): + try: + return json.load(open(os.path.join(DATA, "manifest.json"), encoding="utf-8")).get(key, []) + except Exception: + return [] + + +if __name__ == "__main__": + main() diff --git a/dpl3-revive/viewer/bundle.py b/dpl3-revive/viewer/bundle.py new file mode 100644 index 0000000..fb3b119 --- /dev/null +++ b/dpl3-revive/viewer/bundle.py @@ -0,0 +1,947 @@ +#!/usr/bin/env python3 +""" +bundle.py -- pack DPL3 models (.B2Z) + textures (.SVT) into a single, self-contained +WebGL viewer HTML. No server, no dependencies, no external assets: geometry is inlined +as JSON and textures as base64 RGBA. Open the output in any browser. + + python viewer/bundle.py # builds viewer/dpl3-viewer.html from SHOWCASE + +The viewer proves the whole recovered art pipeline end-to-end: B2Z geometry + UVs +rendered on modern hardware, textured with decoded SVT bitmaps. +""" +import base64 +import glob +import json +import math +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +sys.path.insert(0, os.path.join(ROOT, "parser")) +import b2z +import scn +import spl +import bsl +import gamemap +import svt + +# the read-only source archive (models/textures are resolved by name from here) +ARCHIVE = r"s:/OneDrive/Tesla III/DaveMcCoy/sda4" + + +# ---- what to put in the viewer --------------------------------------------- +# Each model: file + per-material {color | texture}. Material is matched by the +# f_material name carried in the B2Z; "*" is the fallback for any other material. +SAMPLES = os.path.join(ROOT, "samples") +SHOWCASE = [ + { + "name": "Raptor terrain flyover (fly-through)", + "scene": "DPL3RLS/SCENES/RAPTOR.SCN", + "spline": "DPL3RLS/EXAMPLES/CAMERA.SPL", + "up": "y", + }, + { + "name": "Red Planet canal (assembled scene, 47 objects)", + "scene": "DPL3RLS/SCENES/CANAL.SCN", + "up": "y", + }, + { + "name": "Aquarium reef (underwater scene)", + "scene": "DPL3/EXAMPLES/SHARKS.SCN", + "up": "y", + }, + { + "name": "Star Trek — the Tesla (TREK.SCN)", + "scene": "STDAVE/TREK.SCN", + "up": "y", + }, + { + "name": "Red Planet canyon (textured terrain)", + "file": "CANYONS1.B2Z", + "up": "z", + "materials": { + "redrock_mtl": {"texture": "ROCKNOIZ.SVT", "tint": [1.0, 0.55, 0.4]}, + "water1_mtl": {"color": [0.15, 0.28, 0.5]}, + "*": {"texture": "ROCKNOIZ.SVT", "tint": [1.0, 0.6, 0.45]}, + }, + }, + { + "name": "Test box (multi-material, per-part colour)", + "file": "TESTBOX.B2Z", + "up": "y", + "materials": {"*": {"from_material": True}}, + }, + { + "name": "Sphere (smooth-shaded)", + "file": "BALL.B2Z", + "up": "y", + "materials": {"*": {"color": [0.72, 0.74, 0.8]}}, + }, +] + + +def compute_normals(positions, indices): + n = [0.0] * len(positions) + for t in range(0, len(indices), 3): + i, j, k = indices[t] * 3, indices[t + 1] * 3, indices[t + 2] * 3 + ax, ay, az = positions[i], positions[i + 1], positions[i + 2] + bx, by, bz = positions[j], positions[j + 1], positions[j + 2] + cx, cy, cz = positions[k], positions[k + 1], positions[k + 2] + ux, uy, uz = bx - ax, by - ay, bz - az + vx, vy, vz = cx - ax, cy - ay, cz - az + nx, ny, nz = uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx + for base in (i, j, k): + n[base] += nx; n[base + 1] += ny; n[base + 2] += nz + for o in range(0, len(n), 3): + L = math.sqrt(n[o]**2 + n[o+1]**2 + n[o+2]**2) or 1.0 + n[o] /= L; n[o+1] /= L; n[o+2] /= L + return n + + +_TEX_INFO = {} + + +def load_texture_rgba(path): + """Decode a texture ref to (edge, rgba, is_luminance). + Refs are .SVT paths or ("bsl", path, slice) bit-slice picks. + Full-colour painted skins (vehicle textures) are shown as-is. Greyscale 'noise' + maps, bit-slices, and broken intensity maps (a dead colour channel) are rebuilt + as luminance and flagged -- those get coloured by the material's diffuse.""" + if path in _TEX_INFO: + return _TEX_INFO[path] + if isinstance(path, tuple) and path[0] == "bsl": + b = bsl.load(path[1]) + rgba = bsl.slice_plane(b, path[2]) + res = (b["edge"], rgba, True) + _TEX_INFO[path] = res + return res + data, edge = svt.load(path) + rgba = bytearray(svt.to_rgba(data, "xbgr")) + chan_max = [max(rgba[c::4]) for c in range(3)] + is_lum = False + if min(chan_max) < 32: # a dead channel -> intensity map + for i in range(0, len(rgba), 4): + L = max(rgba[i], rgba[i+1], rgba[i+2]) + rgba[i] = rgba[i+1] = rgba[i+2] = L + is_lum = True + else: # inherently greyscale (low saturation)? + sat = n = 0 + for i in range(0, len(rgba), 4 * 17): + sat += max(rgba[i], rgba[i+1], rgba[i+2]) - min(rgba[i], rgba[i+1], rgba[i+2]) + n += 1 + if n and sat / n < 22: + is_lum = True + res = (edge, bytes(rgba), is_lum) + _TEX_INFO[path] = res + return res + + +def build_texture(svt_path, cache): + if svt_path in cache: + return cache[svt_path] + edge, rgba, _is_lum = load_texture_rgba(svt_path) + idx = len(cache) + cache[svt_path] = idx + build_texture.records.append({ + "w": edge, "h": edge, + "rgba": base64.b64encode(rgba).decode("ascii"), + }) + return idx + + +# ---- scene assembly -------------------------------------------------------- +_INDEX = None +_MODEL_CACHE = {} +_MATS_CACHE = {} +FLIP_V = False # The original renderer indexed texture rows directly: v=0 = SVT + # row 0 (image top). Verified by A/B renders: mech pod vents vs + # missile tubes, the MADCOL "08" insignia, and the shark's + # back/belly shading are all correct UNFLIPPED and inverted when + # flipped. GL's texImage2D also puts the first uploaded row at + # t=0, so v passes through unchanged. + + +def load_model_cached(path): + if path not in _MODEL_CACHE: + _MODEL_CACHE[path] = b2z.load(path) + return _MODEL_CACHE[path] + + +def load_mats_cached(geo_path): + if geo_path not in _MATS_CACHE: + v = v2z_for(geo_path) + _MATS_CACHE[geo_path] = parse_v2z_materials(v) if v else {} + return _MATS_CACHE[geo_path] + + +def archive_index(): + """basename(lower, no ext) -> path indexes over the read-only archive: + geo : geometry (.B2Z/.BIZ first, then game-side .BGF) + tex : .SVT texture maps + bsl : .BSL bit-slice texture packs + v2z : .V2Z text model sources (materials) + mtl : material libraries (.VMF text preferred over .BMF binary)""" + global _INDEX + if _INDEX is None: + geo, tex, v2z, bsl, mtl = {}, {}, {}, {}, {} + for ext in ("B2Z", "BIZ", "BGF"): + for f in glob.glob(ARCHIVE + "/**/*." + ext, recursive=True): + geo.setdefault(os.path.basename(f)[:-4].lower(), f) + for f in glob.glob(ARCHIVE + "/**/*.SVT", recursive=True): + tex.setdefault(os.path.basename(f)[:-4].lower(), f) + for f in glob.glob(ARCHIVE + "/**/*.BSL", recursive=True): + bsl.setdefault(os.path.basename(f)[:-4].lower(), f) + for f in glob.glob(ARCHIVE + "/**/*.V2Z", recursive=True): + v2z.setdefault(os.path.basename(f)[:-4].lower(), f) + for f in glob.glob(ARCHIVE + "/**/*.VMF", recursive=True): + mtl.setdefault(os.path.basename(f)[:-4].lower(), f) + for f in glob.glob(ARCHIVE + "/**/*.BMF", recursive=True): + mtl.setdefault(os.path.basename(f)[:-4].lower(), f) + mod = {} + for f in glob.glob(ARCHIVE + "/**/*.MOD", recursive=True): + mod.setdefault(os.path.basename(f)[:-4].lower(), f) + _INDEX = {"geo": geo, "tex": tex, "v2z": v2z, "bsl": bsl, "mtl": mtl, "mod": mod} + return _INDEX + + +def v2z_for(geo_path): + """The material text source matching a geometry file -- SAME directory/basename + first (geometry and materials must come from one source; different asset dirs + define the same material name with different colours), else any same-name V2Z. + Game-side .BGF models pair with a .VGF text twin the same way.""" + if not geo_path: + return None + base = os.path.splitext(geo_path)[0] + for ext in (".V2Z", ".v2z", ".VGF", ".vgf"): + if os.path.exists(base + ext): + return base + ext + return archive_index()["v2z"].get(os.path.basename(base).lower()) + + +_NAME_RE = re.compile(r'NAME\s*=\s*"?([\w.]+)"?', re.I) +_SPECIAL_RE = re.compile(r'SPECIAL\s*=\s*"([^"]*)"', re.I) +_SCROLL_RE = re.compile(r"SCROLL\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)") +_MAP_RE = re.compile(r"MAP\s*\{\"?(\w+)\"?\}") +_DIF_RE = re.compile(r"DIFFUSE\s*\{([^}]*)\}") +_MTLTEX_RE = re.compile(r"TEXTURE\s*\{\"?([\w.]+)\"?\}") +_SLICE_RE = re.compile(r"BITSLICE\s*\{(\d+)\}") + + +def _blocks(text, kw): + """yield (header, body) for `KW(header) {body}` blocks, brace-matched. + Two source dialects exist: modeller exports (NAME=x, `// End of ...` marker + comments) and the compact game libraries (NAME="x", no markers, SPECIAL= + hooks in the header). Matching braces handles both; `KW {...}` references + (no parens) inside other bodies are not block starts and are skipped.""" + pos = 0 + while True: + j = text.find(kw + "(", pos) + if j < 0: + return + if j and (text[j - 1].isalnum() or text[j - 1] == "_"): + pos = j + len(kw) + continue + h_end = text.find(")", j) + if h_end < 0: + return + b = text.find("{", h_end) + nxt = text.find(kw + "(", h_end) + if b < 0 or (0 <= nxt < b): # headerless block (no body braces) + yield text[j + len(kw) + 1:h_end], "" + pos = h_end + 1 + continue + depth, e = 0, b + for e in range(b, len(text)): + c = text[e] + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + if depth == 0: + break + yield text[j + len(kw) + 1:h_end], text[b + 1:e] + pos = e + 1 + + +def _parse_scroll(special): + """texture SPECIAL hook ' SCROLL u0 v0 du dv' -> [u0,v0,du,dv] or None.""" + if not special: + return None + m = _SCROLL_RE.search(special) + return [float(m.group(i)) for i in range(1, 5)] if m else None + + +def parse_v2z_materials(path): + """Authoritative materials from a .V2Z/.VGF/.VMF text source: + name -> (diffuse[3], map_name_or_None, bitslice, scroll_or_None, texname). + scroll is the texture entity's SPECIAL " SCROLL u0 v0 du dv" hook (UV drift + rates -- rivers, smoke, laser beams); texname is the TEXTURE entity name a + scene-side SCROLL override addresses.""" + try: + t = open(path, encoding="latin-1").read() + except OSError: + return {} + texmap = {} + for header, body in _blocks(t, "TEXTURE"): + nm = _NAME_RE.search(header) + if not nm: + continue + mm = _MAP_RE.search(body) + sl = _SLICE_RE.search(body) + sp = _SPECIAL_RE.search(header) + texmap[nm.group(1).lower()] = (mm.group(1).lower() if mm else None, + int(sl.group(1)) if sl else 0, + _parse_scroll(sp.group(1) if sp else None)) + out = {} + for header, body in _blocks(t, "MATERIAL"): + nm = _NAME_RE.search(header) + if not nm: + continue + dif = _DIF_RE.search(body) + diffuse = [float(x) for x in dif.group(1).split(",")] if dif else [0.7, 0.7, 0.7] + tx = _MTLTEX_RE.search(body) + texname = tx.group(1).lower() if tx else None + tmap, sl, scroll = texmap.get(texname, (None, 0, None)) if texname else (None, 0, None) + out[nm.group(1).lower()] = (diffuse, tmap, sl, scroll, texname) + return out + + +_LIB_CACHE = {} + + +def load_material_library(libname): + """Load a named material library (`stships` in `stships:eblu4_mtl`). + Text .VMF preferred; binary .BMF parsed with the b2z reader (which now + captures the 0x018 BITSLICE tag).""" + key = libname.lower() + if key in _LIB_CACHE: + return _LIB_CACHE[key] + path = archive_index()["mtl"].get(key) + mats = {} + if path and path.lower().endswith((".vmf", ".vgf", ".v2z")): + mats = parse_v2z_materials(path) + elif path: + try: + lib = b2z.load(path) + for name, mt in lib.materials.items(): + tx = lib.textures.get(mt.texture) if mt.texture else None + mats[name.lower()] = (list(mt.diffuse), + tx.mapfile.lower() if tx and tx.mapfile else None, + tx.bitslice if tx else 0, + _parse_scroll(tx.special) if tx else None, + mt.texture.lower() if mt.texture else None) + except Exception: + mats = {} + _LIB_CACHE[key] = mats + return mats + + +# fallback surface colours when a material has no texture (keyed by name substring) +COLOR_HINTS = [ + ("water", [0.15, 0.28, 0.5]), ("redrock", [0.55, 0.32, 0.24]), + ("rednoi", [0.58, 0.3, 0.24]), ("rock", [0.5, 0.42, 0.38]), + ("techy", [0.5, 0.52, 0.57]), ("grey", [0.55, 0.55, 0.58]), + ("metal", [0.55, 0.57, 0.6]), ("sky", [0.4, 0.6, 0.8]), +] + + +# when a material has no exact .SVT, fall back to a representative texture +# for common terrain surfaces (the authoritative map lives in material libraries +# we haven't decoded; this is a documented plausibility fallback -- see SCN spec). +# keyword -> (texture stem, tint). Grey noise textures are tinted by the material +# colour, as the original renderer did (e.g. red rock = grey rock-noise * red diffuse). +TEXTURE_HINTS = [ + ("redrock", ("rocknoiz", [1.0, 0.50, 0.38])), + ("rednoi", ("rocknoiz", [1.0, 0.50, 0.38])), + ("cliff", ("rocknoiz", [1.0, 0.50, 0.38])), + ("rock", ("rocknoiz", [0.95, 0.62, 0.52])), + ("techy", ("generic", [0.78, 0.80, 0.85])), +] + + +def surface_for(matname, has_uv, idx, tint=(1.0, 1.0, 1.0)): + """Resolve a material name to (texture_path_or_None, color). + First the exact stem==texture convention (e.g. 'vtvcab_mtl' -> VTVCAB.SVT), + then a keyword texture fallback (tinted), then a flat colour.""" + stem = (matname or "").lower() + if stem.endswith("_mtl"): + stem = stem[:-4] + if has_uv and stem in idx["tex"]: + return idx["tex"][stem], list(tint) + if has_uv: + for key, (tname, ttint) in TEXTURE_HINTS: + if key in stem and tname in idx["tex"]: + return idx["tex"][tname], list(ttint) + for key, col in COLOR_HINTS: + if key in stem: + return None, col + return None, [0.62, 0.6, 0.6] + + +def bake_instances(instances, idx, scrolls=None): + """Bake a list of (model, materials, matrix, model_name) into merged, textured + surfaces in world space. Shared by whole scenes and single-model previews. + `scrolls` is the scene's SCROLL override map: texture entity id ("lib:name" + or plain name) -> [u0,v0,du,dv]; a texture's own SPECIAL scroll applies + even without an override. Returns (groups, tex_records, bounds, tris).""" + surfaces = {} # (texpath, colour) -> dict(pos,normal,uv,idx,color,texture) + tex_ids = {} + tex_records = [] + + def resolve(matname, has_uv, mats, model_name=None): + """-> (texture_ref|None, colour, from_material, scroll|None, texid|None). + A texture_ref is an .SVT path or ("bsl", path, slice) for bit-slice packs; + texid is the addressable texture entity ("lib:name" / plain name). + + Namespaced names (`stships:eblu4_mtl`, the game-side convention) resolve + through the named material library (.VMF/.BMF) and use its declared map + directly. Plain names use the model's own materials with candidates: + per-part skin .SVT, the declared map, then the model-name skin.""" + mname = (matname or "").lower() + if ":" in mname: + lib, sub = mname.split(":", 1) + info = load_material_library(lib).get(sub) + if info is not None: + diffuse, tmap, sl, scroll, texname = info + tref = None + if has_uv and tmap: + if tmap in idx["bsl"]: + tref = ("bsl", idx["bsl"][tmap], sl) + elif tmap in idx["tex"]: + tref = idx["tex"][tmap] + return tref, list(diffuse), True, scroll, \ + (lib + ":" + texname) if texname else None + return None, [0.62, 0.6, 0.6], True, None, None + info = mats.get(mname) + stem = mname[:-4] if mname.endswith("_mtl") else mname + tmap, sl, scroll, texname = (info[1], info[2], info[3], info[4]) if info \ + else (None, 0, None, None) + tref = None + if has_uv: + for cand in (stem, tmap, model_name): + if cand and cand in idx["tex"]: + tref = idx["tex"][cand] + break + if tref is None and tmap and tmap in idx["bsl"]: + tref = ("bsl", idx["bsl"][tmap], sl) + if info is not None: + return tref, list(info[0]), True, scroll, texname + if tref is not None: # generic material, but a model skin exists + return tref, [1.0, 1.0, 1.0], True, None, None + tp, color = surface_for(matname, has_uv, idx) + return tp, color, False, None, None + + def get_surface(matname, has_uv, mats, model_name=None, anim=None, akey=None): + tpath, color, from_mat, scroll, texid = resolve(matname, has_uv, mats, model_name) + if anim is None and tpath is not None: + rates = (scrolls or {}).get(texid, scroll) + if rates and (rates[2] or rates[3]): + anim = {"type": "scroll", "rate": [rates[2], rates[3]], + "phase": [rates[0], rates[1]]} + akey = ("scroll", texid, rates[2], rates[3]) + tid = -1 + if tpath is not None: + edge, rgba, is_lum = load_texture_rgba(tpath) + if from_mat and not is_lum: + color = [1.0, 1.0, 1.0] # full-colour skin: show texture as-is + if tpath not in tex_ids: + tex_ids[tpath] = len(tex_records) + tex_records.append({"w": edge, "h": edge, + "rgba": base64.b64encode(rgba).decode("ascii")}) + tid = tex_ids[tpath] + # key on texture AND colour so materials sharing a texture but differing + # in tint stay separate; animated parts get their own surfaces (akey) so + # the viewer can move them independently of the static batch. + key = (tpath, tuple(round(c, 4) for c in color), akey) + if key not in surfaces: + surfaces[key] = {"positions": [], "normals": [], "uvs": [], + "indices": [], "color": color, "texture": tid} + if anim: + surfaces[key]["anim"] = anim + return surfaces[key] + + for inst in instances: + model, mats, M, mname = inst[0], inst[1], inst[2], inst[3] + anim = inst[4] if len(inst) > 4 else None + if model is None: + continue + akey = id(anim) if anim else None + for g in model.geogroups: + uv_present = any(geom.verts and geom.verts[0].uv for geom in g.geoms) + surf = get_surface(g.f_material, uv_present, mats, mname, anim, akey) + for geom in g.geoms: + gbase = len(surf["positions"]) // 3 + for v in geom.verts: + wx, wy, wz = scn.xform_point(M, v.pos[0], v.pos[1], v.pos[2]) + surf["positions"] += [wx, wy, wz] + if v.normal: + nx, ny, nz = scn.xform_dir(M, *v.normal) + else: + nx, ny, nz = 0.0, 1.0, 0.0 + surf["normals"] += [nx, ny, nz] + if v.uv: + surf["uvs"] += [v.uv[0], (1.0 - v.uv[1]) if FLIP_V else v.uv[1]] + else: + surf["uvs"] += [0.0, 0.0] + for (a, b, c) in b2z.triangles(geom): + surf["indices"] += [gbase + a, gbase + b, gbase + c] + + groups = [] + for s in surfaces.values(): + if not s["indices"]: + continue + g = { + "positions": [round(x, 3) for x in s["positions"]], + "normals": [round(x, 4) for x in s["normals"]], + "uvs": [round(x, 4) for x in s["uvs"]], + "indices": s["indices"], "color": s["color"], "texture": s["texture"], + } + if s.get("anim"): + g["anim"] = s["anim"] + groups.append(g) + xs = [g["positions"][i] for g in groups for i in range(0, len(g["positions"]), 3)] + ys = [g["positions"][i+1] for g in groups for i in range(0, len(g["positions"]), 3)] + zs = [g["positions"][i+2] for g in groups for i in range(0, len(g["positions"]), 3)] + bounds = {"min": [min(xs), min(ys), min(zs)], "max": [max(xs), max(ys), max(zs)]} if xs \ + else {"min": [-1, -1, -1], "max": [1, 1, 1]} + tris = sum(len(g["indices"]) for g in groups) // 3 + return groups, tex_records, bounds, tris + + +def build_model_archive(geo_path, name): + """Build one standalone model (identity transform) with authoritative materials.""" + idx = archive_index() + model = load_model_cached(geo_path) + mats = load_mats_cached(geo_path) + groups, tex_records, bounds, tris = bake_instances( + [(model, mats, scn.ident(), name.lower())], idx) + return {"name": name.upper(), "up": "y", "groups": groups, + "textures": tex_records, "bounds": bounds, "tris": tris} + + +def _tree_file(map_path, subdir, base, exts): + """Look for /..//. -- assets from the SAME game + tree as the map (BTDAVE maps use BTDAVE models, not BTLIVE's).""" + root = os.path.dirname(os.path.dirname(map_path)) + for d in (os.path.join(root, subdir), os.path.join(root, subdir, "EDITBACK")): + for ext in exts: + p = os.path.join(d, base + ext) + if os.path.exists(p): + return p + return None + + +def _dropzone_marker(x, y, z): + """A small red pyramid marking a pod dropzone.""" + s, h = 1.5, 4.0 + pos = [x, y + h, z, x - s, y, z - s, x + s, y, z - s, + x + s, y, z + s, x - s, y, z + s] + idx = [0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 1, 1, 3, 2, 1, 4, 3] + return pos, idx + + +def build_map(cfg): + """Assemble a game .MAP (arena / track) into a renderable scene.""" + map_path = os.path.join(ARCHIVE, cfg["map"]) + idx = archive_index() + + def mod_resolver(modfile): + """-> [(bgf_name, local_anim_or_None), ...] for every video object in the + MOD. Door leaves get a data-driven slide from the .SUB (MotionExtent / + TravelTime / DeadTime); laser-drill objects ('laz*') get a glow pulse.""" + base = os.path.splitext(os.path.basename(modfile or ""))[0].lower() + if not base: + return [] + p = _tree_file(map_path, "MODELS", base, (".MOD",)) or idx["mod"].get(base) + if not p: + return [] + names, gclass, subfile = gamemap.parse_mod_objects(p) + doors = {} + if subfile: + sp = os.path.join(os.path.dirname(p), os.path.basename(subfile)) + if os.path.exists(sp): + doors = gamemap.parse_sub(sp) + out = [] + for n in names: + anim = None + if n in doors: + d = doors[n] + anim = {"type": "slide", "extent": d["extent"], + "travel": d["travel"], "dead": d["dead"]} + elif n.startswith("laz"): + anim = {"type": "pulse"} # laser drill: engine-driven; approximated + out.append((n, anim)) + return out + + placements, dropzones = gamemap.expand(map_path, mod_resolver) + instances, missing = [], set() + for (bgf, anim), M in placements: + p = _tree_file(map_path, "VIDEO", bgf, (".BGF",)) or idx["geo"].get(bgf) + if not p: + missing.add(bgf) + continue + wanim = None + if anim and anim.get("type") == "slide": + ex, ey, ez = scn.xform_dir(M, *anim["extent"]) # local -> world axis + wanim = {"type": "slide", "extent": [round(ex, 3), round(ey, 3), round(ez, 3)], + "travel": anim["travel"], "dead": anim["dead"]} + elif anim: + wanim = dict(anim) + instances.append((load_model_cached(p), load_mats_cached(p), M, bgf, wanim)) + groups, tex_records, bounds, tris = bake_instances(instances, idx) + + if dropzones: + pos, nrm, uv, ind = [], [], [], [] + for d in dropzones: + mp, mi = _dropzone_marker(d[0], d[1], d[2]) + base = len(pos) // 3 + pos += mp + nrm += [0.0, 1.0, 0.0] * (len(mp) // 3) + uv += [0.0, 0.0] * (len(mp) // 3) + ind += [base + i for i in mi] + groups.append({"positions": [round(v, 3) for v in pos], "normals": nrm, + "uvs": uv, "indices": ind, + "color": [0.9, 0.1, 0.1], "texture": -1}) + + out = {"name": cfg["name"], "up": "y", "groups": groups, + "textures": tex_records, "bounds": bounds, "tris": tris, + "background": [0.05, 0.05, 0.08]} + print(" map %-22s %d placements (%d missing: %s), %d dropzones, %d surfaces" + % (os.path.basename(cfg["map"]), len(placements), len(missing), + sorted(missing)[:4], len(dropzones), len(groups))) + return out + + +def build_scene(cfg): + sc = scn.load(os.path.join(ARCHIVE, cfg["scene"])) + idx = archive_index() + cache, mat_cache = {}, {} + for inst in sc.instances: + n = inst.name.lower() + if n not in cache: + path = idx["geo"].get(n) + cache[n] = load_model_cached(path) if path else None + mat_cache[n] = load_mats_cached(path) if path else {} + + instances = [(cache.get(i.name.lower()), mat_cache.get(i.name.lower(), {}), + i.matrix, i.name.lower()) for i in sc.instances] + placed = sum(1 for m, _, _, _ in instances if m is not None) + groups, tex_records, bounds, tris = bake_instances(instances, idx, sc.scrolls) + + out = { + "name": cfg["name"], "up": cfg.get("up", "y"), "groups": groups, + "textures": tex_records, "bounds": bounds, "tris": tris, + "background": list(sc.background), + } + if sc.fog: + z0, z1, r, g, b = sc.fog + out["fog"] = [r, g, b, z0, z1] + if cfg.get("spline"): + frames = spl.sample_flythrough(os.path.join(ARCHIVE, cfg["spline"])) + out["flythrough"] = [{"eye": [round(x, 2) for x in f["eye"]], + "center": [round(x, 2) for x in f["center"]], + "up": [round(x, 3) for x in f["up"]]} for f in frames] + print(" scene %-22s %d/%d objects placed, %d surfaces, %d textures%s" + % (os.path.basename(cfg["scene"]), placed, len(sc.instances), + len(groups), len(tex_records), + ", %d camera frames" % len(out["flythrough"]) if "flythrough" in out else "")) + return out + + +def build_model(cfg): + if "scene" in cfg: + return build_scene(cfg) + model = b2z.load(os.path.join(SAMPLES, cfg["file"])) + idx = archive_index() + v2z = idx["v2z"].get(os.path.splitext(cfg["file"])[0].lower()) + mats = parse_v2z_materials(v2z) if v2z else {} + tex_cache = {} + build_texture.records = [] + groups = [] + for g in model.geogroups: + # merge every geometry in the geogroup into one indexed mesh + pos, uv, ind = [], [], [] + have_uv = False + for geom in g.geoms: + base = len(pos) // 3 + for v in geom.verts: + pos += [v.pos[0], v.pos[1], v.pos[2]] + if v.uv: + have_uv = True + uv += [v.uv[0], 1.0 - v.uv[1]] # flip V for GL origin + else: + uv += [0.0, 0.0] + for (a, b, c) in b2z.triangles(geom): + ind += [base + a, base + b, base + c] + if not ind: + continue + + # resolve material -> colour / texture. Prefer the model's own authoritative + # material (.V2Z: diffuse x texture); fall back to the showcase cfg. + color = [0.7, 0.7, 0.72] + tex = -1 + info = mats.get((g.f_material or "").lower()) + if info is not None: + diffuse, tmap = info[0], info[1] + color = list(diffuse) + if have_uv and tmap and tmap in idx["tex"]: + tex = build_texture(idx["tex"][tmap], tex_cache) # modulated by diffuse + else: + spec = cfg.get("materials", {}).get(g.f_material) or cfg.get("materials", {}).get("*", {}) + tint = spec.get("tint", [1.0, 1.0, 1.0]) + if spec.get("from_material") and g.f_material in model.materials: + d = model.materials[g.f_material].diffuse + color = [d[0], d[1], d[2]] + elif "color" in spec: + color = spec["color"] + if "texture" in spec and have_uv: + tex = build_texture(os.path.join(SAMPLES, spec["texture"]), tex_cache) + color = tint + + groups.append({ + "positions": [round(x, 4) for x in pos], + "normals": [round(x, 4) for x in compute_normals(pos, ind)], + "uvs": [round(x, 4) for x in uv], + "indices": ind, + "color": color, + "texture": tex, + }) + + # bounds + xs = [g["positions"][i] for g in groups for i in range(0, len(g["positions"]), 3)] + ys = [g["positions"][i+1] for g in groups for i in range(0, len(g["positions"]), 3)] + zs = [g["positions"][i+2] for g in groups for i in range(0, len(g["positions"]), 3)] + bounds = {"min": [min(xs), min(ys), min(zs)], "max": [max(xs), max(ys), max(zs)]} + tris = sum(len(g["indices"]) for g in groups) // 3 + return { + "name": cfg["name"], "up": cfg["up"], "groups": groups, + "textures": build_texture.records, "bounds": bounds, "tris": tris, + } + + +def main(): + scene = {"models": [build_model(c) for c in SHOWCASE]} + payload = json.dumps(scene, separators=(",", ":")) + html = TEMPLATE.replace("/*__SCENE__*/null", payload) + out = os.path.join(HERE, "dpl3-viewer.html") + with open(out, "w", encoding="utf-8") as fp: + fp.write(html) + kb = len(html) / 1024 + print("wrote %s (%.0f KB, %d models)" % (out, kb, len(scene["models"]))) + for m in scene["models"]: + print(" - %-42s %5d tris, %d textures" % (m["name"], m["tris"], len(m["textures"]))) + + +# ---- the viewer (content-only: no // so it doubles as an +# Artifact source; browsers render it fine opened directly too) ---------- +TEMPLATE = r"""DPL3 Model Viewer + + +
+
DPL3 · recovered art
+

Model Viewer

+
Virtual World Entertainment, c.1994 — on modern hardware
+ +
triangles
+
geo​groups
+
textures
+ + + +
drag to orbit · scroll to zoom
+
+
+""" + + +if __name__ == "__main__": + main() diff --git a/dpl3-revive/viewer/canal_fpv.png b/dpl3-revive/viewer/canal_fpv.png new file mode 100644 index 0000000..6bf2b9d Binary files /dev/null and b/dpl3-revive/viewer/canal_fpv.png differ diff --git a/dpl3-revive/viewer/flythru_mid.png b/dpl3-revive/viewer/flythru_mid.png new file mode 100644 index 0000000..a5b2be5 Binary files /dev/null and b/dpl3-revive/viewer/flythru_mid.png differ diff --git a/dpl3-revive/viewer/flythru_start.png b/dpl3-revive/viewer/flythru_start.png new file mode 100644 index 0000000..917d7a7 Binary files /dev/null and b/dpl3-revive/viewer/flythru_start.png differ diff --git a/dpl3-revive/viewer/preview_0.png b/dpl3-revive/viewer/preview_0.png new file mode 100644 index 0000000..06caee5 Binary files /dev/null and b/dpl3-revive/viewer/preview_0.png differ diff --git a/dpl3-revive/viewer/preview_1.png b/dpl3-revive/viewer/preview_1.png new file mode 100644 index 0000000..c2094cd Binary files /dev/null and b/dpl3-revive/viewer/preview_1.png differ diff --git a/dpl3-revive/viewer/preview_box.png b/dpl3-revive/viewer/preview_box.png new file mode 100644 index 0000000..c2094cd Binary files /dev/null and b/dpl3-revive/viewer/preview_box.png differ diff --git a/dpl3-revive/viewer/preview_canyon.png b/dpl3-revive/viewer/preview_canyon.png new file mode 100644 index 0000000..06caee5 Binary files /dev/null and b/dpl3-revive/viewer/preview_canyon.png differ diff --git a/dpl3-revive/viewer/preview_scene.png b/dpl3-revive/viewer/preview_scene.png new file mode 100644 index 0000000..9c98ad6 Binary files /dev/null and b/dpl3-revive/viewer/preview_scene.png differ diff --git a/dpl3-revive/viewer/reef.png b/dpl3-revive/viewer/reef.png new file mode 100644 index 0000000..1356a75 Binary files /dev/null and b/dpl3-revive/viewer/reef.png differ diff --git a/dpl3-revive/viewer/render_preview.py b/dpl3-revive/viewer/render_preview.py new file mode 100644 index 0000000..daa2c80 --- /dev/null +++ b/dpl3-revive/viewer/render_preview.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +render_preview.py -- CPU cross-check of the viewer pipeline. + +Consumes exactly what the WebGL viewer consumes (bundle.build_model output: +merged positions/normals/uvs/indices + decoded SVT texture) and software-rasterizes +one model to a PNG. If this looks right, the WebGL viewer -- fed identical data -- +renders the same. Verification, not the product. + + python viewer/render_preview.py 0 out.png # model index (default 0) +""" +import base64 +import math +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +sys.path.insert(0, os.path.join(os.path.dirname(HERE), "parser")) +import bundle +import svt + +W, H = 640, 480 + + +def mv(m, v): + return [sum(m[r][c] * v[c] for c in range(4)) for r in range(4)] + + +def matmul(a, b): + return [[sum(a[r][k] * b[k][c] for k in range(4)) for c in range(4)] for r in range(4)] + + +def look_at(eye, ctr, up): + def sub(a, b): return [a[0]-b[0], a[1]-b[1], a[2]-b[2]] + def crs(a, b): return [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]] + def nrm(a): + l = math.hypot(*a) or 1.0; return [a[0]/l, a[1]/l, a[2]/l] + def dot(a, b): return a[0]*b[0]+a[1]*b[1]+a[2]*b[2] + z = nrm(sub(eye, ctr)); x = nrm(crs(up, z)); y = crs(z, x) + return [[x[0], x[1], x[2], -dot(x, eye)], + [y[0], y[1], y[2], -dot(y, eye)], + [z[0], z[1], z[2], -dot(z, eye)], + [0, 0, 0, 1]] + + +def persp(fovy, asp, n, f): + t = 1/math.tan(fovy/2) + return [[t/asp, 0, 0, 0], [0, t, 0, 0], + [0, 0, (f+n)/(n-f), 2*f*n/(n-f)], [0, 0, -1, 0]] + + +def render(md, outpath, cam=None): + # decode textures to (w,h,rgba bytes) + texs = [] + for t in md["textures"]: + texs.append((t["w"], t["h"], base64.b64decode(t["rgba"]))) + + b = md["bounds"] + ctr = [(b["min"][i]+b["max"][i])/2 for i in range(3)] + rad = math.hypot(*(b["max"][i]-b["min"][i] for i in range(3)))/2 or 1.0 + up = [0, 0, 1] if md["up"] == "z" else [0, 1, 0] + az, el, dist = 0.9, 0.55, rad*2.6 + ce = math.cos(el) + if md["up"] == "z": + eye = [ctr[0]+dist*ce*math.sin(az), ctr[1]+dist*ce*math.cos(az), ctr[2]+dist*math.sin(el)] + else: + eye = [ctr[0]+dist*ce*math.sin(az), ctr[1]+dist*math.sin(el), ctr[2]+dist*ce*math.cos(az)] + if cam is not None: + eye, ctr, up = cam["eye"], cam["center"], cam["up"] + P = matmul(persp(1.0, W/H, 50, rad*5+2000), look_at(eye, ctr, up)) + else: + P = matmul(persp(1.0, W/H, dist*0.02, dist*10+rad*4), look_at(eye, ctr, up)) + L = [0.4, 0.7, 0.6]; ll = math.hypot(*L); L = [c/ll for c in L] + + img = bytearray(b"\x1a\x14\x10" * (W*H)) # dark ground + zbuf = [1e30]*(W*H) + + for g in md["groups"]: + pos, nrm, uv, idx = g["positions"], g["normals"], g["uvs"], g["indices"] + col = g["color"]; ti = g["texture"] + tex = texs[ti] if ti >= 0 else None + # project all verts + sx = [0.0]*(len(pos)//3); sy = list(sx); sz = list(sx); sw = list(sx) + for i in range(len(pos)//3): + c = mv(P, [pos[i*3], pos[i*3+1], pos[i*3+2], 1.0]) + w = c[3] if abs(c[3]) > 1e-9 else 1e-9 + sw[i] = w + sx[i] = (c[0]/w*0.5+0.5)*W + sy[i] = (1-(c[1]/w*0.5+0.5))*H + sz[i] = c[2]/w + for t in range(0, len(idx), 3): + a, bb, cc = idx[t], idx[t+1], idx[t+2] + if sw[a] <= 0 or sw[bb] <= 0 or sw[cc] <= 0: + continue + x0, y0, x1, y1, x2, y2 = sx[a], sy[a], sx[bb], sy[bb], sx[cc], sy[cc] + area = (x1-x0)*(y2-y0)-(x2-x0)*(y1-y0) + if abs(area) < 1e-9: + continue + minx = max(0, int(min(x0, x1, x2))); maxx = min(W-1, int(max(x0, x1, x2))+1) + miny = max(0, int(min(y0, y1, y2))); maxy = min(H-1, int(max(y0, y1, y2))+1) + iw = [1.0/sw[a], 1.0/sw[bb], 1.0/sw[cc]] + # world normals for lighting + nl = [] + for vi in (a, bb, cc): + nx, ny, nz = nrm[vi*3], nrm[vi*3+1], nrm[vi*3+2] + nl.append(abs(nx*L[0]+ny*L[1]+nz*L[2])) + for py in range(miny, maxy+1): + for px in range(minx, maxx+1): + w0 = ((x1-px)*(y2-py)-(x2-px)*(y1-py))/area + w1 = ((x2-px)*(y0-py)-(x0-px)*(y2-py))/area + w2 = 1-w0-w1 + if w0 < 0 or w1 < 0 or w2 < 0: + continue + z = w0*sz[a]+w1*sz[bb]+w2*sz[cc] + o = py*W+px + if z >= zbuf[o]: + continue + zbuf[o] = z + lit = 0.35+0.65*(w0*nl[0]+w1*nl[1]+w2*nl[2]) + if tex: + # perspective-correct uv + pw = w0*iw[0]+w1*iw[1]+w2*iw[2] + u = (w0*uv[a*2]*iw[0]+w1*uv[bb*2]*iw[1]+w2*uv[cc*2]*iw[2])/pw + v = (w0*uv[a*2+1]*iw[0]+w1*uv[bb*2+1]*iw[1]+w2*uv[cc*2+1]*iw[2])/pw + tw, th, td = tex + tx = int((u % 1.0)*tw) % tw; tyv = int((v % 1.0)*th) % th + tp = (tyv*tw+tx)*4 + r, gg, bl = td[tp], td[tp+1], td[tp+2] + r *= col[0]; gg *= col[1]; bl *= col[2] + else: + r, gg, bl = col[0]*255, col[1]*255, col[2]*255 + # Division DAC gamma 1.7 (out = in^(1/1.25)) + img[o*3] = min(255, int(((r*lit/255.0)**(1/1.25))*255)) + img[o*3+1] = min(255, int(((gg*lit/255.0)**(1/1.25))*255)) + img[o*3+2] = min(255, int(((bl*lit/255.0)**(1/1.25))*255)) + + # write PNG (reuse svt's encoder, but it expects RGBA) + rgba = bytearray(W*H*4) + for i in range(W*H): + rgba[i*4:i*4+3] = img[i*3:i*3+3]; rgba[i*4+3] = 255 + svt.write_png(outpath, W, H, bytes(rgba)) + print("wrote %s (%dx%d, model=%r, %d tris)" % (outpath, W, H, md["name"], md["tris"])) + + +if __name__ == "__main__": + i = int(sys.argv[1]) if len(sys.argv) > 1 else 0 + out = sys.argv[2] if len(sys.argv) > 2 else os.path.join(HERE, "preview_%d.png" % i) + render(bundle.build_model(bundle.SHOWCASE[i]), out) diff --git a/dpl3-revive/viewer/serve.py b/dpl3-revive/viewer/serve.py new file mode 100644 index 0000000..03964eb --- /dev/null +++ b/dpl3-revive/viewer/serve.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +""" +serve.py -- launch the DPL3 scene archive locally. + +The archive viewer lazy-loads scene JSON via fetch(), which browsers block over +file://. This serves the viewer/ folder over http so it works. Run it, then the +browser opens automatically. + +Threaded (parallel requests don't queue behind a large scene download) and sends +Cache-Control: no-store so a truncated response can never be cached and replayed +by the browser. + + python viewer/serve.py +""" +import http.server +import os +import webbrowser + +PORT = 8765 +os.chdir(os.path.dirname(os.path.abspath(__file__))) + + +class Handler(http.server.SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header("Cache-Control", "no-store") + super().end_headers() + + def log_message(self, fmt, *args): # quieter console + pass + + +url = "http://localhost:%d/archive.html" % PORT +print("DPL3 scene archive -> %s (Ctrl+C to stop)" % url) +try: + webbrowser.open(url) +except Exception: + pass +with http.server.ThreadingHTTPServer(("", PORT), Handler) as httpd: + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nstopped") diff --git a/dpl3-revive/viewer/trek_klngn.png b/dpl3-revive/viewer/trek_klngn.png new file mode 100644 index 0000000..995cd33 Binary files /dev/null and b/dpl3-revive/viewer/trek_klngn.png differ diff --git a/dpl3-revive/viewer/trek_scene.png b/dpl3-revive/viewer/trek_scene.png new file mode 100644 index 0000000..831d501 Binary files /dev/null and b/dpl3-revive/viewer/trek_scene.png differ diff --git a/dpl3-revive/viewer/trek_tesall.png b/dpl3-revive/viewer/trek_tesall.png new file mode 100644 index 0000000..9bfc283 Binary files /dev/null and b/dpl3-revive/viewer/trek_tesall.png differ diff --git a/dpl3-revive/viewer/vf_canal.png b/dpl3-revive/viewer/vf_canal.png new file mode 100644 index 0000000..eb561da Binary files /dev/null and b/dpl3-revive/viewer/vf_canal.png differ diff --git a/dpl3-revive/viewer/vf_mad_front.png b/dpl3-revive/viewer/vf_mad_front.png new file mode 100644 index 0000000..9270c07 Binary files /dev/null and b/dpl3-revive/viewer/vf_mad_front.png differ