ba74bbd5aa
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>
171 lines
10 KiB
C
171 lines
10 KiB
C
// retroDE_ps2 — ps2_sh3_multitex (Ch355 Brick 1 host two-group RUNTIME-STAGING flow)
|
|
//
|
|
// Composites two authentic SH3 draws with DIFFERENT textures/CLUTs (A=idx19562, B=idx89761) into ONE LPDDR
|
|
// framebuffer (320x381) via scene-level texture rebind + staged-list retriggering:
|
|
// preclear ONCE -> fill+CRC tex A -> STREAM list A -> GO A + fresh drain -> REFILL+CRC tex B -> STREAM list B ->
|
|
// GO B + frame_drained high->low->high (anti-stale) -> enable scanout after the 2nd fresh drain.
|
|
// Requires the frame_drained diagnostic bit (0x02C[6]) — bundled since Ch354's fit.
|
|
//
|
|
// Build on the board: gcc -O2 -o ps2_sh3_multitex ps2_sh3_multitex.c
|
|
// Run (after fit+boot): sudo ./ps2_sh3_multitex sh3_mtA_tex_lpddr.mem sh3_mtB_tex_lpddr.mem feeder_sh3_mtA.mem feeder_sh3_mtB.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>
|
|
|
|
#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 [6]frame_drained (STABLE ordered ack)
|
|
#define OFF_LPDDR_BYTES 0x030
|
|
#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
|
|
#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 FRAME_DRAINED 0x40 // 0x02C[6]
|
|
#define IDLE_BIT 0x01
|
|
#define WR_PENDING 0x04 // 0x054[2]
|
|
#define READY_BIT 0x01
|
|
|
|
#define N_TEX 65536 // 512x512 PSMT8 / 4
|
|
#define TEX_BYTES 262144
|
|
#define N_BEATS 8192
|
|
#define STG_MAX 2048
|
|
#define FB_WORDS (320*381) // 121920 ; bytes = 0x77100
|
|
|
|
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); }
|
|
|
|
// NOTE: buffer must exceed the longest banner line; skip any line whose first non-space char isn't a hex digit
|
|
// (comments/blank) — a too-small buffer that splits a long comment must NOT parse the tail as a data word.
|
|
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;
|
|
}
|
|
|
|
// preclear the whole 320x381 FB once (write-probe), poll write_pending, check BRESP
|
|
static int preclear_fb(br_t*b){
|
|
wr32(b, OFF_WRADDR, 0);
|
|
for (unsigned i=0;i<FB_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("[mt] preclear FB %u words (0..0x%x) wr_bresp_errs=%u\n", FB_WORDS, FB_WORDS*4, e);
|
|
return e?-1:0;
|
|
}
|
|
// upload a texture to LPDDR (write-probe), then arm a FRESH cache fill and verify crc/beats/bytes/rd_errs.
|
|
static int upload_and_fill(br_t*b, const char*nm, uint32_t*tex, uint32_t exp_crc){
|
|
wr32(b, OFF_WRADDR, 0x200000u);
|
|
for (int i=0;i<N_TEX;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);
|
|
// FRESH fill: arm, then require fill_done low->high (cache rearm), not a lingering high.
|
|
wr32(b, OFF_TEX_FILL, 0x1);
|
|
if(!b->dry){ int g=0; while((rd32(b,OFF_TEX_FILL)&0x1)&&g<2000)g++; // drops (busy)
|
|
g=0; while(!(rd32(b,OFF_TEX_FILL)&0x1)&&g<400000)g++; } // rises (done)
|
|
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("[mt] tex %s: upload wr_errs=%u ; fill crc=0x%08x (exp 0x%08x) beats=%u (exp %d) bytes=%u (exp %d) rd_errs=%u\n",
|
|
nm, werr, crc, exp_crc, beats, N_BEATS, bytes, TEX_BYTES, rderr);
|
|
if(b->dry) return 0;
|
|
if(werr || crc!=exp_crc || beats!=N_BEATS || bytes!=TEX_BYTES || rderr){ fprintf(stderr," FAIL tex %s fill mismatch\n", nm); 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; }
|
|
// stream a 64-bit staging list over the bridge; report words + the 12-bit address bound.
|
|
static int stream_list(br_t*b, const char*nm, uint64_t*list, int nwords){
|
|
if (nwords > 4095){ fprintf(stderr," FAIL list %s %d words exceeds 12-bit bridge addr\n", nm, nwords); return -1; }
|
|
if (wait_ready(b)) return -1; // feeder in C_READY before staging
|
|
wr32(b, OFF_STG_STATUS, 0); // reset staging write address to 0 (value MUST be 0 — matches ps2_feeder)
|
|
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; // staging accepted
|
|
uint32_t addr=rd32(b,OFF_STG_LO);
|
|
printf("[mt] staged list %s: %d words written, staged_addr=%u (exp %d, < 4096)\n", nm, nwords, addr, nwords);
|
|
if (!b->dry && addr!=(uint32_t)nwords) fprintf(stderr," warn: staged_addr=%u != %d (addressing off)\n", addr, nwords);
|
|
return 0;
|
|
}
|
|
// GO + await a fresh ordered drain. expect_stale: frame_drained is HIGH from a prior scene -> require high->low->high.
|
|
// After the drain, the feeder returns to C_READY (whole scene drained); records_emitted then reads the tri count.
|
|
static int go_await_drain(br_t*b, const char*nm, int exp_records, int expect_stale){
|
|
wr32(b, OFF_GO, 0x1);
|
|
if(b->dry){ printf("[mt] scene %s: GO (dry-run)\n", nm); return 0; }
|
|
int g;
|
|
if (expect_stale){ g=0; while((rd32(b,OFF_LPDDR_STATUS)&FRAME_DRAINED)&&g<40000000)g++; // wait the FALL (fresh render)
|
|
if (rd32(b,OFF_LPDDR_STATUS)&FRAME_DRAINED){ fprintf(stderr," FAIL %s: frame_drained never fell — STALE drain\n", nm); return -1; } }
|
|
g=0; while(!(rd32(b,OFF_LPDDR_STATUS)&FRAME_DRAINED)&&g<40000000)g++; // wait the RISE (drained)
|
|
if (wait_ready(b)) return -1; // feeder back to C_READY
|
|
uint32_t fd=rd32(b,OFF_LPDDR_STATUS)&FRAME_DRAINED, by=rd32(b,OFF_LPDDR_BYTES), recs=rd32(b,OFF_STG_HI);
|
|
printf("[mt] scene %s: GO -> fresh drain frame_drained=%u, records_emitted=%u (exp %d), FB beats written=%u\n",
|
|
nm, fd?1:0, recs, exp_records, by);
|
|
if (fd==0 || by==0){ fprintf(stderr," FAIL %s: empty render (frame_drained=%u beats=%u)\n", nm, fd?1:0, by); return -1; }
|
|
if (!b->dry && (int)recs!=exp_records) fprintf(stderr," warn %s: records=%u != %d\n", nm, recs, exp_records);
|
|
return 0;
|
|
}
|
|
|
|
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 *fa="sh3_mtA_tex_lpddr.mem",*fb="sh3_mtB_tex_lpddr.mem",*la="feeder_sh3_mtA.mem",*lb="feeder_sh3_mtB.mem";
|
|
int ai=1;
|
|
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(argv[i][0]!='-'){ if(ai==1)fa=argv[i]; else if(ai==2)fb=argv[i]; else if(ai==3)la=argv[i]; else if(ai==4)lb=argv[i]; ai++; } }
|
|
static uint32_t texA[N_TEX], texB[N_TEX]; static uint64_t listA[STG_MAX], listB[STG_MAX];
|
|
if (load32(fa,texA,N_TEX)!=N_TEX||load32(fb,texB,N_TEX)!=N_TEX){fprintf(stderr,"texture load failed\n");return 1;}
|
|
int na=load64(la,listA,STG_MAX), nb=load64(lb,listB,STG_MAX);
|
|
if(na<8||nb<8){fprintf(stderr,"list load failed\n");return 1;}
|
|
// trim trailing zero padding to the actual list length (header word0 low16 = ntris; words = 7 + ntris*9)
|
|
int ntA=(int)(listA[0]&0xFFFF), ntB=(int)(listB[0]&0xFFFF); int nwA=7+ntA*9, nwB=7+ntB*9;
|
|
uint32_t crcA=0,crcB=0; for(int i=0;i<N_TEX;i++){crcA+=texA[i];crcB+=texB[i];}
|
|
printf("[mt] texA sum32=0x%08x (%d tris/%d words) texB sum32=0x%08x (%d tris/%d words) bridge 0x%lx%s\n",
|
|
crcA,ntA,nwA, crcB,ntB,nwB, base, dry?" DRY":"");
|
|
|
|
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; }
|
|
|
|
// (1) wait feeder ready (setup + relocated-CLUT bootlet done)
|
|
if(!dry){ int g=0; while(!(rd32(&br,OFF_STG_STATUS)&READY_BIT)&&g<20000000)g++; }
|
|
printf("[mt] feeder ready=%d\n", dry?1:((rd32(&br,OFF_STG_STATUS)&READY_BIT)?1:0));
|
|
int rc=0;
|
|
wr32(&br, OFF_LPDDR_FBBASE, 0); // FB base 0
|
|
// (2) preclear ONCE
|
|
rc|=preclear_fb(&br);
|
|
// (3) scene A: fill tex A, stream list A, arm writer, GO A, fresh drain (A is first -> not stale)
|
|
rc|=upload_and_fill(&br,"A",texA,crcA);
|
|
rc|=stream_list(&br,"A",listA,nwA);
|
|
wr32(&br, OFF_LPDDR_CTRL, 0x1); // arm writer (video_src OFF)
|
|
rc|=go_await_drain(&br,"A",ntA,0);
|
|
// (4) scene B: REFILL tex B (cache rearm), stream list B, GO B, high->low->high fresh drain (stale from A)
|
|
rc|=upload_and_fill(&br,"B",texB,crcB);
|
|
rc|=stream_list(&br,"B",listB,nwB);
|
|
rc|=go_await_drain(&br,"B",ntB,1);
|
|
// (5) enable scanout ONLY after the 2nd fresh drain
|
|
wr32(&br, OFF_LPDDR_CTRL, 0x1|0x4); // arm + video_src
|
|
printf("[mt] video_src=1 -> HDMI sources the LPDDR line-buffer scanout (320x381, A+B composited)\n");
|
|
|
|
if(!dry){ munmap(map,0x2000); close(fd); }
|
|
printf("[mt] DONE rc=%d %s\n", rc, rc?"(a gate failed above)":"(all gates passed)");
|
|
return rc?1:0;
|
|
}
|