// retroDE_ps2 — ps2_sh3_sched (Ch356 N-TEXTURE SCHEDULER host, data-driven epoch descriptors)
//
// Generalizes Ch355's hard-coded two-group flow (ps2_sh3_multitex) to a DATA-DRIVEN scheduler that reads an epoch
// descriptor table (sh3_sched_epochs.txt) and iterates it: N authentic SH3 draws with DIFFERENT textures/CLUTs
// composite into ONE LPDDR framebuffer via scene-level texture rebind + staged-list retriggering. The CLUT table is
// preloaded by the bootlet (all N relocated CLUTs) — runtime CLUT upload is OUT of scope; the scheduler SELECTS a
// preloaded palette per epoch (via each list's TEX0 CBP).
//
//   wait ready -> FB base 0 -> preclear ONCE -> for each epoch k:
//       upload tex_k -> LPDDR (single region) -> FRESH cache fill + verify CRC/beats/bytes/rd_errs
//       STREAM list_k over the bridge (0x0D8=0 reset, 0x0DC/0x0E4 commit) -> arm writer (k==0) -> GO
//       await FRESH ordered drain (k==0: low->high; k>0: high->low->high anti-stale) -> records == tris
//   -> enable scanout (video_src) ONLY after the LAST fresh drain.
// Requires the frame_drained diagnostic bit (0x02C[6]).
//
// Build on the board:  gcc -O2 -o ps2_sh3_sched ps2_sh3_sched.c
// Run (after fit+boot): sudo ./ps2_sh3_sched            (reads sh3_sched_epochs.txt in the cwd)
//                  or:  sudo ./ps2_sh3_sched <epochs.txt> [--datadir DIR]
// Optional framebuffer readback:
//                      sudo ./ps2_sh3_sched --zbuf sh3_zsched_epochs.txt --dump-fb sh3_zsched_board_fb.mem
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <ctype.h>
#include <time.h>

#define OFF_LPDDR_CTRL   0x018   // W: [0]arm [1]canary [2]video_src [3]scanout_lb
#define OFF_LPDDR_FBBASE 0x01C
#define OFF_LPDDR_STATUS 0x02C   // R: [0]idle [3]rd_pending [6]frame_drained (STABLE ordered ack)
#define OFF_LPDDR_BYTES  0x030
#define OFF_LPDDR_RDADDR 0x03C   // W: read byte addr + trigger ; R: latched read data
#define OFF_WRADDR       0x04C
#define OFF_WRDATA       0x050
#define OFF_TEX_FILL     0x054   // W[0] arm fill ; R[0] fill_done [2] write_pending
#define OFF_TEX_BEATS    0x058
#define OFF_TEX_BYTES    0x05C
#define OFF_TEX_RDERRS   0x068
#define OFF_WR_ERRS      0x06C
#define OFF_TEX_CRC      0x070
#define OFF_STG_STATUS   0x0D8   // R[0] feeder ready ; W reset staging write address (value MUST be 0)
#define OFF_STG_LO       0x0DC   // W low32 ; R current staging addr
#define OFF_STG_HI       0x0E4   // W high32 (commits {hi,lo}, ++addr) ; R records emitted
#define OFF_GO           0x0E8   // W[0] trigger feeder
#define OFF_CLUT_CTRL    0x1E0   // R[0] busy R[1] done-toggle; W[0] commit staged palette
#define OFF_CLUT_EXPECT  0x1E4   // W expected palette sum32; R readback
#define OFF_CLUT_RESULT  0x1E8   // R completed palette sum32
#define OFF_CLUT_STAGE   0x200   // W 256 x 32-bit palette entries (0x200..0x5FC)
#define FRAME_DRAINED    0x40    // 0x02C[6]
#define CLEAR_DONE       0x80    // 0x02C[7] — Ch357 persistent-Z preclear complete (host waits before first GO)
#define DROPS_NONZERO    0x100   // 0x02C[8] — Ch357 sticky: a fragment was EVER dropped (fail-closed)
#define OFF_FRAG_DROPS   0x0EC   // R: Ch357 persistent-Z dropped-fragment snapshot (per-scene delta must be 0)
#define RD_PENDING       0x08    // 0x02C[3]
#define WR_PENDING       0x04    // 0x054[2]
#define READY_BIT        0x01
#define CLUT_BUSY        0x01
#define CLUT_DONE        0x02

#define MAX_EPOCHS 8192          // descriptors are small; epoch payloads are streamed from disk one at a time
#define STG_MAX    2048
#define TEX_MAX    65536         // 512x512 PSMT8 / 4 = 65536 words
#define MAX_SEQUENCE_SCENES 16

typedef struct { volatile uint8_t *base; int dry; } br_t;
static void     wr32(br_t*b,int o,uint32_t v){ if(!b->dry) *(volatile uint32_t*)(b->base+o)=v; }
static uint32_t rd32(br_t*b,int o){ return b->dry?0:*(volatile uint32_t*)(b->base+o); }

// Polling the lightweight HPS bridge flat-out can consume the entire host-side
// transaction window while a large LPDDR scene is draining.  Use elapsed time,
// not a CPU/bridge-speed-dependent read count, and yield briefly between reads.
static int wait_status_level(br_t*b, uint32_t mask, int want_set, int timeout_ms){
    struct timespec t0, tn;
    clock_gettime(CLOCK_MONOTONIC, &t0);
    for(;;){
        if (!!(rd32(b,OFF_LPDDR_STATUS)&mask) == !!want_set) return 0;
        usleep(10);
        clock_gettime(CLOCK_MONOTONIC, &tn);
        int64_t elapsed_ms=(int64_t)(tn.tv_sec-t0.tv_sec)*1000 + (tn.tv_nsec-t0.tv_nsec)/1000000;
        if(elapsed_ms >= timeout_ms) return -1;
    }
}

// buffer must exceed the longest banner line; skip any line whose first non-space char isn't a hex digit.
static int load32(const char*p, uint32_t*a, int n){
    FILE*f=fopen(p,"r"); if(!f){fprintf(stderr,"open %s: %s\n",p,strerror(errno));return -1;}
    int k=0; char ln[512];
    while(k<n && fgets(ln,sizeof ln,f)){ char*s=ln; while(*s==' '||*s=='\t')s++; if(!isxdigit((unsigned char)*s))continue; a[k++]=(uint32_t)strtoul(s,NULL,16);}
    fclose(f); return k;
}
static int load64(const char*p, uint64_t*a, int n){
    FILE*f=fopen(p,"r"); if(!f){fprintf(stderr,"open %s: %s\n",p,strerror(errno));return -1;}
    int k=0; char ln[512];
    while(k<n && fgets(ln,sizeof ln,f)){ char*s=ln; while(*s==' '||*s=='\t')s++; if(!isxdigit((unsigned char)*s))continue; a[k++]=(uint64_t)strtoull(s,NULL,16);}
    fclose(f); return k;
}

typedef struct { int k, idx, tbp, cbp; char tex[128], list[128], pal[128]; unsigned long lpddr; uint32_t crc, pal_crc; int words, records, reuse; } epoch_t;
typedef struct { int n_epochs, fbpxw, fbh, fbwords, tex_words, n_beats; unsigned long lpddr_tex; epoch_t ep[MAX_EPOCHS]; } sched_t;

// parse the descriptor table: a META line + one row per epoch.
static int parse_epochs(const char*path, sched_t*s){
    FILE*f=fopen(path,"r"); if(!f){fprintf(stderr,"open %s: %s\n",path,strerror(errno));return -1;}
    memset(s,0,sizeof *s); char ln[512]; int ne=0;
    while(fgets(ln,sizeof ln,f)){
        if(!strncmp(ln,"META",4)){
            char *p; if((p=strstr(ln,"n_epochs")))  sscanf(p+8," %d",&s->n_epochs);
                     if((p=strstr(ln,"fbpxw")))     sscanf(p+5," %d",&s->fbpxw);
                     if((p=strstr(ln,"fbh")))       sscanf(p+3," %d",&s->fbh);
                     if((p=strstr(ln,"fbwords")))   sscanf(p+7," %d",&s->fbwords);
                     if((p=strstr(ln,"tex_words"))) sscanf(p+9," %d",&s->tex_words);
                     if((p=strstr(ln,"n_beats")))   sscanf(p+7," %d",&s->n_beats);
                     if((p=strstr(ln,"lpddr_tex"))) sscanf(p+9," %lx",&s->lpddr_tex);
            continue;
        }
        char*c=ln; while(*c==' '||*c=='\t')c++; if(*c=='#'||*c=='\n'||*c==0)continue;   // comment/blank
        if(ne>=MAX_EPOCHS){fprintf(stderr,"too many epochs\n");fclose(f);return -1;}
        epoch_t*e=&s->ep[ne];
        // k idx tbp cbp_reloc tex_file lpddr size crc list_file words records [reuse [pal_file pal_sum32]]
        // Ch359 — trailing EXPLICIT reuse flag (1 = texture cache already resident; SKIP upload+fill, VERIFY the
        // resident CRC register instead). Older tables (zsched/zs640) have no column -> reuse=0 (back-compatible).
        e->reuse=0; snprintf(e->pal,sizeof e->pal,"-"); e->pal_crc=0;
        int got=sscanf(ln,"%d %d %d %d %127s %lx %*d %x %127s %d %d %d %127s %x",
                       &e->k,&e->idx,&e->tbp,&e->cbp,e->tex,&e->lpddr,&e->crc,e->list,&e->words,&e->records,&e->reuse,e->pal,&e->pal_crc);
        if(got!=10 && got!=11 && got!=13){fprintf(stderr,"bad epoch row (got %d): %s",got,ln);fclose(f);return -1;}
        if(e->reuse && ne==0){fprintf(stderr,"epoch 0 cannot be reuse=1 (nothing resident yet)\n");fclose(f);return -1;}
        ne++;
    }
    fclose(f);
    if(s->n_epochs==0) s->n_epochs=ne;
    if(s->n_epochs!=ne){fprintf(stderr,"META n_epochs=%d != %d rows\n",s->n_epochs,ne);return -1;}
    if(s->fbwords<=0||s->tex_words<=0||s->n_beats<=0){fprintf(stderr,"META missing fbwords/tex_words/n_beats\n");return -1;}
    return ne;
}
// Ch367 -- stage exactly one PSMCT32 CLUT and wait for the design-clock
// copier's done toggle. `pal_sum` is a sum32 over the 256 host words, the
// same simple integrity contract used by the LPDDR texture-fill gate.
static int upload_palette(br_t*b, int k, const uint32_t *pal, uint32_t pal_sum){
    uint32_t before=rd32(b,OFF_CLUT_CTRL)&CLUT_DONE;
    if(!b->dry && (rd32(b,OFF_CLUT_CTRL)&CLUT_BUSY)){
        fprintf(stderr,"  FAIL epoch %d: CLUT copier busy before palette commit\n",k); return -1;
    }
    wr32(b,OFF_CLUT_EXPECT,pal_sum);
    for(int i=0;i<256;i++) wr32(b,OFF_CLUT_STAGE+i*4,pal[i]);
    wr32(b,OFF_CLUT_CTRL,1);
    if(!b->dry){
        int g=0; while(((rd32(b,OFF_CLUT_CTRL)&CLUT_DONE)==before) && g<400000) g++;
        if((rd32(b,OFF_CLUT_CTRL)&CLUT_DONE)==before){
            fprintf(stderr,"  FAIL epoch %d: CLUT copier completion timeout\n",k); return -1;
        }
    }
    uint32_t got=rd32(b,OFF_CLUT_RESULT);
    printf("[sched] epoch %d pal: runtime copy sum32=0x%08x (exp 0x%08x)\n",k,got,pal_sum);
    if(!b->dry && got!=pal_sum){ fprintf(stderr,"  FAIL epoch %d: CLUT palette sum mismatch\n",k); return -1; }
    return 0;
}

static int preclear_range(br_t*b, uint32_t base, int words, const char*tag){
    wr32(b, OFF_WRADDR, base);   // write-probe auto-increments WRADDR per WRDATA write
    for (int i=0;i<words;i++){ wr32(b, OFF_WRDATA, 0);
        if(!b->dry){int g=0; while((rd32(b,OFF_TEX_FILL)&WR_PENDING)&&g<2000000)g++;} }
    uint32_t e=rd32(b,OFF_WR_ERRS);
    printf("[sched] preclear %s %d words (0x%x..0x%x)  wr_bresp_errs=%u\n", tag, words, base, base+words*4, e);
    return e?-1:0;
}
static int preclear_fb(br_t*b, int fbwords){ return preclear_range(b, 0, fbwords, "COLOR"); }
static int upload_and_fill(br_t*b, int k, uint32_t*tex, int texw, unsigned long lpddr, uint32_t exp_crc, int nbeats){
    wr32(b, OFF_WRADDR, (uint32_t)lpddr);
    for (int i=0;i<texw;i++){ wr32(b, OFF_WRDATA, tex[i]);
        if(!b->dry){int g=0; while((rd32(b,OFF_TEX_FILL)&WR_PENDING)&&g<2000000)g++;} }
    uint32_t werr=rd32(b,OFF_WR_ERRS);
    wr32(b, OFF_TEX_FILL, 0x1);                                                   // FRESH fill: low->high rearm
    if(!b->dry){ int g=0; while((rd32(b,OFF_TEX_FILL)&0x1)&&g<2000)g++;
                 g=0; while(!(rd32(b,OFF_TEX_FILL)&0x1)&&g<400000)g++; }
    uint32_t crc=rd32(b,OFF_TEX_CRC), beats=rd32(b,OFF_TEX_BEATS), bytes=rd32(b,OFF_TEX_BYTES), rderr=rd32(b,OFF_TEX_RDERRS);
    printf("[sched] epoch %d tex: upload wr_errs=%u ; fill crc=0x%08x (exp 0x%08x) beats=%u (exp %d) bytes=%u rd_errs=%u\n",
           k, werr, crc, exp_crc, beats, nbeats, bytes, rderr);
    if(b->dry) return 0;
    if(werr || crc!=exp_crc || beats!=(uint32_t)nbeats || rderr){ fprintf(stderr,"  FAIL epoch %d fill mismatch\n", k); return -1; }
    return 0;
}
static int wait_ready(br_t*b){ if(b->dry)return 0; for(int i=0;i<3000000;i++) if(rd32(b,OFF_STG_STATUS)&READY_BIT) return 0;
    fprintf(stderr,"  FAIL: feeder never reached C_READY\n"); return -1; }
static int stream_list(br_t*b, int k, uint64_t*list, int nwords){
    if (nwords > 4095){ fprintf(stderr,"  FAIL epoch %d %d words exceeds 12-bit bridge addr\n", k, nwords); return -1; }
    if (wait_ready(b)) return -1;
    wr32(b, OFF_STG_STATUS, 0);                                                   // reset staging addr to 0
    for (int i=0;i<nwords;i++){ wr32(b, OFF_STG_LO, (uint32_t)(list[i]&0xFFFFFFFF)); wr32(b, OFF_STG_HI, (uint32_t)(list[i]>>32)); }
    if (wait_ready(b)) return -1;
    uint32_t addr=rd32(b,OFF_STG_LO);
    printf("[sched] epoch %d: streamed %d words, staged_addr=%u (exp %d, < 4096)\n", k, nwords, addr, nwords);
    if (!b->dry && addr!=(uint32_t)nwords) fprintf(stderr,"  warn epoch %d: staged_addr=%u != %d\n", k, addr, nwords);
    return 0;
}
// Ch357 — wait for the persistent-Z preclear (z_rmw clear_start) to finish before the FIRST GO. The host arms EARLY so
// this overlaps the texture uploads, but it must be EXPLICITLY confirmed (a broken clear-wait was already found in sim).
static int wait_clear_done(br_t*b){
    if(b->dry){ printf("[sched] zbuf: clear_done wait (dry-run)\n"); return 0; }
    int g=0; while(!(rd32(b,OFF_LPDDR_STATUS)&CLEAR_DONE)&&g<40000000)g++;
    if(!(rd32(b,OFF_LPDDR_STATUS)&CLEAR_DONE)){ fprintf(stderr,"  FAIL: persistent-Z clear_done never rose (Z preclear stuck)\n"); return -1; }
    printf("[sched] zbuf: Z preclear complete (0x02C[7]) — safe to GO\n");
    return 0;
}
// Ch357 — fail-closed drop gate: the per-scene snapshot delta MUST be 0 and the sticky flag MUST be clear. Any dropped
// fragment (request-FIFO overflow) fails the run here rather than silently corrupting the frame.
static int check_no_drops(br_t*b, int k, uint32_t before){
    if(b->dry) return 0;
    uint32_t after=rd32(b,OFF_FRAG_DROPS), st=rd32(b,OFF_LPDDR_STATUS);
    if(after!=before || (st&DROPS_NONZERO)){
        fprintf(stderr,"  FAIL epoch %d: %u fragment(s) DROPPED this scene (snap %u->%u, sticky=%u)\n",
                k, after-before, before, after, (st&DROPS_NONZERO)?1:0);
        return -1;
    }
    printf("[sched] epoch %d: zero fragment drops (snap=%u, sticky=0)\n", k, after);
    return 0;
}
static int go_await_drain(br_t*b, int k, int exp_records, int expect_stale){
    uint32_t recs_before=b->dry?0:rd32(b,OFF_STG_HI);
    uint32_t beats_before=b->dry?0:rd32(b,OFF_LPDDR_BYTES);
    wr32(b, OFF_GO, 0x1);
    if(b->dry){ printf("[sched] epoch %d: GO (dry-run)\n", k); return 0; }
    if (expect_stale && wait_status_level(b,FRAME_DRAINED,0,1000)){
        // A very short draw can clear and reassert frame_drained entirely
        // between two HPS bridge reads.  Accept that missed-low case only
        // when the per-GO completion counters prove that new work finished;
        // otherwise retain the fail-closed stale-drain gate.
        uint32_t st=rd32(b,OFF_LPDDR_STATUS), recs=rd32(b,OFF_STG_HI), beats=rd32(b,OFF_LPDDR_BYTES);
        if (!(st&FRAME_DRAINED) || recs!=(uint32_t)exp_records ||
            (recs==recs_before && beats==beats_before)) {
            fprintf(stderr,"  FAIL epoch %d: frame_drained never fell — STALE drain"
                           " (records %u->%u, FB beats %u->%u)\n",
                    k,recs_before,recs,beats_before,beats);
            return -1;
        }
        printf("[sched] epoch %d: drain low pulse completed between host polls;"
               " counters advanced (records %u->%u, FB beats %u->%u)\n",
               k,recs_before,recs,beats_before,beats);
    }
    if (wait_status_level(b,FRAME_DRAINED,1,120000))
        fprintf(stderr,"  FAIL epoch %d: frame_drained rise timeout after 120 s\n", k);
    if (wait_ready(b)) return -1;
    uint32_t fd=rd32(b,OFF_LPDDR_STATUS)&FRAME_DRAINED, by=rd32(b,OFF_LPDDR_BYTES), recs=rd32(b,OFF_STG_HI);
    printf("[sched] epoch %d: GO -> fresh drain frame_drained=%u, records=%u (exp %d), FB beats=%u\n",
           k, fd?1:0, recs, exp_records, by);
    if (fd==0 || by==0){ fprintf(stderr,"  FAIL epoch %d: empty render (frame_drained=%u beats=%u)\n", k, fd?1:0, by); return -1; }
    if ((int)recs!=exp_records) fprintf(stderr,"  warn epoch %d: records=%u != %d\n", k, recs, exp_records);
    return 0;
}

// Optional board evidence: dump the rendered linear PSMCT32 LPDDR framebuffer at base 0 before HDMI scanout is enabled.
// This uses the same HPS read-probe contract as ps2_sh3_tex_upload.c: write byte address, wait rd_pending clear,
// then read the latched word back from OFF_LPDDR_RDADDR.
static int dump_fb(br_t*b, const char*path, int fbwords, int fbpxw, int fbh){
    if(!path) return 0;
    if(b->dry){ printf("[sched] dump-fb %s (%d words, dry-run)\n", path, fbwords); return 0; }
    FILE*f=fopen(path,"w");
    if(!f){ fprintf(stderr,"  FAIL: open dump-fb %s: %s\n", path, strerror(errno)); return -1; }

    uint32_t sum=0, xr=0, nonzero=0;
    int minx=-1, miny=-1, maxx=-1, maxy=-1, timeouts=0;
    for(int i=0;i<fbwords;i++){
        wr32(b, OFF_LPDDR_RDADDR, (uint32_t)i*4u);
        // The bridge-to-EMIF request and return both cross clock domains. RD_PENDING
        // may assert and clear between two Linux MMIO polls, so observing its rising
        // edge is not reliable. Wait longer than the complete CDC/one-beat path, then
        // retain the pending-low timeout guard before consuming the returned word.
        usleep(10);
        int g=0; while((rd32(b,OFF_LPDDR_STATUS)&RD_PENDING) && g<1000000) g++;
        if(g==1000000) { timeouts++; continue; }
        uint32_t v=rd32(b, OFF_LPDDR_RDADDR);
        fprintf(f,"%08x\n", v);
        sum += v; xr ^= v;
        if(v){
            nonzero++;
            if(fbpxw>0){
                int x=i%fbpxw, y=i/fbpxw;
                if(minx<0 || x<minx) minx=x;
                if(maxx<0 || x>maxx) maxx=x;
                if(miny<0 || y<miny) miny=y;
                if(maxy<0 || y>maxy) maxy=y;
            }
        }
    }
    if(fclose(f)){ fprintf(stderr,"  FAIL: close dump-fb %s: %s\n", path, strerror(errno)); return -1; }
    printf("[sched] dump-fb: wrote %d words -> %s  sum32=0x%08x xor32=0x%08x nonzero=%u",
           fbwords, path, sum, xr, nonzero);
    if(fbpxw>0 && fbh>0 && minx>=0) printf(" bounds=(%d,%d)..(%d,%d) FB=%dx%d", minx,miny,maxx,maxy,fbpxw,fbh);
    if(timeouts) printf(" rd_pending_timeouts=%d", timeouts);
    printf("\n");
    return timeouts ? -1 : 0;
}

// Run one complete descriptor table as a frame. A scene boundary deliberately drops arm/video before preclear so
// the following arm rise invalidates and clears persistent Z again; prior_scene makes the first GO demand a fresh
// high->low->high drain rather than accepting the preceding frame's stale completion.
static int run_scene(br_t*br, const char*epf, const char*datadir, int zbuf, const char*dump_fb_path,
                     int prior_scene, int present_ms){
    static sched_t S; if (parse_epochs(epf,&S)<0) return 1;
    printf("[sched] scene %s: %d epochs, FB %d words, tex %d words/%d beats%s\n",
           epf, S.n_epochs, S.fbwords, S.tex_words, S.n_beats, prior_scene?" (fresh after prior frame)":"");

    // Keep memory bounded as coverage scales into thousands of epochs.  The
    // old [MAX_EPOCHS][TEX_MAX] preload consumed 128 MiB at 512 epochs and
    // would exceed 2 GiB at 8192.  Validate with one scratch set, then reload
    // that epoch immediately before it is sent to the board.
    static uint32_t tex[TEX_MAX], pal[256]; static uint64_t list[STG_MAX];
    uint32_t last_fill_crc=0;
    for (int k=0;k<S.n_epochs;k++){
        char pt[256], pl[256]; snprintf(pt,sizeof pt,"%s/%s",datadir,S.ep[k].tex); snprintf(pl,sizeof pl,"%s/%s",datadir,S.ep[k].list);
        int n=load64(pl,list,STG_MAX); if(n<8){fprintf(stderr,"list load failed epoch %d\n",k);return 1;}
        int nt=(int)(list[0]&0xFFFF);
        int hdr_words=(list[0]&(1ULL<<34)) ? 8 : 7;
        int nw=hdr_words+nt*9;
        if(n<nw){fprintf(stderr,"list truncated epoch %d: loaded %d need %d\n",k,n,nw);return 1;}
        if (S.ep[k].reuse){
            if (S.ep[k].crc!=last_fill_crc){fprintf(stderr,"  FAIL epoch %d: reuse=1 but crc 0x%08x != resident 0x%08x\n",k,S.ep[k].crc,last_fill_crc);return 1;}
            printf("[sched] epoch %d: idx%d tbp%d cbp%d  REUSE resident tex (crc 0x%08x)  %d tris/%d words\n",
                   k,S.ep[k].idx,S.ep[k].tbp,S.ep[k].cbp, S.ep[k].crc, nt,nw);
        } else {
            if (load32(pt,tex,S.tex_words)!=S.tex_words){fprintf(stderr,"tex load failed epoch %d\n",k);return 1;}
            uint32_t crc=0; for(int i=0;i<S.tex_words;i++)crc+=tex[i];
            printf("[sched] epoch %d: idx%d tbp%d cbp%d  tex crc=0x%08x (exp 0x%08x)  %d tris/%d words\n",
                   k,S.ep[k].idx,S.ep[k].tbp,S.ep[k].cbp, crc,S.ep[k].crc, nt,nw);
            if (crc!=S.ep[k].crc){fprintf(stderr,"  FAIL epoch %d local CRC mismatch\n",k);return 1;}
            last_fill_crc=S.ep[k].crc;
        }
        if (strcmp(S.ep[k].pal,"-")) {
            char pp[256]; snprintf(pp,sizeof pp,"%s/%s",datadir,S.ep[k].pal);
            if(load32(pp,pal,256)!=256){fprintf(stderr,"palette load failed epoch %d\n",k);return 1;}
            uint32_t sum=0; for(int i=0;i<256;i++) sum+=pal[i];
            if(sum!=S.ep[k].pal_crc){fprintf(stderr,"  FAIL epoch %d local palette sum mismatch\n",k);return 1;}
        }
        if (nt!=S.ep[k].records) fprintf(stderr,"  warn epoch %d header tris=%d != descriptor records=%d\n",k,nt,S.ep[k].records);
    }

    // A frame transition is an explicit ARM falling edge followed by the rise below. This keeps scanout off while
    // color/Z are cleared and guarantees zc_clear_start is a new pulse instead of relying on a stale armed state.
    wr32(br, OFF_LPDDR_CTRL, 0);
    if(!br->dry) usleep(1000);
    int rc=0;
    wr32(br, OFF_LPDDR_FBBASE, 0);
    rc|=preclear_fb(br, S.fbwords);
    if (zbuf){
        rc|=preclear_range(br, 0x140000, S.fbwords/2, "Z");
        wr32(br, OFF_LPDDR_CTRL, 0x1);
        printf("[sched] zbuf: armed early for fresh frame Z-preclear\n");
    }
    for (int k=0;k<S.n_epochs;k++){
        char pt[256], pl[256];
        snprintf(pt,sizeof pt,"%s/%s",datadir,S.ep[k].tex);
        snprintf(pl,sizeof pl,"%s/%s",datadir,S.ep[k].list);
        int n=load64(pl,list,STG_MAX);
        if(n<8){fprintf(stderr,"list reload failed epoch %d\n",k);return 1;}
        int nt=(int)(list[0]&0xFFFF);
        int nw=((list[0]&(1ULL<<34)) ? 8 : 7)+nt*9;
        if(n<nw){fprintf(stderr,"list reload truncated epoch %d: loaded %d need %d\n",k,n,nw);return 1;}
        if (S.ep[k].reuse){
            uint32_t rcrc=rd32(br,OFF_TEX_CRC), rbeats=rd32(br,OFF_TEX_BEATS);
            printf("[sched] epoch %d: REUSE resident texture (no upload/fill): crc=0x%08x (exp 0x%08x) beats=%u (exp %d)\n",
                   k, rcrc, S.ep[k].crc, rbeats, S.n_beats);
            if(!br->dry && (rcrc!=S.ep[k].crc || rbeats!=(uint32_t)S.n_beats)){
                fprintf(stderr,"  FAIL epoch %d: resident texture cache disturbed (residency broken)\n",k); rc|=1; }
        } else {
            if(load32(pt,tex,S.tex_words)!=S.tex_words){fprintf(stderr,"tex reload failed epoch %d\n",k);return 1;}
            rc|=upload_and_fill(br,k,tex,S.tex_words,S.ep[k].lpddr,S.ep[k].crc,S.n_beats);
        }
        if (strcmp(S.ep[k].pal,"-")) {
            char pp[256]; snprintf(pp,sizeof pp,"%s/%s",datadir,S.ep[k].pal);
            if(load32(pp,pal,256)!=256){fprintf(stderr,"palette reload failed epoch %d\n",k);return 1;}
            rc|=upload_palette(br,k,pal,S.ep[k].pal_crc);
        }
        rc|=stream_list(br,k,list,nw);
        if (k==0 && !zbuf) wr32(br, OFF_LPDDR_CTRL, 0x1);
        if (zbuf && k==0) rc|=wait_clear_done(br);
        uint32_t drops0 = (zbuf && !br->dry) ? rd32(br,OFF_FRAG_DROPS) : 0;
        rc|=go_await_drain(br,k,S.ep[k].records,(k>0)||prior_scene);
        if (zbuf) rc|=check_no_drops(br,k,drops0);
    }
    rc|=dump_fb(br, dump_fb_path, S.fbwords, S.fbpxw, S.fbh);
    wr32(br, OFF_LPDDR_CTRL, 0x1|0x4);
    printf("[sched] video_src=1 -> HDMI sources the LPDDR line-buffer scanout (%d-epoch composite)\n", S.n_epochs);
    if (present_ms>0 && !br->dry){ printf("[sched] presenting for %d ms\n",present_ms); usleep((useconds_t)present_ms*1000u); }
    return rc;
}

int main(int argc, char**argv){
    unsigned long base=0x40000000UL; int dry=0; char*env=getenv("PS2_BRIDGE_BASE"); if(env) base=strtoul(env,NULL,0);
    const char *epf="sh3_sched_epochs.txt", *datadir=".", *dump_fb_path=NULL, *dump_prefix=NULL, *sequence=NULL;
    int loops=1, frame_ms=100;
    int zbuf=0;   // Ch357 GS_SH3_LPDDR_FB_Z: also preclear the LPDDR Z region + ARM EARLY (so z_rmw's clear_start
                  // Z-preclear overlaps the ~1.6ms texture uploads and completes long before the first GO — no drops).
    for (int i=1;i<argc;i++){ if(!strcmp(argv[i],"--dry-run"))dry=1;
        else if(!strcmp(argv[i],"--base")&&i+1<argc)base=strtoul(argv[++i],NULL,0);
        else if(!strcmp(argv[i],"--datadir")&&i+1<argc)datadir=argv[++i];
        else if(!strcmp(argv[i],"--zbuf"))zbuf=1;
        else if(!strcmp(argv[i],"--dump-fb")){
            dump_fb_path="board_fb.mem";
            if(i+1<argc && argv[i+1][0]!='-') dump_fb_path=argv[++i];
        }
        else if(!strncmp(argv[i],"--dump-fb=",10)){
            dump_fb_path=argv[i]+10;
            if(!*dump_fb_path) dump_fb_path="board_fb.mem";
        }
        else if(!strcmp(argv[i],"--dump-prefix")&&i+1<argc) dump_prefix=argv[++i];
        else if(!strncmp(argv[i],"--dump-prefix=",14)){
            dump_prefix=argv[i]+14;
            if(!*dump_prefix) dump_prefix="board_frame";
        }
        else if(!strcmp(argv[i],"--sequence")&&i+1<argc) sequence=argv[++i];
        else if(!strcmp(argv[i],"--loops")&&i+1<argc) loops=atoi(argv[++i]);
        else if(!strcmp(argv[i],"--frame-ms")&&i+1<argc) frame_ms=atoi(argv[++i]);
        else if(argv[i][0]!='-') epf=argv[i]; }

    br_t br={0,dry}; int fd=-1; void*map=NULL;
    if(!dry){ fd=open("/dev/mem",O_RDWR|O_SYNC); if(fd<0){perror("open /dev/mem (root?)");return 1;}
        map=mmap(NULL,0x2000,PROT_READ|PROT_WRITE,MAP_SHARED,fd,(off_t)base);
        if(map==MAP_FAILED){perror("mmap");close(fd);return 1;} br.base=map; }

    if(!dry){ int g=0; while(!(rd32(&br,OFF_STG_STATUS)&READY_BIT)&&g<20000000)g++; }
    printf("[sched] feeder ready=%d\n", dry?1:((rd32(&br,OFF_STG_STATUS)&READY_BIT)?1:0));

    int rc=0;
    if(sequence){
        char seq_buf[2048], *scenes[MAX_SEQUENCE_SCENES]; int nscenes=0;
        snprintf(seq_buf,sizeof seq_buf,"%s",sequence);
        for(char *p=seq_buf;;){
            if(nscenes==MAX_SEQUENCE_SCENES){ fprintf(stderr,"--sequence supports at most %d epoch tables\n",MAX_SEQUENCE_SCENES); rc=1; break; }
            scenes[nscenes++]=p;
            char *comma=strchr(p,',');
            if(!comma) break;
            *comma=0; p=comma+1;
        }
        if(!rc && (nscenes<2 || !scenes[0][0] || !scenes[nscenes-1][0])){ fprintf(stderr,"--sequence requires two or more non-empty comma-separated epoch tables\n"); rc=1; }
        for(int s=0;!rc && s<nscenes;s++) {
            if(!scenes[s][0]){ fprintf(stderr,"--sequence contains an empty epoch-table entry\n"); rc=1; }
        }
        if(!rc && (!zbuf || loops<1 || frame_ms<0)){ fprintf(stderr,"--sequence requires --zbuf, --loops >= 1, and --frame-ms >= 0\n"); rc=1; }
        if(!rc) {
            for(int n=0;n<loops;n++){
                for(int s=0;s<nscenes;s++){
                    int scene_no=n*nscenes+s, final_scene=(n==loops-1 && s==nscenes-1);
                    char scene_dump[512]; const char *scene_dump_path=NULL;
                    if(dump_prefix){
                        snprintf(scene_dump,sizeof scene_dump,"%s_%03d.mem",dump_prefix,scene_no);
                        scene_dump_path=scene_dump;
                    } else if(final_scene) scene_dump_path=dump_fb_path;
                    rc|=run_scene(&br,scenes[s],datadir,zbuf,scene_dump_path,scene_no>0,
                                  final_scene ? 0 : frame_ms);
                }
            }
        }
    } else {
        rc|=run_scene(&br,epf,datadir,zbuf,dump_fb_path,0,0);
    }

    if(!dry){ munmap(map,0x2000); close(fd); }
    printf("[sched] DONE rc=%d %s\n", rc, rc?"(a gate failed above)":"(all gates passed)");
    return rc?1:0;
}
