// 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;
}
