Files
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

88 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""retroDE_ps2 — Ch349 GS local-memory model (the bridge Codex framed).
A faithful byte-level model of GS local memory (4 MiB VRAM) so a draw's SAMPLED texture can be reconstructed
as the real hardware sees it — even when the asset is STREAMED in one PSM (e.g. PSMCT32, fast word writes)
and SAMPLED in another (e.g. PSMT8 indexed). Both operations address the SAME physical bytes through their
respective GS swizzles; modeling that crossover is the whole point.
Swizzle math is ported verbatim from the project's own RTL (which is in turn locked to PCSX2 GSTables.cpp):
- PSMCT32 write/read <- rtl/gif_gs/gs_swizzle_psmct32_stub.sv (block grid + byte_in_block = yb*32+xb*4)
- PSMT8 read <- rtl/gif_gs/gs_swizzle_psmt8_stub.sv (block grid + 16x16 columnTable8)
Address convention matches the codebase: VRAM byte base = PTR*256 (TBP0/DBP/CBP are 14-bit, *256 == 4 MiB),
and PTRs are page-aligned (multiple of 32) so page_index*8192 composes correctly off that base.
"""
# block grid is shared by PSMCT32 and PSMT8 (4 rows x 8 cols), value = block index within page
BLOCK = [
[ 0, 1, 4, 5,16,17,20,21],
[ 2, 3, 6, 7,18,19,22,23],
[ 8, 9,12,13,24,25,28,29],
[10,11,14,15,26,27,30,31],
]
# PSMT8 within-block 16x16 -> byte permutation (columnTable8)
COL8 = [
[ 0, 4, 16, 20, 32, 36, 48, 52, 2, 6, 18, 22, 34, 38, 50, 54],
[ 8, 12, 24, 28, 40, 44, 56, 60, 10, 14, 26, 30, 42, 46, 58, 62],
[ 33, 37, 49, 53, 1, 5, 17, 21, 35, 39, 51, 55, 3, 7, 19, 23],
[ 41, 45, 57, 61, 9, 13, 25, 29, 43, 47, 59, 63, 11, 15, 27, 31],
[ 96,100,112,116, 64, 68, 80, 84, 98,102,114,118, 66, 70, 82, 86],
[104,108,120,124, 72, 76, 88, 92,106,110,122,126, 74, 78, 90, 94],
[ 65, 69, 81, 85, 97,101,113,117, 67, 71, 83, 87, 99,103,115,119],
[ 73, 77, 89, 93,105,109,121,125, 75, 79, 91, 95,107,111,123,127],
[128,132,144,148,160,164,176,180,130,134,146,150,162,166,178,182],
[136,140,152,156,168,172,184,188,138,142,154,158,170,174,186,190],
[161,165,177,181,129,133,145,149,163,167,179,183,131,135,147,151],
[169,173,185,189,137,141,153,157,171,175,187,191,139,143,155,159],
[224,228,240,244,192,196,208,212,226,230,242,246,194,198,210,214],
[232,236,248,252,200,204,216,220,234,238,250,254,202,206,218,222],
[193,197,209,213,225,229,241,245,195,199,211,215,227,231,243,247],
[201,205,217,221,233,237,249,253,203,207,219,223,235,239,251,255],
]
def ct32_addr(dbp, dbw, x, y):
"""Byte address of PSMCT32 pixel (x,y) in a buffer based at dbp (256-byte units), width dbw (64px units)."""
page_index = (y >> 5) * dbw + (x >> 6)
block_idx = BLOCK[(y >> 3) & 3][(x >> 3) & 7]
return dbp*256 + page_index*8192 + block_idx*256 + (y & 7)*32 + (x & 7)*4
def psmt8_addr(tbp, fbw, x, y):
"""Byte address of PSMT8 pixel (x,y) in a buffer based at tbp (256-byte units), width fbw (64px units)."""
page_index = (y >> 6) * (fbw >> 1) + (x >> 7)
block_idx = BLOCK[(y >> 4) & 3][(x >> 4) & 7]
return tbp*256 + page_index*8192 + block_idx*256 + COL8[y & 15][x & 15]
class LocalMem:
"""4 MiB GS VRAM. Seed from the dump's initial snapshot, then replay host->local uploads in order."""
SIZE = 0x400000
def __init__(self, init_bytes=None):
if init_bytes is not None and len(init_bytes) >= self.SIZE:
self.m = bytearray(init_bytes[:self.SIZE])
else:
self.m = bytearray(self.SIZE)
def write_image_ct32(self, dbp, dbw, dsax, dsay, w, h, words):
"""Host->local upload in PSMCT32: raster-order words fill (dsax..+w)x(dsay..+h) via ct32 swizzle.
`words` may be shorter than w*h (partial transfer); fill stops when exhausted (GS behaviour)."""
n = len(words); i = 0
for py in range(h):
for px in range(w):
if i >= n: return
a = ct32_addr(dbp, dbw, dsax+px, dsay+py)
if 0 <= a and a+4 <= self.SIZE:
self.m[a:a+4] = (words[i] & 0xFFFFFFFF).to_bytes(4, "little")
i += 1
def read_psmt8(self, tbp, fbw, tw, th):
out = bytearray(tw*th)
for y in range(th):
r = y*tw
for x in range(tw):
a = psmt8_addr(tbp, fbw, x, y)
out[r+x] = self.m[a] if 0 <= a < self.SIZE else 0
return out
def read_ct32_word(self, dbp, dbw, x, y):
a = ct32_addr(dbp, dbw, x, y)
return int.from_bytes(self.m[a:a+4], "little") if a+4 <= self.SIZE else 0