ec82764bef
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression (272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps, and all dump-derived textures/traces) is excluded via .gitignore and stays local. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
114 lines
5.8 KiB
Python
114 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
||
"""retroDE_ps2 — Ch349 step 4: software reference image for an ACTUAL SH3 draw.
|
||
|
||
Rasterizes the chosen draw's REAL geometry (its TRI_STRIP, real per-vertex screen XY + perspective S/T/Q +
|
||
vertex RGBA) sampling the texture RECONSTRUCTED from GS local memory (gs_sh3_recon / gs_localmem). Perspective-
|
||
correct: S,T,Q (= s/w, t/w, 1/w) interpolate linearly in screen space, then u=(S/Q)*TW, v=(T/Q)*TH; texel ->
|
||
CLUT -> ABGR; TFX=MODULATE applies the vertex color (×2/255-ish, GS 128=1.0). This is the host-first reference
|
||
that must pixel-check against the real PCSX2 frame BEFORE anything goes to feeder/board.
|
||
|
||
Usage: gs_sh3_draw_ref.py <dump.gs.zst> [--draw-idx N] [--tbp T --cbp C --tbw W --tw 512 --th 512]
|
||
[--clut-order grid|linear] [--modulate 0|1] [--out DIR]
|
||
"""
|
||
import sys, os
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
import gs_sh3_draw_census as C
|
||
import gs_sh3_recon as RC
|
||
|
||
SW, SH = 640, 480
|
||
|
||
def edge(ax, ay, bx, by, px, py):
|
||
return (px-ax)*(by-ay) - (py-ay)*(bx-ax)
|
||
|
||
def raster_strip(verts, prim_type, tex_idx, pal, tw, th, modulate):
|
||
"""Rasterize a TRI_STRIP/TRI_FAN/TRIANGLE list of verts into an SW*SH RGBA buffer with a z-buffer."""
|
||
fb = [(0,0,0,0)]*(SW*SH)
|
||
zb = [-1]*(SW*SH)
|
||
def tri(i0, i1, i2):
|
||
v0, v1, v2 = verts[i0], verts[i1], verts[i2]
|
||
x0,y0 = v0["x"],v0["y"]; x1,y1 = v1["x"],v1["y"]; x2,y2 = v2["x"],v2["y"]
|
||
minx = max(0, int(min(x0,x1,x2))); maxx = min(SW-1, int(max(x0,x1,x2))+1)
|
||
miny = max(0, int(min(y0,y1,y2))); maxy = min(SH-1, int(max(y0,y1,y2))+1)
|
||
area = edge(x0,y0, x1,y1, x2,y2)
|
||
if abs(area) < 1e-6: return
|
||
inv = 1.0/area
|
||
for py in range(miny, maxy+1):
|
||
for px in range(minx, maxx+1):
|
||
cx, cy = px+0.5, py+0.5
|
||
w0 = edge(x1,y1, x2,y2, cx,cy)
|
||
w1 = edge(x2,y2, x0,y0, cx,cy)
|
||
w2 = edge(x0,y0, x1,y1, cx,cy)
|
||
# inside test (either winding)
|
||
if not ((w0>=0 and w1>=0 and w2>=0) or (w0<=0 and w1<=0 and w2<=0)):
|
||
continue
|
||
b0, b1, b2 = w0*inv, w1*inv, w2*inv
|
||
S = b0*v0["s"] + b1*v1["s"] + b2*v2["s"]
|
||
T = b0*v0["t"] + b1*v1["t"] + b2*v2["t"]
|
||
Q = b0*v0["q"] + b1*v1["q"] + b2*v2["q"]
|
||
if abs(Q) < 1e-12: continue
|
||
u = (S/Q)*tw; v = (T/Q)*th
|
||
tx = int(u) % tw; ty = int(v) % th # REPEAT wrap (SH3 CLAMP default)
|
||
if tx < 0: tx += tw
|
||
if ty < 0: ty += th
|
||
pidx = tex_idx[ty*tw + tx]
|
||
p = pal[pidx & 0xFF]
|
||
r,g,b = p&0xFF, (p>>8)&0xFF, (p>>16)&0xFF
|
||
if modulate:
|
||
vr = b0*((v0["rgba"])&0xFF)+b1*((v1["rgba"])&0xFF)+b2*((v2["rgba"])&0xFF)
|
||
vg = b0*((v0["rgba"]>>8)&0xFF)+b1*((v1["rgba"]>>8)&0xFF)+b2*((v2["rgba"]>>8)&0xFF)
|
||
vb = b0*((v0["rgba"]>>16)&0xFF)+b1*((v1["rgba"]>>16)&0xFF)+b2*((v2["rgba"]>>16)&0xFF)
|
||
r = min(255, int(r*vr/128.0)); g = min(255, int(g*vg/128.0)); b = min(255, int(b*vb/128.0))
|
||
z = int(b0*v0["z"] + b1*v1["z"] + b2*v2["z"])
|
||
o = py*SW + px
|
||
if z >= zb[o]:
|
||
zb[o] = z; fb[o] = (r,g,b,255)
|
||
n = len(verts)
|
||
if prim_type == 3: # TRIANGLE list
|
||
for i in range(0, n-2, 3): tri(i,i+1,i+2)
|
||
elif prim_type == 4: # TRI_STRIP
|
||
for i in range(2, n): tri(i-2, i-1, i)
|
||
elif prim_type == 5: # TRI_FAN
|
||
for i in range(2, n): tri(0, i-1, i)
|
||
return fb
|
||
|
||
def main(argv):
|
||
if len(argv) < 2:
|
||
print(__doc__); return 2
|
||
dump = argv[1]
|
||
def opt(n, dv=None): return argv[argv.index(n)+1] if n in argv else dv
|
||
draw_idx = int(opt("--draw-idx","89761"))
|
||
tbp = int(opt("--tbp","9216")); cbp = int(opt("--cbp","13952"))
|
||
fbw = int(opt("--tbw","8")); tw = int(opt("--tw","512")); th = int(opt("--th","512"))
|
||
order = opt("--clut-order","grid"); modulate = int(opt("--modulate","1"))
|
||
outdir = opt("--out", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||
"captures","gs","silenthill3","extracted","recon"))
|
||
os.makedirs(outdir, exist_ok=True)
|
||
|
||
dr = C.get_draw(dump, draw_idx)
|
||
if dr is None:
|
||
print(f"draw first_idx={draw_idx} not found"); return 1
|
||
t0 = dr["tex0"]
|
||
print(f"[step4] draw f{dr['frame']} idx{draw_idx} {C.PRIMT[dr['prim']['type']]} nprim={dr['nprim']} "
|
||
f"nvert={dr['nvert']} TEX0 tbp={t0['tbp']} psm=0x{t0['psm']:02x} {t0['tw']}x{t0['th']}")
|
||
|
||
mem, replayed, uploads, events, vram = RC.build_localmem_to(dump, draw_idx)
|
||
if mem is None: print("VRAM absent"); return 1
|
||
tex_idx = mem.read_psmt8(tbp, fbw, tw, th)
|
||
pal = RC.read_clut32(mem, cbp, order=order)
|
||
print(f"[step4] reconstructed tex {tw}x{th} ({len(set(tex_idx))} idx), CLUT {order} ({len(set(pal))} colors)")
|
||
|
||
fb = raster_strip(dr["verts"], dr["prim"]["type"], tex_idx, pal, tw, th, modulate)
|
||
painted = sum(1 for p in fb if p[3])
|
||
RC.save_png(os.path.join(outdir, f"draw_ref_{draw_idx}_{order}.png"), SW, SH, fb)
|
||
print(f"[step4] painted {painted} px -> draw_ref_{draw_idx}_{order}.png ({outdir})")
|
||
# crop to the draw's on-screen bbox for easier visual compare with the PCSX2 frame
|
||
x0=max(0,int(dr['xmin'])); x1=min(SW,int(dr['xmax'])+1); y0=max(0,int(dr['ymin'])); y1=min(SH,int(dr['ymax'])+1)
|
||
if x1>x0 and y1>y0:
|
||
crop = [fb[y*SW+x] for y in range(y0,y1) for x in range(x0,x1)]
|
||
RC.save_png(os.path.join(outdir, f"draw_ref_{draw_idx}_{order}_crop.png"), x1-x0, y1-y0, crop)
|
||
print(f"[step4] bbox crop x[{x0}..{x1}] y[{y0}..{y1}] -> draw_ref_{draw_idx}_{order}_crop.png")
|
||
return 0
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main(sys.argv))
|