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

525 lines
30 KiB
C

// retroDE_ps2 — ps2_feeder (Ch339): native HPS userspace command producer for the GS feeder.
//
// Encodes structured drawing commands (triangle / native rectangle / flat or per-vertex color / Z)
// into the PROVEN staging format and streams them to the FPGA over the existing HPS bridge via
// /dev/mem + mmap — the SAME register protocol the docs/hardware/ps2_feeder_*.sh anchors use. The
// RTL and bridge protocol are UNCHANGED; this is purely a host-side encoder + streamer.
//
// The staging word layout and every GS-register packing here are a byte-exact port of the golden
// encoder in sim/data/top_psmct32_raster_demo/bake.py. `--dump <scene>` emits the 256 staging words
// so the encoder can be diffed against the golden feeder_*.mem fixtures (byte-equivalence gate).
//
// Build: gcc -O2 -Wall -o ps2_feeder ps2_feeder.c
// Usage:
// ps2_feeder --list list built-in named scenes
// ps2_feeder --dump <scene> print the 256 staging words (no board access)
// ps2_feeder --dump-file <file> parse a scene file, print the first scene's staging words
// ps2_feeder [opts] <scene>... stream each named scene to the board (submit/go/wait each)
// ps2_feeder [opts] -f <file> stream each scene in a text scene file
// Options:
// --base 0xADDR bridge base (default 0x40000000) --dry-run encode+validate, no board access
//
// Scene file grammar (one scene per `go`/EOF; '#' comments; whitespace-separated):
// tri x0 y0 x1 y1 x2 y2 z r g b flat triangle
// trig x0 y0 r0 g0 b0 x1 y1 r1 g1 b1 x2 y2 r2 g2 b2 z gouraud (per-vertex) triangle
// tritile T z r g b flat triangle filling grid tile T (0..15)
// rect T z r g b native rectangle in grid tile T
// go submit the accumulated scene; start next
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
// ---- profile constants (must match the GS_FEEDER_DEMO build) ----
#define FEEDER_STG_WORDS 256
#define FIFO_DEPTH 8
#define CAP_FBW 1
#define CAP_TBP1 96
#define TMP_TBW 1
static const int TMP_UV[3][2] = {{0,0},{3,0},{0,3}};
// ---- bridge register byte offsets from base (mirror docs/hardware/ps2_feeder_*.sh) ----
#define OFF_STATUS 0x0D8 // R: bit0 = feeder ready ; W: reset staging write address
#define OFF_LO 0x0DC // W: low 32 bits of the next staging word ; R: current staging address
#define OFF_HI 0x0E4 // W: high 32 bits (commits {hi,lo}, auto-increments addr) ; R: records emitted
#define OFF_GO 0x0E8 // W: bit0 = trigger feeder ; R: fifo wait cycles
// ======================== GS register / vertex packers (port of bake.py) ===================
static uint64_t frame_1_psmct32(uint32_t fbw){ return (uint64_t)(fbw & 0x3F) << 16; }
static uint64_t alpha_pack(uint32_t a,uint32_t b,uint32_t c,uint32_t d,uint32_t fix){
return (uint64_t)(a&3) | ((uint64_t)(b&3)<<2) | ((uint64_t)(c&3)<<4)
| ((uint64_t)(d&3)<<6) | ((uint64_t)(fix&0xFF)<<32);
}
static uint64_t test1_geq(void){ return (1ull<<16) | (2ull<<17); }
static uint64_t zbuf1_pack(uint32_t zbp,uint32_t zmsk,uint32_t psm){
return (uint64_t)(zbp&0x1FF) | ((uint64_t)(psm&0xF)<<24) | (uint64_t)(zmsk&1);
}
static uint64_t tex0_pack(uint32_t tbp0,uint32_t tbw,uint32_t psm,uint32_t tw,uint32_t th,uint32_t tfx){
return (uint64_t)(tbp0&0x3FFF) | ((uint64_t)(tbw&0x3F)<<14) | ((uint64_t)(psm&0x3F)<<20)
| ((uint64_t)(tw&0xF)<<26) | ((uint64_t)(th&0xF)<<30) | ((uint64_t)(tfx&3)<<35);
}
static uint64_t prim_tri_tme_abe(void){ return 3 | (1<<4) | (1<<6); } // TRI+TME+ABE (legacy scenes)
static uint64_t prim_tri_tme(void){ return 3 | (1<<4); } // Ch342 TRI+TME, ABE=0 (cube)
static uint64_t prim_sprite_tme_abe(void){ return 6 | (1<<4) | (1<<6); } // Ch345a SPRITE+TME+ABE (textured-alpha)
static uint64_t rgbaq_data(uint8_t r,uint8_t g,uint8_t b){
return ((uint64_t)0xFF<<24) | ((uint64_t)b<<16) | ((uint64_t)g<<8) | r; // a = 0xFF
}
static uint64_t uv_data(int ui,int vi){
return (uint64_t)(((ui<<4)&0x3FFF)) | ((uint64_t)(((vi<<4)&0x3FFF))<<14);
}
static uint64_t xyz2_dataz(int x,int y,uint32_t z){
return ((uint64_t)(x&0xFFF)<<4) | ((uint64_t)(y&0xFFF)<<20) | ((uint64_t)z<<32);
}
// Ch342 — perspective ST/Q packers matching the Ch301 gs_stub contract (24-bit fixed-point, FRAC=12).
// ST register: S_fp in [23:0], T_fp in [55:32]. RGBAQ register: Q_fp in [55:32] (NOT IEEE float).
static uint64_t st_data(uint32_t s_fp,uint32_t t_fp){
return (uint64_t)(s_fp & 0xFFFFFF) | ((uint64_t)(t_fp & 0xFFFFFF) << 32);
}
static uint64_t rgbaq_persp(uint8_t r,uint8_t g,uint8_t b,uint32_t q_fp){
return ((uint64_t)0xFF<<24) | ((uint64_t)b<<16) | ((uint64_t)g<<8) | r | ((uint64_t)(q_fp & 0xFFFFFF) << 32);
}
// ======================== scene model ===================
#define MAX_TRI 64
#define MAX_RECT 64
#define MAX_SPRITE 64
typedef struct { int x[3], y[3]; uint8_t r[3], g[3], b[3]; int u[3], v[3];
uint32_t s_fp[3], t_fp[3], q_fp[3]; uint32_t z; } tri_t; // Ch342 fixed-point ST/Q
typedef struct { int tile; uint8_t r, g, b; uint32_t z; } rect_t;
// Ch345a — textured + source-over alpha SPRITE record (the Ch344-proven subset): screen rect (x0,y0)-(x1,y1),
// per-corner affine UV, flat MODULATE tint. As (source alpha) comes from the TEXEL (TCC=1), not the tint.
typedef struct { int x0,y0,x1,y1; int u0,v0,u1,v1; uint8_t r,g,b; } sprite_t;
typedef struct {
tri_t tri[MAX_TRI]; int ntri; rect_t rect[MAX_RECT]; int nrect; const char *name;
sprite_t sprite[MAX_SPRITE]; int nsprite; // Ch345a
// Ch341 — optional scene-level texture binding. textured=0 keeps the proven unity+MODULATE header
// and TMP_UV (byte-exact with all prior scenes). textured=1 (set by the `tex0` command) overrides
// the scene TEX0 and lets `tritex` carry per-vertex real UV.
int textured; uint32_t tex_tbp, tex_tbw, tex_tw, tex_th, tex_tfx;
// Ch342 — perspective format: word0[32]=1, per-vertex RGBAQ/ST/XYZ2 (fixed-point S/T/Q). Requires
// `textured`; rects are rejected (the host fails closed so a mixed scene is never silently dropped).
int perspective;
// Ch345a — SPRITE format: word0[33]=1, each prim = SPRITE record (2 verts x RGBAQ/UV/XYZ2). Requires
// `textured`; mutually exclusive with tris/rects/perspective (fail closed).
int sprite_mode;
} scene_t;
static void scene_reset(scene_t *s, const char *name){ s->ntri=0; s->nrect=0; s->nsprite=0; s->name=name; s->textured=0; s->perspective=0; s->sprite_mode=0; }
static int scene_empty(const scene_t *s){ return s->ntri==0 && s->nrect==0 && s->nsprite==0; }
// tri_in_tile(t): the half-tile triangle bake.py draws for grid tile t (0..15, row*4+col).
static void tri_in_tile(int t, int vx[3], int vy[3]){
int ox=(t%4)*16, oy=(t/4)*16;
vx[0]=ox+1; vy[0]=oy+1;
vx[1]=ox+14; vy[1]=oy+1;
vx[2]=ox+1; vy[2]=oy+14;
}
// returns 0 ok, -1 on capacity/range error (message printed)
static int add_tri_gouraud(scene_t *s, const int vx[3], const int vy[3],
const uint8_t rr[3], const uint8_t gg[3], const uint8_t bb[3], uint32_t z){
if (s->ntri >= MAX_TRI){ fprintf(stderr,"error: too many triangles (max %d)\n", MAX_TRI); return -1; }
for (int i=0;i<3;i++){
if (vx[i]<0||vx[i]>4095||vy[i]<0||vy[i]>4095){
fprintf(stderr,"error: vertex (%d,%d) out of 12-bit range [0..4095]\n", vx[i], vy[i]); return -1; }
}
tri_t *t=&s->tri[s->ntri++];
for (int i=0;i<3;i++){ t->x[i]=vx[i]; t->y[i]=vy[i]; t->r[i]=rr[i]; t->g[i]=gg[i]; t->b[i]=bb[i];
t->u[i]=TMP_UV[i][0]; t->v[i]=TMP_UV[i][1]; } // default UV (byte-exact w/ prior scenes)
t->z=z;
return 0;
}
// Ch341 — textured triangle: per-vertex real UV (texel coords) + a flat MODULATE color. Requires the
// scene `tex0` binding (caller sets s->textured). UV range [0..4095] (10.4 fixed via uv_data).
static int add_tritex(scene_t *s, const int vx[3], const int vy[3], const int uu[3], const int vv[3],
uint8_t r, uint8_t g, uint8_t b, uint32_t z){
uint8_t rr[3]={r,r,r}, gg[3]={g,g,g}, bb[3]={b,b,b};
if (add_tri_gouraud(s, vx, vy, rr, gg, bb, z)) return -1;
tri_t *t=&s->tri[s->ntri-1];
for (int i=0;i<3;i++){
if (uu[i]<0||uu[i]>4095||vv[i]<0||vv[i]>4095){
fprintf(stderr,"error: UV (%d,%d) out of range [0..4095]\n", uu[i], vv[i]); s->ntri--; return -1; }
t->u[i]=uu[i]; t->v[i]=vv[i];
}
return 0;
}
static int add_tri_flat(scene_t *s, const int vx[3], const int vy[3], uint8_t r, uint8_t g, uint8_t b, uint32_t z){
uint8_t rr[3]={r,r,r}, gg[3]={g,g,g}, bb[3]={b,b,b};
return add_tri_gouraud(s, vx, vy, rr, gg, bb, z);
}
static int add_tritile(scene_t *s, int tile, uint32_t z, uint8_t r, uint8_t g, uint8_t b){
if (tile<0||tile>15){ fprintf(stderr,"error: tile %d out of range [0..15]\n", tile); return -1; }
int vx[3], vy[3]; tri_in_tile(tile, vx, vy);
return add_tri_flat(s, vx, vy, r, g, b, z);
}
// Ch342 — perspective textured triangle: per-vertex screen (x,y) + fixed-point (S_fp,T_fp,Q_fp) + flat
// MODULATE color. Requires the scene `tex0` + `persp`. 24-bit fixed-point (FRAC=12), so S/T/Q <= 0xFFFFFF.
static int add_persptri(scene_t *s, const int vx[3], const int vy[3],
const uint32_t sf[3], const uint32_t tf[3], const uint32_t qf[3],
uint8_t r, uint8_t g, uint8_t b, uint32_t z){
uint8_t rr[3]={r,r,r}, gg[3]={g,g,g}, bb[3]={b,b,b};
if (add_tri_gouraud(s, vx, vy, rr, gg, bb, z)) return -1;
tri_t *t=&s->tri[s->ntri-1];
for (int i=0;i<3;i++){
if (sf[i]>0xFFFFFF||tf[i]>0xFFFFFF||qf[i]>0xFFFFFF){
fprintf(stderr,"error: ST/Q fixed-point out of 24-bit range\n"); s->ntri--; return -1; }
t->s_fp[i]=sf[i]; t->t_fp[i]=tf[i]; t->q_fp[i]=qf[i];
}
return 0;
}
static int add_rect(scene_t *s, int tile, uint32_t z, uint8_t r, uint8_t g, uint8_t b){
if (s->perspective){ fprintf(stderr,"error: rects not allowed in perspective mode\n"); return -1; }
if (tile<0||tile>15){ fprintf(stderr,"error: rect tile %d out of range [0..15]\n", tile); return -1; }
if (s->nrect >= MAX_RECT){ fprintf(stderr,"error: too many rects (max %d)\n", MAX_RECT); return -1; }
rect_t *q=&s->rect[s->nrect++]; q->tile=tile; q->z=z; q->r=r; q->g=g; q->b=b;
return 0;
}
// Ch345a — textured + source-over alpha SPRITE: screen rect + per-corner affine UV + flat MODULATE tint.
// Requires the scene `tex0` binding (textured); sets sprite_mode. Fails closed if mixed with tris/rects/persp.
static int add_sprite(scene_t *s, int x0, int y0, int x1, int y1,
int u0, int v0, int u1, int v1, uint8_t r, uint8_t g, uint8_t b){
if (!s->textured){ fprintf(stderr,"error: sprite requires a prior tex0 binding\n"); return -1; }
if (s->perspective || s->ntri || s->nrect){
fprintf(stderr,"error: sprite mode is exclusive with persp/tris/rects\n"); return -1; }
if (s->nsprite >= MAX_SPRITE){ fprintf(stderr,"error: too many sprites (max %d)\n", MAX_SPRITE); return -1; }
if (x0<0||x0>4095||y0<0||y0>4095||x1<0||x1>4095||y1<0||y1>4095){
fprintf(stderr,"error: sprite vertex out of 12-bit range [0..4095]\n"); return -1; }
if (u0<0||u0>4095||v0<0||v0>4095||u1<0||u1>4095||v1<0||v1>4095){
fprintf(stderr,"error: sprite UV out of range [0..4095]\n"); return -1; }
s->sprite_mode = 1;
sprite_t *q=&s->sprite[s->nsprite++];
q->x0=x0; q->y0=y0; q->x1=x1; q->y1=y1; q->u0=u0; q->v0=v0; q->u1=u1; q->v1=v1; q->r=r; q->g=g; q->b=b;
return 0;
}
// ======================== staging builder (byte-exact with bake.py builders) ===================
// Emits: word0 = {rect_count[31:16], tri_count[15:0]} (== tri_count when rect_count==0, matching the
// colored/gouraud builders), words1..6 = FRAME/ALPHA/TEST/ZBUF/TEX0(unity+MODULATE)/PRIM, then each
// triangle as 3x(RGBAQ,UV,XYZ2), then each rect as rect_record (RGBAQ, corner0 XYZ2, corner1 XYZ2).
// out[] is zero-padded to FEEDER_STG_WORDS. Returns meaningful word count, or -1 if it won't fit.
static int build_staging(const scene_t *s, uint64_t out[FEEDER_STG_WORDS]){
int need = s->sprite_mode ? (7 + s->nsprite*6) : (7 + s->ntri*9 + s->nrect*3);
if (need > FEEDER_STG_WORDS){
fprintf(stderr,"error: scene '%s' needs %d staging words > %d (FEEDER_STG_WORDS) — too large\n",
s->name?s->name:"?", need, FEEDER_STG_WORDS);
return -1;
}
if (s->perspective && (!s->textured || s->nrect)){
fprintf(stderr,"error: perspective mode requires tex0 and no rects\n"); return -1;
}
// Ch345a — SPRITE format: word0[33]=1, SPRITE+TME+ABE source-over, per-corner UV, As from texel (TCC).
if (s->sprite_mode){
if (!s->textured){ fprintf(stderr,"error: sprite mode requires tex0\n"); return -1; }
for (int i=0;i<FEEDER_STG_WORDS;i++) out[i]=0;
int n=0;
out[n++] = (uint64_t)(s->nsprite & 0xFFFF) | ((uint64_t)1 << 33);
out[n++] = frame_1_psmct32(CAP_FBW);
out[n++] = alpha_pack(0,1,0,1,0); // source-over
out[n++] = 0; // TEST_1 (ZTE=0, no depth)
out[n++] = 0; // ZBUF_1
out[n++] = tex0_pack(s->tex_tbp, s->tex_tbw, 0, s->tex_tw, s->tex_th, s->tex_tfx); // PSMCT32
out[n++] = prim_sprite_tme_abe(); // SPRITE+TME+ABE
for (int i=0;i<s->nsprite;i++){
const sprite_t *q=&s->sprite[i];
// RGBAQ A=0x80 (neutral; the vertex alpha is IGNORED — As comes from the texel, TCC=1). Matches
// the bake.py golden so test_ps2_feeder.sh can byte-check the sprite staging.
uint64_t tint = ((uint64_t)0x80<<24) | ((uint64_t)q->b<<16) | ((uint64_t)q->g<<8) | q->r;
out[n++] = tint; out[n++] = uv_data(q->u0,q->v0); out[n++] = xyz2_dataz(q->x0,q->y0,0);
out[n++] = tint; out[n++] = uv_data(q->u1,q->v1); out[n++] = xyz2_dataz(q->x1,q->y1,0);
}
return n;
}
for (int i=0;i<FEEDER_STG_WORDS;i++) out[i]=0;
int n=0;
// Ch342 — word0[32] = perspective format flag (feeder emits ST instead of UV; rects forced off).
out[n++] = ((uint64_t)(s->nrect & 0xFFFF) << 16) | (uint64_t)(s->ntri & 0xFFFF)
| (s->perspective ? ((uint64_t)1 << 32) : 0);
out[n++] = frame_1_psmct32(CAP_FBW);
out[n++] = alpha_pack(0,1,0,1,0);
out[n++] = test1_geq();
out[n++] = zbuf1_pack(2,0,0);
out[n++] = s->textured ? tex0_pack(s->tex_tbp, s->tex_tbw, 0, s->tex_tw, s->tex_th, s->tex_tfx)
: tex0_pack(CAP_TBP1, TMP_TBW, 0, 2, 2, 0); // unity texture + MODULATE (default)
out[n++] = s->perspective ? prim_tri_tme() : prim_tri_tme_abe(); // Ch342: ABE=0 -> S1 perspective path
for (int i=0;i<s->ntri;i++){
const tri_t *t=&s->tri[i];
for (int v=0; v<3; v++){
if (s->perspective){ // RGBAQ(+Q_fp) / ST(S_fp,T_fp) / XYZ2
out[n++] = rgbaq_persp(t->r[v], t->g[v], t->b[v], t->q_fp[v]);
out[n++] = st_data(t->s_fp[v], t->t_fp[v]);
} else { // legacy: RGBAQ / UV / XYZ2 (byte-exact)
out[n++] = rgbaq_data(t->r[v], t->g[v], t->b[v]);
out[n++] = uv_data(t->u[v], t->v[v]);
}
out[n++] = xyz2_dataz(t->x[v], t->y[v], t->z);
}
}
for (int i=0;i<s->nrect;i++){
const rect_t *q=&s->rect[i];
int ox=(q->tile%4)*16, oy=(q->tile/4)*16;
out[n++] = rgbaq_data(q->r, q->g, q->b);
out[n++] = xyz2_dataz(ox+1, oy+1, q->z);
out[n++] = xyz2_dataz(ox+14, oy+14, q->z);
}
return n;
}
// ======================== built-in named scenes (reproduce the Ch333-338 goldens) ===================
#define RED 0xFF,0x00,0x00
#define GREEN 0x00,0xFF,0x00
#define BLUE 0x00,0x00,0xFF
#define YELLOW 0xFF,0xFF,0x00
#define WHITE 0xFF,0xFF,0xFF
static int build_named(const char *name, scene_t *s){
scene_reset(s, name);
if (!strcmp(name,"color-tri")){ // Ch333 feeder_color_tri.mem
return add_tritile(s,0,0x5000,RED) || add_tritile(s,5,0x5100,GREEN) || add_tritile(s,10,0x5200,BLUE);
}
if (!strcmp(name,"sprite")){ // Ch345a feeder_sprite.mem — 3 textured-alpha sprites
// tex0 8x8 PSMCT32 @ TBP=64 (TW=TH=3 log2), MODULATE; white (0x80) tint = identity. As from texel.
s->textured=1; s->tex_tbp=64; s->tex_tbw=1; s->tex_tw=3; s->tex_th=3; s->tex_tfx=0;
return add_sprite(s, 8,24,24,40, 0,0,8,8, 128,128,128)
|| add_sprite(s, 26,24,42,40, 0,0,8,8, 128,128,128)
|| add_sprite(s, 44,24,60,40, 0,0,8,8, 128,128,128);
}
if (!strcmp(name,"native-rect")){ // Ch334 feeder_native_rect.mem (3 rects, 0 tris)
return add_rect(s,0,0x5000,RED) || add_rect(s,5,0x5100,GREEN) || add_rect(s,10,0x5200,BLUE);
}
if (!strcmp(name,"gouraud-tri")){ // Ch335 feeder_gouraud_tri.mem (1 gouraud tri, tile 0)
int vx[3],vy[3]; tri_in_tile(0,vx,vy);
uint8_t rr[3]={0xFF,0x00,0x00}, gg[3]={0x00,0xFF,0x00}, bb[3]={0x00,0x00,0xFF};
return add_tri_gouraud(s,vx,vy,rr,gg,bb,0x5000);
}
if (!strcmp(name,"accum")){ // Ch336 feeder_accum.mem
for (int t=0;t<14;t++) if (t<8 ? add_tritile(s,t,0x5000+t*0x100,RED)
: add_tritile(s,t,0x5000+t*0x100,BLUE)) return -1;
return 0;
}
if (!strcmp(name,"retrigger-a")){ // Ch337 feeder_scene_a.mem
for (int t=0;t<14;t++) if (add_tritile(s,t,0x5000+t*0x100,RED)) return -1;
return 0;
}
if (!strcmp(name,"retrigger-b")){ // Ch337 feeder_scene_b.mem (tiles 2..15)
for (int t=2;t<16;t++) if (add_tritile(s,t,0x5000+(t-2)*0x100,BLUE)) return -1;
return 0;
}
if (!strcmp(name,"zpersist-near") || !strcmp(name,"zpersist-far")){ // Ch338 flat overlap
int near_first = !strcmp(name,"zpersist-near");
static const int b0f[7]={0,1,2,3,4,6,7}, b1f[5]={8,9,10,11,12};
// batch0 = overlap prim + 7 fillers ; batch1 = overlap prim + 5 fillers.
if (near_first){
if (add_tritile(s,5,0x7000,RED)) return -1; // near RED (batch0)
for (int i=0;i<7;i++) if (add_tritile(s,b0f[i],0x6000,RED)) return -1;
if (add_tritile(s,5,0x5000,BLUE)) return -1; // far BLUE (batch1)
for (int i=0;i<5;i++) if (add_tritile(s,b1f[i],0x6000,BLUE)) return -1;
} else {
if (add_tritile(s,5,0x5000,BLUE)) return -1; // far BLUE (batch0)
for (int i=0;i<7;i++) if (add_tritile(s,b0f[i],0x6000,BLUE)) return -1;
if (add_tritile(s,5,0x7000,RED)) return -1; // near RED (batch1)
for (int i=0;i<5;i++) if (add_tritile(s,b1f[i],0x6000,RED)) return -1;
}
return 0;
}
if (!strcmp(name,"zpersist-grad")){ // Ch338 feeder_zpersist_grad.mem (gouraud near + flat far)
static const int b0f[7]={0,1,2,3,4,6,7}, b1f[5]={8,9,10,11,12};
int vx[3],vy[3]; tri_in_tile(5,vx,vy);
uint8_t rr[3]={0xFF,0x00,0x00}, gg[3]={0x00,0xFF,0x00}, bb[3]={0x00,0x00,0xFF};
if (add_tri_gouraud(s,vx,vy,rr,gg,bb,0x7000)) return -1; // near gradient (batch0)
for (int i=0;i<7;i++) if (add_tritile(s,b0f[i],0x6000,GREEN)) return -1;
if (add_tritile(s,5,0x5000,WHITE)) return -1; // far flat white (batch1)
for (int i=0;i<5;i++) if (add_tritile(s,b1f[i],0x6000,WHITE)) return -1;
return 0;
}
fprintf(stderr,"error: unknown scene '%s' (try --list)\n", name);
return -1;
}
static const char *named_scenes[] = {
"color-tri","native-rect","gouraud-tri","accum","retrigger-a","retrigger-b",
"zpersist-near","zpersist-far","zpersist-grad", NULL
};
// ======================== scene-file parser ===================
// Parses tokens into one or more scenes (split on `go`). Returns count, or -1 on error.
// scenes[] must hold at least max_scenes entries.
static int parse_int(const char *tok, long *out){
char *end; errno=0; long v=strtol(tok,&end,0);
if (*end || errno) return -1;
*out=v; return 0;
}
static int parse_scene_file(const char *path, scene_t *scenes, int max_scenes){
FILE *f=fopen(path,"r");
if (!f){ fprintf(stderr,"error: cannot open '%s': %s\n", path, strerror(errno)); return -1; }
int nsc=0; scene_reset(&scenes[0], path);
char line[512]; int lineno=0;
while (fgets(line,sizeof line,f)){
lineno++;
char *h=strchr(line,'#'); if (h) *h=0; // strip comment
char *tok=strtok(line," \t\r\n"); if (!tok) continue;
long a[24]; int na=0; char op[32]; // persptri needs 19 args
snprintf(op,sizeof op,"%s",tok);
for (char *t=strtok(NULL," \t\r\n"); t && na<24; t=strtok(NULL," \t\r\n")){
if (parse_int(t,&a[na])){ fprintf(stderr,"error: %s:%d bad number '%s'\n",path,lineno,t); fclose(f); return -1; }
na++;
}
scene_t *s=&scenes[nsc];
int rc=0;
if (!strcmp(op,"go")){
if (!scene_empty(s)){ if (++nsc>=max_scenes){ fprintf(stderr,"error: too many scenes\n"); fclose(f); return -1; } scene_reset(&scenes[nsc], path); }
} else if (!strcmp(op,"tri") && na==10){
int vx[3]={(int)a[0],(int)a[2],(int)a[4]}, vy[3]={(int)a[1],(int)a[3],(int)a[5]};
rc=add_tri_flat(s,vx,vy,(uint8_t)a[7],(uint8_t)a[8],(uint8_t)a[9],(uint32_t)a[6]);
} else if (!strcmp(op,"trig") && na==16){
int vx[3]={(int)a[0],(int)a[5],(int)a[10]}, vy[3]={(int)a[1],(int)a[6],(int)a[11]};
uint8_t rr[3]={(uint8_t)a[2],(uint8_t)a[7],(uint8_t)a[12]};
uint8_t gg[3]={(uint8_t)a[3],(uint8_t)a[8],(uint8_t)a[13]};
uint8_t bb[3]={(uint8_t)a[4],(uint8_t)a[9],(uint8_t)a[14]};
rc=add_tri_gouraud(s,vx,vy,rr,gg,bb,(uint32_t)a[15]);
} else if (!strcmp(op,"tritile") && na==5){
rc=add_tritile(s,(int)a[0],(uint32_t)a[1],(uint8_t)a[2],(uint8_t)a[3],(uint8_t)a[4]);
} else if (!strcmp(op,"rect") && na==5){
rc=add_rect(s,(int)a[0],(uint32_t)a[1],(uint8_t)a[2],(uint8_t)a[3],(uint8_t)a[4]);
} else if (!strcmp(op,"tex0") && na==5){ // tex0 TBP TBW TW TH TFX — scene-level texture bind
s->textured=1; s->tex_tbp=(uint32_t)a[0]; s->tex_tbw=(uint32_t)a[1];
s->tex_tw=(uint32_t)a[2]; s->tex_th=(uint32_t)a[3]; s->tex_tfx=(uint32_t)a[4];
} else if (!strcmp(op,"tritex") && na==16){ // tritex x0 y0 u0 v0 x1 y1 u1 v1 x2 y2 u2 v2 z r g b
if (!s->textured){ fprintf(stderr,"error: %s:%d tritex needs a prior tex0\n",path,lineno); fclose(f); return -1; }
int vx[3]={(int)a[0],(int)a[4],(int)a[8]}, vy[3]={(int)a[1],(int)a[5],(int)a[9]};
int uu[3]={(int)a[2],(int)a[6],(int)a[10]}, vv[3]={(int)a[3],(int)a[7],(int)a[11]};
rc=add_tritex(s,vx,vy,uu,vv,(uint8_t)a[13],(uint8_t)a[14],(uint8_t)a[15],(uint32_t)a[12]);
} else if (!strcmp(op,"persp") && na==0){ // mark scene perspective (needs prior tex0)
if (!s->textured){ fprintf(stderr,"error: %s:%d persp needs a prior tex0\n",path,lineno); fclose(f); return -1; }
s->perspective=1;
} else if (!strcmp(op,"persptri") && na==19){ // persptri x y s t q (x3) z r g b — fixed-point ST/Q
if (!s->perspective){ fprintf(stderr,"error: %s:%d persptri needs a prior persp\n",path,lineno); fclose(f); return -1; }
int vx[3]={(int)a[0],(int)a[5],(int)a[10]}, vy[3]={(int)a[1],(int)a[6],(int)a[11]};
uint32_t sf[3]={(uint32_t)a[2],(uint32_t)a[7],(uint32_t)a[12]};
uint32_t tf[3]={(uint32_t)a[3],(uint32_t)a[8],(uint32_t)a[13]};
uint32_t qf[3]={(uint32_t)a[4],(uint32_t)a[9],(uint32_t)a[14]};
rc=add_persptri(s,vx,vy,sf,tf,qf,(uint8_t)a[16],(uint8_t)a[17],(uint8_t)a[18],(uint32_t)a[15]);
} else if (!strcmp(op,"sprite") && na==11){ // Ch345a — sprite x0 y0 x1 y1 u0 v0 u1 v1 r g b
// textured + source-over alpha SPRITE (needs a prior tex0). r/g/b = MODULATE tint; As from texel.
rc=add_sprite(s,(int)a[0],(int)a[1],(int)a[2],(int)a[3],(int)a[4],(int)a[5],(int)a[6],(int)a[7],
(uint8_t)a[8],(uint8_t)a[9],(uint8_t)a[10]);
} else {
fprintf(stderr,"error: %s:%d malformed command '%s' (got %d args)\n",path,lineno,op,na); fclose(f); return -1;
}
if (rc){ fprintf(stderr," (at %s:%d)\n",path,lineno); fclose(f); return -1; }
}
fclose(f);
if (!scene_empty(&scenes[nsc])) nsc++; // trailing scene with no closing 'go'
return nsc;
}
// ======================== bridge I/O ===================
typedef struct { volatile uint8_t *base; int dry; } bridge_t;
static void wr32(bridge_t *br, int off, uint32_t v){ if (!br->dry) *(volatile uint32_t*)(br->base+off)=v; }
static uint32_t rd32(bridge_t *br, int off){ return br->dry ? 0 : *(volatile uint32_t*)(br->base+off); }
static int wait_ready(bridge_t *br){
if (br->dry) return 0;
for (int i=0;i<300000;i++){ if (rd32(br,OFF_STATUS)&1) return 0; usleep(10); }
fprintf(stderr,"error: feeder never reported ready\n"); return -1;
}
// Stream one scene: reset addr, write {lo,hi} per word, GO, wait completion. Prints diagnostics.
static int stream_scene(bridge_t *br, const scene_t *s){
uint64_t w[FEEDER_STG_WORDS];
int n = build_staging(s, w);
if (n < 0) return -1;
int exp_prims = s->ntri + s->nrect*2; // feeder expands each rect -> 2 triangles
int exp_batches = (exp_prims + FIFO_DEPTH - 1) / FIFO_DEPTH;
printf("[scene %-14s] tris=%d rects=%d -> staged_words=%d expand_prims=%d batches~=%d\n",
s->name?s->name:"?", s->ntri, s->nrect, n, exp_prims, exp_batches);
if (wait_ready(br)) return -1;
wr32(br, OFF_STATUS, 0); // reset staging write address
for (int i=0;i<n;i++){ wr32(br, OFF_LO, (uint32_t)(w[i]&0xFFFFFFFF)); wr32(br, OFF_HI, (uint32_t)(w[i]>>32)); }
uint32_t addr = rd32(br, OFF_LO);
if (!br->dry && (int)addr != n){ fprintf(stderr,"error: bridge addr=%u after streaming %d words\n", addr, n); return -1; }
if (wait_ready(br)) return -1; // staging accepted
wr32(br, OFF_GO, 1); // trigger
if (wait_ready(br)) return -1; // Ch337: ready only after the WHOLE scene drained
uint32_t records = rd32(br, OFF_HI), waits = rd32(br, OFF_GO);
printf(" staged_addr=%u records=%u waits=%u completion=ready%s\n",
addr, records, waits, br->dry?" (dry-run, no hw readback)":"");
if (!br->dry && (int)records != exp_prims)
fprintf(stderr," warn: hw records=%u != host-expected %d (rect expansion / format?)\n", records, exp_prims);
return 0;
}
// ======================== main ===================
static void dump_words(const uint64_t w[FEEDER_STG_WORDS]){
for (int i=0;i<FEEDER_STG_WORDS;i++) printf("%016llx\n", (unsigned long long)w[i]);
}
static void usage(void){
fprintf(stderr,
"usage: ps2_feeder [--base 0xADDR] [--dry-run] <scene>...\n"
" ps2_feeder [--base 0xADDR] [--dry-run] -f <scenefile>\n"
" ps2_feeder --dump <scene> | --dump-file <scenefile> | --list\n");
}
int main(int argc, char **argv){
unsigned long base = 0x40000000UL;
int dry = 0;
const char *dump = NULL, *dumpfile = NULL, *scenefile = NULL;
const char *names[64]; int nnames=0;
for (int i=1;i<argc;i++){
if (!strcmp(argv[i],"--list")){ for (const char**p=named_scenes;*p;p++) printf("%s\n",*p); return 0; }
else if (!strcmp(argv[i],"--base") && i+1<argc) base=strtoul(argv[++i],NULL,0);
else if (!strcmp(argv[i],"--dry-run")) dry=1;
else if (!strcmp(argv[i],"--dump") && i+1<argc) dump=argv[++i];
else if (!strcmp(argv[i],"--dump-file") && i+1<argc) dumpfile=argv[++i];
else if (!strcmp(argv[i],"-f") && i+1<argc) scenefile=argv[++i];
else if (argv[i][0]=='-'){ usage(); return 2; }
else if (nnames<64) names[nnames++]=argv[i];
}
// ---- dump modes (no board access) ----
if (dump){
scene_t s; if (build_named(dump,&s)) return 1;
uint64_t w[FEEDER_STG_WORDS]; if (build_staging(&s,w)<0) return 1;
dump_words(w); return 0;
}
if (dumpfile){
scene_t scenes[16]; int nsc=parse_scene_file(dumpfile,scenes,16); if (nsc<0) return 1;
if (nsc==0){ fprintf(stderr,"error: no scenes in '%s'\n",dumpfile); return 1; }
uint64_t w[FEEDER_STG_WORDS]; if (build_staging(&scenes[0],w)<0) return 1;
dump_words(w); return 0;
}
// ---- build the work list (named scenes or a scene file) ----
scene_t scenes[64]; int nsc=0;
if (scenefile){ nsc=parse_scene_file(scenefile,scenes,64); if (nsc<0) return 1; }
for (int i=0;i<nnames;i++){ if (nsc>=64){ fprintf(stderr,"too many scenes\n"); return 1; } if (build_named(names[i],&scenes[nsc])) return 1; nsc++; }
if (nsc==0){ usage(); return 2; }
// validate every scene encodes before touching hardware
for (int i=0;i<nsc;i++){ uint64_t w[FEEDER_STG_WORDS]; if (build_staging(&scenes[i],w)<0) return 1; }
// ---- open the bridge (unless dry-run) ----
bridge_t br = { .base=NULL, .dry=dry };
int fd=-1; void *map=NULL;
if (!dry){
fd=open("/dev/mem", O_RDWR|O_SYNC);
if (fd<0){ fprintf(stderr,"error: open /dev/mem: %s\n", strerror(errno)); return 1; }
map=mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, (off_t)base);
if (map==MAP_FAILED){ fprintf(stderr,"error: mmap 0x%lx: %s\n", base, strerror(errno)); close(fd); return 1; }
br.base=(volatile uint8_t*)map;
printf("ps2_feeder: bridge @ 0x%lx, %d scene(s)\n", base, nsc);
} else {
printf("ps2_feeder: DRY-RUN (encode+validate only), %d scene(s)\n", nsc);
}
int rc=0;
for (int i=0;i<nsc;i++){ if (stream_scene(&br,&scenes[i])){ rc=1; break; } }
if (!dry){ munmap(map,0x1000); close(fd); }
printf("ps2_feeder: %s\n", rc?"FAILED":"done");
return rc;
}