Files
retroDE_ps2/tools/gs_make_synthetic.py
thejayman77 ec82764bef 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>
2026-06-29 20:10:50 -04:00

86 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Author a small CONTENT-CLEAN synthetic PCSX2 .gs dump for Ch340 byte-exact parser tests.
No copyrighted content — every byte is hand-authored here. Exercises the container header + each
packet type + each GIF mode the parser must handle: PACKED A+D, PACKED PRE-prim, PACKED RGBAQ/XYZ2,
an IMAGE upload, and a VSync frame boundary. Writes captures/gs/synthetic/mini.gs (whitelisted).
"""
import struct, os
OUT = os.path.join(os.path.dirname(__file__), "..", "captures", "gs", "synthetic", "mini.gs")
def giftag(nloop, eop, flg, nreg, regs, pre=0, prim=0):
lo = (nloop & 0x7FFF) | ((eop & 1) << 15) | ((pre & 1) << 46) | ((prim & 0x7FF) << 47) \
| ((flg & 3) << 58) | ((nreg & 0xF) << 60)
return (lo | ((regs & ((1 << 64) - 1)) << 64)).to_bytes(16, "little")
def ad(addr, data): # PACKED A+D qword: data[63:0], addr[72:64]
return ((addr & 0xFF) << 64 | (data & ((1 << 64) - 1))).to_bytes(16, "little")
def packed_rgbaq(r, g, b, a): # R[7:0] G[39:32] B[71:64] A[103:96]
return (r | (g << 32) | (b << 64) | (a << 96)).to_bytes(16, "little")
def packed_xyz2(x, y, z, adc=0): # X[15:0] Y[47:32] Z[95:64] ADC[111]
return ((x & 0xFFFF) | ((y & 0xFFFF) << 32) | ((z & 0xFFFFFFFF) << 64) | (adc << 111)).to_bytes(16, "little")
def transfer(path, gifdata):
return bytes([0]) + bytes([path]) + struct.pack("<I", len(gifdata)) + gifdata
def vsync(field): return bytes([1, field])
# ---- GIF payloads ----
# P1: one A+D write to FRAME_1 (0x4C) = 0x000000000C001807 (an arbitrary recognizable value).
p1 = giftag(1, 0, 0, 1, 0xE) + ad(0x4C, 0x0000_0000_0C00_1807)
# P2: a TRIANGLE (prim type 3, IIP/TME bits) via PRE, PACKED, nreg=2 (RGBAQ=1, XYZ2=5), nloop=3.
prim_val = 3 | (1 << 4) | (1 << 6) # TRIANGLE + TME + ABE
regs2 = 0x1 | (0x5 << 4) # desc0=RGBAQ, desc1=XYZ2
verts = [(100, 50, 0x5000, 0xFF, 0x00, 0x00), (200, 50, 0x5000, 0x00, 0xFF, 0x00),
(100, 150, 0x5000, 0x00, 0x00, 0xFF)]
p2 = giftag(3, 1, 0, 2, regs2, pre=1, prim=prim_val)
for (x, y, z, r, g, b) in verts:
p2 += packed_rgbaq(r, g, b, 0x80) + packed_xyz2(x << 4, y << 4, z)
# P3: an IMAGE upload, 2 qwords of dummy texture bytes.
p3 = giftag(2, 1, 2, 0, 0) + bytes(32)
packets = transfer(3, p1) + transfer(3, p2) + transfer(3, p3) + vsync(0)
def build_container(serial, packets, state_size=16):
state = bytes(state_size); regs_block = bytes(8192)
hdr = struct.pack("<9I", 9, len(state), 36, len(serial), 0x12345678, 0, 0, 36 + len(serial), 0)
header_block = hdr + serial
return (struct.pack("<I", 0xFFFFFFFF) + struct.pack("<I", len(header_block)) + header_block
+ state + regs_block + packets)
def write(path, data):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f: f.write(data)
print(f"wrote {path} ({len(data)} bytes)")
# mini.gs — mixed fixture for the byte-exact PARSER test (P2's triangle is TME=1 = unsupported).
write(OUT, build_container(b"SYNTH001", packets))
# mini_supported.gs — two NON-textured opaque triangles for the TRANSLATOR test (TME=0, ABE=0, flat).
OUT2 = os.path.join(os.path.dirname(__file__), "..", "captures", "gs", "synthetic", "mini_supported.gs")
def flat_tri(prim_val, tris):
out = b""
for (vs, rgb) in tris: # vs = 3x(x,y,z) ; flat color rgb
out += giftag(3, 1, 0, 2, 0x1 | (0x5 << 4), pre=1, prim=prim_val)
for (x, y, z) in vs:
out += packed_rgbaq(rgb[0], rgb[1], rgb[2], 0x80) + packed_xyz2(x << 4, y << 4, z)
return out
sup = transfer(3, giftag(1, 0, 0, 1, 0xE) + ad(0x18, 0x0000_0000_0000_0000)) # XYOFFSET_1 = 0
sup += transfer(3, flat_tri(3, [ # PRIM=TRIANGLE only (TME=0,ABE=0)
([(10, 10, 0x6000), (50, 10, 0x6000), (10, 50, 0x6000)], (0xFF, 0x00, 0x00)), # red
([(50, 50, 0x5000), (14, 50, 0x5000), (50, 14, 0x5000)], (0x00, 0x00, 0xFF)), # blue
]))
sup += vsync(0)
write(OUT2, build_container(b"SYNTHSUP", sup))
# mini_st.gs — Ch342 regression: a PACKED ST write. The GS routes the ST qword's Q lane (bits [95:64])
# to RGBAQ.Q; the parser must expose it as info["q_stq"]. Guards the easy-to-reintroduce drop-Q bug.
OUT3 = os.path.join(os.path.dirname(__file__), "..", "captures", "gs", "synthetic", "mini_st.gs")
def packed_st(s, t, q): # PACKED ST: S[31:0], T[63:32], Q[95:64]
return (s | (t << 32) | (q << 64)).to_bytes(16, "little")
p_st = giftag(1, 1, 0, 1, 0x2) + packed_st(0x11111111, 0x22222222, 0x33333333) # descriptor 0x2 = ST
write(OUT3, build_container(b"SYNTHST0", transfer(3, p_st) + vsync(0)))