Files
TeslaRel410/emulator/firmware-decomp/emu860c/exp_precision.py
T
CydandClaude Opus 4.8 b688e0e4bd M4c-raster: GPU render BIT-IDENTICAL to the CPU reference, ~50x faster
m4b_gpu.py moves the per-draw rasterization to a GPU compute shader (nearest-z
winner per pixel) with the verified M5 texel decode as a vectorized numpy
post-pass. Output matches the M5-verified CPU render (frame_*.png) bit-for-bit
across all 12 frames (differ>24 = 0.000%), at 1.4s/12 frames vs 69.9s (~50x;
the firmware's 3.7s now dominates -- the render keeps up with real-time).

Reaching bit-identity took finding two real bugs (honest trail in M4B-RESULTS.md):
  1. 4-edge clip: the shader tested only edges[:4] while the CPU clips with ALL
     edges (up to 6); 5-6-edge polys bled past their boundary and overwrote
     neighbours -- the 18% region divergence on the receding walls/floor. Fixed
     with 6 edge slots.
  2. float32 planes: A*x+B*y+C in float32 flipped the z-test winner vs the CPU's
     float64 at contested depths. Fixed with fp64 in the shader.

exp_precision.py is the diagnostic that REJECTED int-truncation as a reconciler
(it worsens float32/64 sensitivity to ~60%; true fixed-point width is a separate
spec item). Verified honestly by measuring, not eyeballing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 09:14:18 -05:00

146 lines
5.7 KiB
Python

"""Diagnostic: is the CPU/GPU 18% divergence float32-vs-float64 in the plane
eval, and does the authentic int-truncation (golden model stores int(Ax+By+C))
reconcile them? Renders frame 11's prims three ways and compares.
A = float64 planes, no truncation (== current render_faithful / CPU ref)
B = float64 planes, int-truncated (authentic: int(Ax+By+C) fields)
C = float32 planes, int-truncated (authentic + GPU precision)
If B ~= C, int-truncation removes the float32/64 sensitivity -> the fix.
"""
import sys, os, struct
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
import numpy as np
from driver import boot, CpuShim
from vrboard import A
from texstore import build_texstore
from dpl_sampler import mode_to_texflags, wrap_index, composite
ANAME = {int(a): a.name for a in A}
emu860.Mem.log = lambda self, *a, **k: None
SRC = r'C:/VWE/TeslaRel410/emulator/render-bridge/captures/netdeath-20260708.fifodump'
PROG_LO, PROG_HI = 0x08158000, 0x08170000
W, H = 832, 512
EDGE = {0x42, 0x0d, 0x2c}
TARGET = 11
data = open(SRC, 'rb').read()
recs = []; off = 0
while True:
i = data.find(b'VPXM', off)
if i < 0:
break
ln = struct.unpack_from('<I', data, i + 4)[0]
body = data[i + 8:i + 8 + ln]; off = i + 8 + ln
if len(body) >= 4 and struct.unpack_from('<I', body, 0)[0] < 0x100:
recs.append((struct.unpack_from('<I', body, 0)[0], body[4:]))
store = build_texstore(recs)
texlist = list(store.items())
r = boot(fw='vrend410', queue=recs)
shim = CpuShim()
RECV = emu_main.MAPS['vrend410']['receive']
yy64, xx64 = np.mgrid[0:H, 0:W].astype(np.float64)
yy32, xx32 = xx64.astype(np.float32), yy64.astype(np.float32)
def f32(w):
return struct.unpack('<f', struct.pack('<I', w & 0xffffffff))[0]
def capture_polys():
prog = {a: emu860c.r32(a) for a in range(PROG_LO, PROG_HI, 4) if emu860c.r32(a)}
rd = lambda a: prog.get(a, 0)
setups = [a for a in sorted(prog) if rd(a) == 0x100]
polys = []
for a in setups:
p = a + 4; edges = []
while ((rd(p) >> 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
zp = tzp = up = vp = None; tid = None; q = p
for _ in range(70):
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 == 32 and tzp is None: tzp = (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 op == 0xf7 and ad in (141, 142) and tid is None: tid = (rd(q+4) >> 2) & 0x3f
q += 4
if zp is None:
continue
polys.append((edges, zp, tzp, up, vp, tid))
return polys
def render(polys, xx, yy, trunc):
zbuf = np.full((H, W), -1e30)
img = np.zeros((H, W, 3), np.uint8)
for edges, zp, tzp, up, vp, tid in polys:
ep = np.ones((H, W), bool); en = np.ones((H, W), bool)
for Ae, Be, Ce in edges:
v = Ae*xx + Be*yy + Ce; ep &= v >= 0; en &= v < 0
inside = ep | en
if not inside.any():
continue
z = zp[0]*xx + zp[1]*yy + zp[2]
if trunc:
z = np.trunc(z)
inside &= z > zbuf
if not inside.any():
continue
if up and vp and tzp:
h, (tu, tv, mode, arr) = texlist[(tid or 0) % len(texlist)]
wu, wv, cm = mode_to_texflags(mode)
tzv = tzp[0]*xx + tzp[1]*yy + tzp[2]
uu = up[0]*xx + up[1]*yy + up[2]
vv = vp[0]*xx + vp[1]*yy + vp[2]
if trunc:
tzv = np.trunc(tzv); uu = np.trunc(uu); vv = np.trunc(vv)
tzv = np.where(np.abs(tzv) < 1.0, np.sign(tzv) + (tzv == 0), tzv)
uc = (uu.astype(np.float64)/tzv*2048).astype(np.int64)
vc = (vv.astype(np.float64)/tzv*2048).astype(np.int64)
ui = wrap_index((uc*tu) >> 8, tu, wu); vi = wrap_index((vc*tv) >> 8, tv, wv)
dm = composite(img, inside, arr[vi, ui], cm)
else:
img[inside] = (60, 60, 70); dm = inside
zbuf[dm] = np.asarray(z, np.float64)[dm]
return img
frames = 0; prev = False
while r.qi < len(r.queue):
reason, _ = emu860c.run(500_000_000)
if reason == 3:
continue
if reason != 0:
break
pc = emu860c.getstate()['pc']
h = r.hooks.get(pc)
if h is None:
break
if pc == RECV:
if prev:
if frames == TARGET:
polys = capture_polys()
print("frame %d: %d polys" % (frames, len(polys)), flush=True)
A_ = render(polys, xx64, yy64, False)
B_ = render(polys, xx64, yy64, True)
C_ = render(polys, xx32, yy32, True)
def cmp(p, q, na, nb):
d = np.abs(p.astype(int) - q.astype(int)).sum(2)
print(" %s vs %s: differ>24 %.2f%% mean|d| %.2f"
% (na, nb, 100*(d > 24).mean(), d.mean()), flush=True)
cmp(A_, B_, "float64-raw(A)", "float64-trunc(B)")
cmp(B_, C_, "float64-trunc(B)", "float32-trunc(C)")
cmp(A_, C_, "float64-raw(A)", "float32-trunc(C)")
break
frames += 1
prev = False
if r.qi < len(r.queue) and ANAME.get(r.queue[r.qi][0]) == 'draw_scene':
prev = True
if h(shim) == 'done':
break