From cc7c95d7388a865fcd3d9c62a573e1a6e9c7b829 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 19 Jul 2026 09:47:21 -0500 Subject: [PATCH] ACCEPTANCE 2: fxtest content renders -- perspective ground + textured boxes The per-draw effect programs (0x816f000, second DMA emitter) parse and execute through the named ISA: 9 primitives, z-buffered perspective scene with u/v texture gradients. The earlier 'fixed per-tile program' conclusion was a sniffer artifact. Co-Authored-By: Claude Opus 4.8 --- .../IGC-ENCODING-DERIVATION.md | 13 +++ emulator/firmware-decomp/render_fx.py | 81 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 emulator/firmware-decomp/render_fx.py diff --git a/emulator/firmware-decomp/IGC-ENCODING-DERIVATION.md b/emulator/firmware-decomp/IGC-ENCODING-DERIVATION.md index 3c251bf3..75fb2996 100644 --- a/emulator/firmware-decomp/IGC-ENCODING-DERIVATION.md +++ b/emulator/firmware-decomp/IGC-ENCODING-DERIVATION.md @@ -322,3 +322,16 @@ Remaining to full purity: name the texz seed op + extract the real ramp table. the emulator (the dn* unpack programs then define the texel wire format). - Registry census at cmd 13000: 1186 live objects (405 type-9 + 405 type-10 pairs = effect+texmap, 159 type-11, 48 type-8, ...). + +## ACCEPTANCE 2: fxtest CONTENT RENDERS (2026-07-19 ~10:30) +The draws build FRESH float-coefficient programs at 0x816f000 (emitter +0xf0400ebc — a second DMA writer; the earlier "4 fixed payloads" was a sniffer +artifact) + transformed vertex buffers (0x2c7xxxxx+, 80B records). Packet +shape per primitive: {19w setup: SETENABS + op-0x2c edges + MEMltTREE z + +z-write} + {80w x4-MIP ladder: TREEintoMEM_L3(texz/texu/texv) x5 rungs, with +FOOTER.SS's Cturn_z_to_tex_by_4 constant verbatim}. render_fx.py executes +them: 9 primitives -> a PERSPECTIVE GROUND PLANE + TEXTURED 3D BOXES (the +classic VWE building scene) with correct z-occlusion; u/v gradients shown as +false colour (fx_render/fx_depth.png in the scratchpad). Remaining polish: +op-0x2c edge exact semantics (treated as winding-agnostic edge) + real texel +sampling via the TXDN stream. diff --git a/emulator/firmware-decomp/render_fx.py b/emulator/firmware-decomp/render_fx.py new file mode 100644 index 00000000..40fc79b5 --- /dev/null +++ b/emulator/firmware-decomp/render_fx.py @@ -0,0 +1,81 @@ +"""Render fxtest's per-draw effect primitives from fx_program.pkl: per pair +{19w setup: edges+z, 80w ladder: texz/u/v planes}, rasterize with z-buffer and +visualize (u,v) as colour + z as depth. Structure render (no texel sampling).""" +import pickle, os, struct +import numpy as np +from PIL import Image +S = r'C:\Users\cyd\AppData\Local\Temp\claude\c--VWE-TeslaRel410\4e848c76-6e89-4034-8047-d8d491cb32d8\scratchpad' +d = pickle.load(open(os.path.join(S, 'fx_program.pkl'), 'rb')) +prog = d['prog'] +def f32(w): return struct.unpack('> 8) & 0xff) == 0x2c: + setups.append(a) +print("%d effect primitives" % len(setups)) + +W, H = 832, 512 +yy, xx = np.mgrid[0:H, 0:W].astype(np.float64) +zbuf = np.full((H, W), -1e30) +rgb = np.zeros((H, W, 3)) +drawn = 0 +for a in setups: + # edges: 4-word {hdr(0x2c), A, B, C} groups until a non-0x2c header + en_p = np.ones((H, W), bool); en_n = np.ones((H, W), bool) + p = a + 4 + nedges = 0 + while ((rd(p) >> 8) & 0xff) == 0x2c and nedges < 6: + A, B, C = f32(rd(p+4)), f32(rd(p+8)), f32(rd(p+12)) + v = A * xx + B * yy + C + en_p &= v >= 0; en_n &= v < 0 + p += 16; nedges += 1 + en = en_p | en_n + if not en.any(): + continue + # scan forward for the z instr (op 0x21) then the ladder (first texu/texv L3) + zp = None; up = None; vp = None + q = p + for _ in range(40): + w = rd(q); op = (w >> 8) & 0xff; ad = w & 0xff + if op == 0x21 and zp is None: + zp = (f32(rd(q+4)), f32(rd(q+8)), f32(rd(q+12))) + if op == 0x43 and ad == 58 and up is None: + up = (f32(rd(q+4)), f32(rd(q+8)), f32(rd(q+12))) + if op == 0x43 and ad == 78 and vp is None: + vp = (f32(rd(q+4)), f32(rd(q+8)), f32(rd(q+12))) + if up and vp and zp: + break + q += 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] + if up and vp: + u = up[0] * xx + up[1] * yy + up[2] + v = vp[0] * xx + vp[1] * yy + vp[2] + # u/v as colour: hue-ish mapping, normalized per prim + un = np.clip((u - u[en].min()) / max(1e-9, np.ptp(u[en])), 0, 1) + vn = np.clip((v - v[en].min()) / max(1e-9, np.ptp(v[en])), 0, 1) + rgb[..., 0][en] = 60 + 180 * un[en] + rgb[..., 1][en] = 40 + 160 * vn[en] + rgb[..., 2][en] = 220 - 140 * un[en] + else: + rgb[..., 0][en] = 200; rgb[..., 1][en] = 200; rgb[..., 2][en] = 200 + drawn += 1 +print("drawn %d" % drawn) +img = np.clip(rgb, 0, 255).astype(np.uint8) +Image.fromarray(img, 'RGB').save(os.path.join(S, 'fx_render.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, (zv[m].max() - zv[m].min())) + zi[m] = (40 + 215 * zn[m]).astype(np.uint8) +Image.fromarray(zi, 'L').save(os.path.join(S, 'fx_depth.png')) +print("wrote fx_render.png + fx_depth.png")