#!/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 [--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))