Files
retroDE_ps2/tools/ps2_sh3_sched.c
thejayman77 ba74bbd5aa Snapshot: fog implementation + fidelity tooling baseline (pre bilinear-clamp fix)
Per-vertex GS fog end-to-end (gs_stub emit incl. persp_emit5, gs_prim_list_feeder
XYZ2->XYZF2 on PRIM.FGE, gs_make_sh3_scheduler_fixture.py F/FGE packing), new fog
TBs, fidelity attribution tooling. Functional baseline before removing the dead
bilinear lerp8 clamps (Codex: 161-node comb loop -> -0.042ns setup fail).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 19:56:46 -04:00

463 lines
27 KiB
C

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