// 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 [--datadir DIR] // Optional framebuffer readback: // sudo ./ps2_sh3_sched --zbuf sh3_zsched_epochs.txt --dump-fb sh3_zsched_board_fb.mem #include #include #include #include #include #include #include #include #include #include #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(kn_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;idry){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;idry){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>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;i0){ int x=i%fbpxw, y=i/fbpxw; if(minx<0 || xmaxx) maxx=x; if(miny<0 || ymaxy) 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;kdry) 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;kdry && (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= 1, and --frame-ms >= 0\n"); rc=1; } if(!rc) { for(int n=0;n0, 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; }