Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)
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>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""retroDE_ps2 — Ch349 step 4 (definitive): composite a WHOLE SH3 frame from GS local memory.
|
||||
|
||||
Per-draw reconstruction (gs_sh3_draw_ref) proves one surface; this composites EVERY textured draw of a frame
|
||||
to pixel-check the reconstruction against the real PCSX2 screenshot — the strongest faithfulness test. It walks
|
||||
draws in capture order, keeping a GS local-memory model live (replaying host->local uploads as their idx is
|
||||
passed, so each draw samples the texture state IT saw, not a stale end-of-frame one), reconstructs+caches each
|
||||
bound texture (PSMT8 via grid-CSM1 CLUT, or PSMCT32 direct), and rasterizes perspective-correct with a z-buffer.
|
||||
|
||||
Output: composited frame PNG + a side-by-side vs the screenshot + a coverage/color summary. SH3-derived ->
|
||||
LOCAL/gitignored.
|
||||
Usage: gs_sh3_frame_ref.py <dump.gs.zst> [--frame F] [--shot path.png] [--max-draws N] [--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
|
||||
import gs_localmem as LM
|
||||
import gs_texture_residency as R
|
||||
|
||||
SW, SH = 640, 480
|
||||
|
||||
def edge(ax, ay, bx, by, px, py):
|
||||
return (px-ax)*(by-ay) - (py-ay)*(bx-ax)
|
||||
|
||||
def reconstruct_texture(mem, t0):
|
||||
"""Return (idx_or_words, pal_or_None, kind) for the bound texture. PSMT8 -> (idx, pal,'i8'); PSMCT32 ->
|
||||
(words, None,'ct32')."""
|
||||
tbp, psm, tbw, tw, th = t0["tbp"], t0["psm"], t0["tbw"], t0["tw"], t0["th"]
|
||||
if psm == 0x13: # PSMT8 indexed
|
||||
idx = mem.read_psmt8(tbp, tbw, tw, th)
|
||||
pal = RC.read_clut32(mem, t0["cbp"], order="grid")
|
||||
return idx, pal, "i8"
|
||||
if psm == 0x00: # PSMCT32 direct
|
||||
words = [mem.read_ct32_word(tbp, tbw, x, y) for y in range(th) for x in range(tw)]
|
||||
return words, None, "ct32"
|
||||
return None, None, None
|
||||
|
||||
def sample(tex, pal, kind, tw, th, u, v):
|
||||
tx = int(u) % tw; ty = int(v) % th
|
||||
if tx < 0: tx += tw
|
||||
if ty < 0: ty += th
|
||||
if kind == "i8":
|
||||
p = pal[tex[ty*tw+tx] & 0xFF]
|
||||
else:
|
||||
p = tex[ty*tw+tx]
|
||||
return (p&0xFF, (p>>8)&0xFF, (p>>16)&0xFF)
|
||||
|
||||
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
|
||||
frame = int(opt("--frame","1")); maxd = int(opt("--max-draws","100000"))
|
||||
min_area = float(opt("--min-area","0"))
|
||||
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)
|
||||
|
||||
# all draws of the frame, in capture (idx) order, with full geometry (parse ONCE, reuse for census)
|
||||
collected = R.collect(dump, 0)
|
||||
d, h, events, uploads, runs, vram = collected
|
||||
draws, _, _ = C.census(dump, frame_filter=frame, min_prims=1, collected=collected)
|
||||
draws = [dr for dr in draws if dr["prim"]["tme"] and dr["tex0"]["psm"] in (0x13,0x00)
|
||||
and dr["onscreen_area"] >= min_area]
|
||||
draws.sort(key=lambda x: x["first_idx"])
|
||||
print(f"[frameref] frame {frame}: {len(draws)} textured (PSMT8/CT32) draws, min_area={min_area}; "
|
||||
f"replaying uploads incrementally")
|
||||
|
||||
mem = LM.LocalMem(vram)
|
||||
up_sorted = sorted([u for u in uploads if u["dpsm"]==0x00], key=lambda u: u["idx"])
|
||||
up_i = 0
|
||||
# epoch counter per tbp (bumped whenever we apply an upload to that dbp) -> texture cache key
|
||||
epoch = {}
|
||||
texcache = {}
|
||||
fb = [(0,0,0,0)]*(SW*SH); zb = [-1]*(SW*SH)
|
||||
|
||||
def apply_uploads_before(idx):
|
||||
nonlocal up_i
|
||||
while up_i < len(up_sorted) and up_sorted[up_i]["idx"] < idx:
|
||||
u = up_sorted[up_i]
|
||||
off, end = u["blob_range"]; blob = d[off:end]
|
||||
words = [int.from_bytes(blob[i*4:i*4+4],"little") for i in range(len(blob)//4)]
|
||||
mem.write_image_ct32(u["dbp"], u["dbw"], u["dx"], u["dy"], u["w"], u["h"], words)
|
||||
epoch[u["dbp"]] = epoch.get(u["dbp"],0)+1
|
||||
up_i += 1
|
||||
|
||||
painted_total = 0
|
||||
for n, dr in enumerate(draws[:maxd]):
|
||||
apply_uploads_before(dr["first_idx"])
|
||||
t0 = dr["tex0"]
|
||||
key = (t0["tbp"], t0["psm"], t0["tbw"], t0["tw"], t0["th"], t0.get("cbp"),
|
||||
epoch.get(t0["tbp"],0), epoch.get(t0.get("cbp"),0))
|
||||
if key not in texcache:
|
||||
texcache[key] = reconstruct_texture(mem, t0)
|
||||
tex, pal, kind = texcache[key]
|
||||
if kind is None: continue
|
||||
tw, th = t0["tw"], t0["th"]
|
||||
verts = dr["verts"]; pt = dr["prim"]["type"]
|
||||
def tri(i0,i1,i2):
|
||||
nonlocal painted_total
|
||||
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)
|
||||
if maxx<minx or maxy<miny: return
|
||||
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):
|
||||
base=py*SW
|
||||
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)
|
||||
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
|
||||
Q=b0*v0["q"]+b1*v1["q"]+b2*v2["q"]
|
||||
if abs(Q)<1e-12: continue
|
||||
S=b0*v0["s"]+b1*v1["s"]+b2*v2["s"]; T=b0*v0["t"]+b1*v1["t"]+b2*v2["t"]
|
||||
u=(S/Q)*tw; vv=(T/Q)*th
|
||||
z=int(b0*v0["z"]+b1*v1["z"]+b2*v2["z"])
|
||||
o=base+px
|
||||
if z>=zb[o]:
|
||||
zb[o]=z; fb[o]=sample(tex,pal,kind,tw,th,u,vv)+(255,); painted_total+=1
|
||||
if pt==4:
|
||||
for i in range(2,len(verts)): tri(i-2,i-1,i)
|
||||
elif pt==5:
|
||||
for i in range(2,len(verts)): tri(0,i-1,i)
|
||||
elif pt==3:
|
||||
for i in range(0,len(verts)-2,3): tri(i,i+1,i+2)
|
||||
if (n+1)%200==0: print(f" ...{n+1}/{len(draws)} draws, {painted_total} px, {len(texcache)} tex cached")
|
||||
|
||||
cov = sum(1 for p in fb if p[3])
|
||||
RC.save_png(os.path.join(outdir, f"frame{frame}_composite.png"), SW, SH, fb)
|
||||
print(f"[frameref] composite: {cov}/{SW*SH} px painted ({100*cov/(SW*SH):.1f}%), {len(texcache)} textures")
|
||||
# side-by-side vs screenshot
|
||||
shot = opt("--shot")
|
||||
if shot and os.path.exists(shot):
|
||||
from PIL import Image
|
||||
comp = Image.open(os.path.join(outdir, f"frame{frame}_composite.png")).convert("RGB")
|
||||
gt = Image.open(shot).convert("RGB").resize((SW,SH))
|
||||
sbs = Image.new("RGB",(SW*2+8,SH),(40,40,40))
|
||||
sbs.paste(comp,(0,0)); sbs.paste(gt,(SW+8,0))
|
||||
sbs.save(os.path.join(outdir, f"frame{frame}_vs_screenshot.png"))
|
||||
print(f"[frameref] wrote frame{frame}_vs_screenshot.png (left=recon, right=PCSX2)")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Reference in New Issue
Block a user