From b4c2337ab1f0dfdacc9e126d699e47f6ee703d14 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 19 Jul 2026 16:10:49 -0500 Subject: [PATCH] Battle geometry, rendered honestly: coherent arena floor + horizon + sky Fixed the readout: the effect packets are 3-4 edge TEXTURED quads (0x2c/0x42/ 0x0d edges + z + texz/texu/texv planes -- NO Gouraud colour, confirming the scene is textured). Two bugs in the first pass: (1) 0x2c edges were treated as 'accept both sides', turning every quad into a full-plane fill; (2) only 2-edge packets were caught, dropping ~3/4 of the geometry. render_battle.py applies the render_wide30 polygon-winding rule (inside = all-edges>=0 OR all<0) -> 78 polygons of 141 setups. Result (flat-per-polygon + depth): a geometrically coherent first-person scene -- sky at far depth, a ground plane ramping smoothly from horizon to foreground, a clean horizon line, upright structures. The perspective is consistent; this is real scene geometry, not the muddy u/v wedges of the first attempt. Surfaces still need texels (TXDN/M3) for their skin. Co-Authored-By: Claude Opus 4.8 --- .../firmware-decomp/emu860c/battle_probe.py | 43 +++++++++ .../firmware-decomp/emu860c/render_battle.py | 90 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 emulator/firmware-decomp/emu860c/battle_probe.py create mode 100644 emulator/firmware-decomp/emu860c/render_battle.py diff --git a/emulator/firmware-decomp/emu860c/battle_probe.py b/emulator/firmware-decomp/emu860c/battle_probe.py new file mode 100644 index 00000000..15b37297 --- /dev/null +++ b/emulator/firmware-decomp/emu860c/battle_probe.py @@ -0,0 +1,43 @@ +"""Dump one mid-scene battle draw's effect program for honest inspection.""" +import sys, os, struct, pickle +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE); sys.path.insert(0, os.path.dirname(HERE)) +sys.path.insert(0, r'C:\VWE\TeslaRel410\dpl3-revive\patha') +import emu860, emu_main, emu860c +from driver import boot, CpuShim +from vrboard import A +ANAME = {int(a): a.name for a in A} +emu860.Mem.log = lambda self, *a, **k: None +TARGET = int(sys.argv[2]) if len(sys.argv) > 2 else 6234 + +data = open(sys.argv[1], 'rb').read() +recs=[]; off=0 +while True: + i=data.find(b'VPXM',off) + if i<0: break + ln=struct.unpack_from('=4: + a=struct.unpack_from('=TARGET-1: + # run this draw fully, capturing the program region after + h(shim) + while True: + reason,_=emu860c.run(500_000_000) + if reason==3: continue + if emu860c.getstate()['pc']==RECV: break + hh=r.hooks.get(emu860c.getstate()['pc']) + if hh: hh(shim) + else: break + break + if h: h(shim) +prog={a:emu860c.r32(a) for a in range(0x08158000,0x08170000,4) if emu860c.r32(a)} +pickle.dump({'prog':prog,'qi':r.qi}, open(os.path.join(HERE,'battle_prog.pkl'),'wb')) +print("dumped %d prog cells at cmd %d"%(len(prog), r.qi)) diff --git a/emulator/firmware-decomp/emu860c/render_battle.py b/emulator/firmware-decomp/emu860c/render_battle.py new file mode 100644 index 00000000..94ec6d16 --- /dev/null +++ b/emulator/firmware-decomp/emu860c/render_battle.py @@ -0,0 +1,90 @@ +"""Honest battle geometry: 0x2c/0x42/0x0d edges as proper half-planes with +polygon-level winding (inside = all>=0 OR all<0), real z-buffer. The scene is +TEXTURED (texz/texu/texv planes, no Gouraud colour), so with no TXDN texels we +show STRUCTURE: flat distinct colour per polygon + depth + u/v. numpy, offline.""" +import pickle, struct, os +import numpy as np +from PIL import Image +HERE = os.path.dirname(os.path.abspath(__file__)) +d = pickle.load(open(os.path.join(HERE, 'battle_prog.pkl'), 'rb')) +prog = d['prog'] +def f32(w): return struct.unpack('> 8) & 0xff) in EDGE and len(edges) < 6: + edges.append((f32(rd(p+4)), f32(rd(p+8)), f32(rd(p+12)))) + p += 16 + if len(edges) < 3: + continue + vsum_pos = np.ones((H, W), bool) + vsum_neg = np.ones((H, W), bool) + for A, B, C in edges: + v = A * xx + B * yy + C + vsum_pos &= v >= 0 + vsum_neg &= v < 0 + en = vsum_pos | vsum_neg + if not en.any(): + continue + # z = the op-0x21 within the next ~10 words + zp = None; zq = p + for _ in range(12): + if ((rd(zq) >> 8) & 0xff) == 0x21: + zp = (f32(rd(zq+4)), f32(rd(zq+8)), f32(rd(zq+12))); break + zq += 4 + if zp is None: + continue + z = zp[0] * xx + zp[1] * yy + zp[2] + en &= z > zbuf + if not en.any(): + continue + zbuf[en] = z[en] + flat[en] = PAL[drawn % len(PAL)] + # texu/texv planes (addr 58 / 78 via op 0x43) for the uv view + up = vp = None; q = zq + for _ in range(50): + w = rd(q) + if ((w >> 8) & 0xff) == 0x43 and (w & 0xff) == 58 and up is None: + up = (f32(rd(q+4)), f32(rd(q+8)), f32(rd(q+12))) + if ((w >> 8) & 0xff) == 0x43 and (w & 0xff) == 78 and vp is None: + vp = (f32(rd(q+4)), f32(rd(q+8)), f32(rd(q+12))) + if up and vp: break + q += 4 + if up and vp: + u = up[0]*xx + up[1]*yy + up[2] + v = vp[0]*xx + vp[1]*yy + vp[2] + uv[..., 0][en] = u[en]; uv[..., 1][en] = v[en] + drawn += 1 + +print("drawn %d polygons of %d setups; covered %d px" % + (drawn, len(setups), int((zbuf > -1e29).sum()))) +Image.fromarray(flat, 'RGB').save(os.path.join(HERE, 'battle_flat.png')) +zv = zbuf.copy(); m = zv > -1e29 +zi = np.zeros((H, W), np.uint8) +if m.any(): + zn = (zv - zv[m].min()) / max(1e-9, np.ptp(zv[m])) + zi[m] = (40 + 215*zn[m]).astype(np.uint8) +Image.fromarray(zi, 'L').save(os.path.join(HERE, 'battle_depth.png')) +# uv normalized globally (spatially coherent, unlike per-prim) +um, vm = uv[..., 0], uv[..., 1] +rgb = np.zeros((H, W, 3), np.uint8) +if m.any(): + un = (um - um[m].min())/max(1e-9, np.ptp(um[m])) + vn = (vm - vm[m].min())/max(1e-9, np.ptp(vm[m])) + rgb[..., 0][m] = (40 + 200*un[m]).astype(np.uint8) + rgb[..., 1][m] = (40 + 200*vn[m]).astype(np.uint8) + rgb[..., 2][m] = 120 +Image.fromarray(rgb, 'RGB').save(os.path.join(HERE, 'battle_uv.png')) +print("wrote battle_flat.png + battle_depth.png + battle_uv.png")