Files
psxrecomp/runtime/tests/sljit_offline_diff.c
T
Matthew Stanley b001e03ce4 sljit: block-local GPR register allocation for overlay shards
Replace the memory-backed GPR model (every instruction loaded operands
from cpu->gpr[] and stored results back) with a block-local cache of MIPS
GPRs in host SAVED registers (S2..). Saved registers are callee-saved, so
they survive the read_/write_ icalls a plain load/store emits; the cache is
only flushed/reset at basic-block boundaries and around helper/call icalls
that touch cpu->gpr[] in memory. Prologue cost scales with register
pressure (saveds sized per-fragment from a distinct-GPR popcount).

Invariants: flush dirty->memory before every control transfer and before a
helper/call that reads gpr; reset (drop mappings) at branch-target labels
(after a preceding flush) and after any helper/call that writes gpr; gpr[0]
never cached.

Validation:
- Standalone emitter harness 66/66 (runtime/tests/sljit_emit_test.c).
- Offline differential (runtime/tests/sljit_offline_diff.c, --dump): the
  new emitter is BYTE-IDENTICAL to the pre-regalloc (HEAD) emitter across
  313 overlay functions, run from identical state + call stub. This is the
  primary correctness proof — stronger than a single live diff.
- sljit_sp_probe.c: $sp-balance probes (prologue/epilogue, branch-delay,
  register-sp, calls, loops).

A regalloc bug can only surface as a same-state differential divergence
(shards run live only after passing it), never a silently-wrong shard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:52:30 -07:00

267 lines
15 KiB
C

/* sljit_offline_diff.c — offline JIT-vs-reference-interpreter differential over a
* captured overlay image. For each candidate entry: JIT the fragment with
* overlay_sljit_try_compile, run a reference MIPS interpreter over the SAME bytes
* with the SAME memory + the SAME call stub, and compare the full register file at
* return. Any mismatch is a codegen bug in that function (the live $sp-off-by-0x18
* divergence at 0x12E478 traced to a callee shard corrupting $sp; this finds it
* without the game).
*
* Build:
* gcc -O2 -I runtime/include -I lib/sljit/sljit_src \
* runtime/tests/sljit_offline_diff.c runtime/src/overlay_sljit.c \
* lib/sljit/sljit_src/sljitLir.c -o /tmp/sljit_offline_diff && \
* /tmp/sljit_offline_diff <capture.json> [entry_hex ...]
*/
#include "cpu_state.h"
#include "overlay_sljit.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* decoded capture image (defined/loaded below; forward globals so ref_run can
* read the size). */
static uint8_t* g_img=NULL; static uint32_t g_base=0,g_imgsz=0;
/* ---- fake guest RAM (2 MB) shared by JIT + reference interp --------------- */
static uint8_t g_ram[0x200000];
static uint32_t MASK(uint32_t a){ return a & 0x1FFFFF; } /* fold KSEG + I/O into 2MB */
static uint32_t rd_word(uint32_t a){ uint32_t v; memcpy(&v,&g_ram[MASK(a)&~3u],4); return v; }
static void wr_word(uint32_t a,uint32_t v){ memcpy(&g_ram[MASK(a)&~3u],&v,4); }
static uint16_t rd_half(uint32_t a){ uint16_t v; memcpy(&v,&g_ram[MASK(a)&~1u],2); return v; }
static void wr_half(uint32_t a,uint16_t v){ memcpy(&g_ram[MASK(a)&~1u],&v,2); }
static uint8_t rd_byte(uint32_t a){ return g_ram[MASK(a)]; }
static void wr_byte(uint32_t a,uint8_t v){ g_ram[MASK(a)]=v; }
static void cpu_wire(CPUState*c){ c->read_word=rd_word;c->write_word=wr_word;
c->read_half=rd_half;c->write_half=wr_half;c->read_byte=rd_byte;c->write_byte=wr_byte; }
/* Deterministic, balanced call stub used by BOTH sides: a callee that does not
* touch $sp, sets $v0 to a fixed value, restores nothing (the caller's frame is
* intact), returns normally (no bail). Identical effect on JIT and interp, so any
* divergence isolates the JIT's own codegen — NOT call semantics. */
static int g_calls=0;
int psx_sljit_call(CPUState*cpu,uint32_t target,uint32_t return_pc,int check){
(void)check; g_calls++; cpu->gpr[2]=0x12340000u ^ target; cpu->gpr[31]=return_pc; cpu->pc=0; return 0; }
void psx_sljit_cop2(CPUState*cpu,uint32_t insn){(void)cpu;(void)insn;}
void psx_sljit_memx(CPUState*cpu,uint32_t insn){(void)cpu;(void)insn;}
/* ---- reference MIPS interpreter (parallels dirty_ram_interp for this subset) */
static uint32_t f_op(uint32_t i){return i>>26;} static uint32_t f_rs(uint32_t i){return(i>>21)&31;}
static uint32_t f_rt(uint32_t i){return(i>>16)&31;} static uint32_t f_rd(uint32_t i){return(i>>11)&31;}
static uint32_t f_sh(uint32_t i){return(i>>6)&31;} static uint32_t f_fn(uint32_t i){return i&63;}
static uint32_t f_imm(uint32_t i){return i&0xFFFF;} static int32_t f_simm(uint32_t i){return(int32_t)(int16_t)(i&0xFFFF);}
/* execute one non-control insn on the reference cpu (mirror of emit_one's intent) */
static void ref_one(CPUState*c,uint32_t in){
uint32_t op=f_op(in),rs=f_rs(in),rt=f_rt(in),rd=f_rd(in),sh=f_sh(in),fn=f_fn(in),imm=f_imm(in);
int32_t simm=f_simm(in); uint32_t*g=c->gpr;
#define W(n,v) do{ if(n)g[n]=(uint32_t)(v); }while(0)
if(op==0){
switch(fn){
case 0x00: W(rd,g[rt]<<sh); break; case 0x02: W(rd,g[rt]>>sh); break;
case 0x03: W(rd,(uint32_t)((int32_t)g[rt]>>sh)); break;
case 0x04: W(rd,g[rt]<<(g[rs]&31)); break; case 0x06: W(rd,g[rt]>>(g[rs]&31)); break;
case 0x07: W(rd,(uint32_t)((int32_t)g[rt]>>(g[rs]&31))); break;
case 0x20: case 0x21: W(rd,g[rs]+g[rt]); break; case 0x22: case 0x23: W(rd,g[rs]-g[rt]); break;
case 0x24: W(rd,g[rs]&g[rt]); break; case 0x25: W(rd,g[rs]|g[rt]); break;
case 0x26: W(rd,g[rs]^g[rt]); break; case 0x27: W(rd,~(g[rs]|g[rt])); break;
case 0x2a: W(rd,((int32_t)g[rs]<(int32_t)g[rt])?1:0); break;
case 0x2b: W(rd,(g[rs]<g[rt])?1:0); break;
case 0x10: W(rd,c->hi); break; case 0x12: W(rd,c->lo); break;
case 0x11: c->hi=g[rs]; break; case 0x13: c->lo=g[rs]; break;
case 0x18: { int64_t r=(int64_t)(int32_t)g[rs]*(int64_t)(int32_t)g[rt]; c->lo=(uint32_t)r; c->hi=(uint32_t)((uint64_t)r>>32);} break;
case 0x19: { uint64_t r=(uint64_t)g[rs]*(uint64_t)g[rt]; c->lo=(uint32_t)r; c->hi=(uint32_t)(r>>32);} break;
case 0x1a: { int32_t a=(int32_t)g[rs],b=(int32_t)g[rt]; if(b==0){c->lo=a<0?1:0xFFFFFFFF;c->hi=(uint32_t)a;} else if((uint32_t)a==0x80000000u&&b==-1){c->lo=0x80000000u;c->hi=0;} else {c->lo=(uint32_t)(a/b);c->hi=(uint32_t)(a%b);} } break;
case 0x1b: { uint32_t a=g[rs],b=g[rt]; if(b==0){c->lo=0xFFFFFFFF;c->hi=a;} else {c->lo=a/b;c->hi=a%b;} } break;
case 0x0f: break; /* sync */
default: printf(" ref: unhandled special fn 0x%02X\n",fn); break;
}
} else switch(op){
case 0x08: case 0x09: W(rt,g[rs]+(uint32_t)simm); break;
case 0x0a: W(rt,((int32_t)g[rs]<simm)?1:0); break;
case 0x0b: W(rt,(g[rs]<(uint32_t)simm)?1:0); break;
case 0x0c: W(rt,g[rs]&imm); break; case 0x0d: W(rt,g[rs]|imm); break; case 0x0e: W(rt,g[rs]^imm); break;
case 0x0f: W(rt,imm<<16); break;
case 0x20: W(rt,(uint32_t)(int32_t)(int8_t)c->read_byte(g[rs]+simm)); break;
case 0x21: W(rt,(uint32_t)(int32_t)(int16_t)c->read_half(g[rs]+simm)); break;
case 0x23: W(rt,c->read_word(g[rs]+simm)); break;
case 0x24: W(rt,c->read_byte(g[rs]+simm)); break;
case 0x25: W(rt,c->read_half(g[rs]+simm)); break;
case 0x28: c->write_byte(g[rs]+simm,(uint8_t)g[rt]); break;
case 0x29: c->write_half(g[rs]+simm,(uint16_t)g[rt]); break;
case 0x2b: c->write_word(g[rs]+simm,g[rt]); break;
default: printf(" ref: unhandled op 0x%02X\n",op); break;
}
g[0]=0;
#undef W
}
static int ref_is_ctrl(uint32_t in){ uint32_t op=f_op(in),fn=f_fn(in);
if(op==0) return (fn==0x08||fn==0x09); if(op==1) return 1; return (op>=2&&op<=7); }
/* Run the reference interp from entry until jr $ra returns (matches the shard's
* fragment semantics). bytes/base define the image. Returns at jr $ra. */
static const uint8_t*g_rimg=NULL; static uint32_t g_rbase=0,g_rsz=0;
static int in_img(uint32_t pc){ uint32_t off=(pc&0x1FFFFFFF)-(g_rbase&0x1FFFFFFF); return off+8<=g_rsz && (pc&0x1FFFFFFF)>=(g_rbase&0x1FFFFFFF); }
static uint32_t rimw(uint32_t pc){ return *(const uint32_t*)(g_rimg+((pc&0x1FFFFFFF)-(g_rbase&0x1FFFFFFF))); }
static void ref_run(CPUState*c,const uint8_t*img,uint32_t base,uint32_t entry){
g_rimg=img; g_rbase=base; g_rsz=g_imgsz;
uint32_t pc=entry; int guard=0;
while(guard++<100000){
if(!in_img(pc)){ printf(" ref: pc 0x%08X left image — stop\n",pc); return; }
uint32_t in=rimw(pc);
uint32_t op=f_op(in),fn=f_fn(in),rs=f_rs(in),rt=f_rt(in),rd=f_rd(in);
if(op==0&&fn==0x08){ /* jr rs */
uint32_t tgt=c->gpr[rs];
uint32_t ds=rimw(pc+4); ref_one(c,ds);
if(rs==31) return; /* return */
pc=tgt; continue; /* jr rX tail */
}
if(op==0&&fn==0x09){ /* jalr rd,rs */
uint32_t tgt=c->gpr[rs]; uint32_t link=rd?rd:31; c->gpr[link]=pc+8;
uint32_t ds=rimw(pc+4); ref_one(c,ds);
psx_sljit_call(c,tgt,pc+8,(rd==0||rd==31)); pc+=8; continue;
}
if(op==3){ /* jal */
uint32_t tgt=((pc+4)&0xF0000000)|((in&0x3FFFFFF)<<2); c->gpr[31]=pc+8;
uint32_t ds=rimw(pc+4); ref_one(c,ds);
psx_sljit_call(c,tgt,pc+8,1); pc+=8; continue;
}
if(op==2){ /* j */
uint32_t tgt=((pc+4)&0xF0000000)|((in&0x3FFFFFF)<<2);
uint32_t ds=rimw(pc+4); ref_one(c,ds);
pc=tgt; continue;
}
if(op==1||(op>=4&&op<=7)){ /* branch */
int take=0; int32_t simm=f_simm(in);
if(op==4) take=(c->gpr[rs]==c->gpr[rt]);
else if(op==5) take=(c->gpr[rs]!=c->gpr[rt]);
else if(op==6) take=((int32_t)c->gpr[rs]<=0);
else if(op==7) take=((int32_t)c->gpr[rs]>0);
else { int lz=(rt==0||rt==0x10); if(rt==0x10||rt==0x11) c->gpr[31]=pc+8;
take = lz?((int32_t)c->gpr[rs]<0):((int32_t)c->gpr[rs]>=0); }
uint32_t ds=rimw(pc+4); ref_one(c,ds);
pc = take ? (pc+4+(simm<<2)) : (pc+8); continue;
}
ref_one(c,in); pc+=4;
}
printf(" ref: guard tripped (no return)\n");
}
/* ---- capture loader (minimal JSON: find base64 image covering an addr) ----- */
static int b64dec(const char*s,uint8_t*out,int outcap){
static int8_t T[256]; static int init=0; if(!init){memset(T,-1,256);
const char*A="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for(int i=0;i<64;i++)T[(unsigned char)A[i]]=i; init=1;}
int n=0,val=0,bits=0;
for(;*s&&*s!='"';s++){ if(*s=='='||T[(unsigned char)*s]<0)continue; val=(val<<6)|T[(unsigned char)*s]; bits+=6;
if(bits>=8){bits-=8; if(n<outcap)out[n++]=(uint8_t)(val>>bits);} }
return n;
}
/* crude: pull load_addr/size/bytes_b64 of the FIRST capture covering target */
static int load_capture(const char*path,uint32_t target){
FILE*f=fopen(path,"rb"); if(!f){printf("cannot open %s\n",path);return 0;}
fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET);
char*buf=malloc(sz+1); fread(buf,1,sz,f); buf[sz]=0; fclose(f);
char*p=buf;
while((p=strstr(p,"\"load_addr\""))){
char*la=strchr(p,':'); unsigned long load=strtoul(strchr(la,'"')+1,NULL,16);
char*szp=strstr(p,"\"size\""); unsigned long size=szp?strtoul(strchr(szp,':')+1,NULL,10):0;
char*bp=strstr(p,"\"bytes_b64\""); if(!bp)break;
char*q=strchr(strchr(bp,':')+1,'"')+1;
if(((load&0x1FFFFFFF)<=(target&0x1FFFFFFF))&&((target&0x1FFFFFFF)<(load&0x1FFFFFFF)+size)){
g_img=malloc(size+16); g_imgsz=b64dec(q,g_img,size+16); g_base=(uint32_t)load;
free(buf); printf("capture: base=0x%08X size=0x%lX decoded=0x%X\n",g_base,size,g_imgsz); return 1;
}
p=q;
}
free(buf); printf("no capture covers 0x%08X\n",target); return 0;
}
static uint32_t imgw(uint32_t addr){ return *(const uint32_t*)(g_img+((addr&0x1FFFFFFF)-(g_base&0x1FFFFFFF))); }
/* Test one entry: JIT it, run JIT + reference from identical state, compare. */
static int g_fail=0,g_ok=0,g_decl=0;
static void test_entry(uint32_t entry){
OverlaySljitResult r={0};
overlay_sljit_try_compile(entry,g_img,g_imgsz,g_base,&r);
if(!r.fn){ g_decl++; return; }
/* identical start state for both: a frame at 0x801FE000, sp below it, plus
* pseudo-random but fixed gpr values so divergences surface. */
CPUState cj,ci; memset(&cj,0,sizeof cj); cpu_wire(&cj);
uint32_t sp=0x801FE358;
for(int i=1;i<32;i++) cj.gpr[i]=0x01010101u*i + 0x100;
cj.gpr[29]=sp; cj.gpr[31]=0x80012000u; cj.gpr[16]=0x801FC000u; /* s0 = object ptr */
/* a plausible saved frame so epilogue loads are sane */
wr_word(sp+0x14,0x80012000u); wr_word(sp+0x10,0x801FC000u);
ci=cj; cpu_wire(&ci);
int calls0=g_calls; r.fn(&cj); int jcalls=g_calls-calls0;
calls0=g_calls; ref_run(&ci,g_img,g_base,entry); int icalls=g_calls-calls0;
int diff=0; for(int i=1;i<32;i++) if(cj.gpr[i]!=ci.gpr[i]) diff=1;
if(cj.hi!=ci.hi||cj.lo!=ci.lo) diff=1;
if(diff){
g_fail++;
printf("DIVERGE 0x%08X (%u insns, jit_calls=%d ref_calls=%d):\n",entry,r.insns,jcalls,icalls);
const char*rn[32]={"zero","at","v0","v1","a0","a1","a2","a3","t0","t1","t2","t3","t4","t5","t6","t7",
"s0","s1","s2","s3","s4","s5","s6","s7","t8","t9","k0","k1","gp","sp","fp","ra"};
for(int i=1;i<32;i++) if(cj.gpr[i]!=ci.gpr[i])
printf(" %-3s jit=0x%08X ref=0x%08X (delta %d)\n",rn[i],cj.gpr[i],ci.gpr[i],(int)(cj.gpr[i]-ci.gpr[i]));
if(cj.hi!=ci.hi) printf(" hi jit=0x%08X ref=0x%08X\n",cj.hi,ci.hi);
if(cj.lo!=ci.lo) printf(" lo jit=0x%08X ref=0x%08X\n",cj.lo,ci.lo);
} else g_ok++;
}
/* DUMP mode: JIT a fragment with whichever emitter is linked, run it from a
* fixed deterministic state + the shared balanced call stub, and print the final
* register file on one line. Build this TU once with the new overlay_sljit.c and
* once with the old (HEAD) one, then `diff` the outputs: any line that differs is
* a function my register allocation changed — a definitive, reference-free bisect
* (the old emitter is the validated baseline). */
static void dump_entry(uint32_t entry){
OverlaySljitResult r={0};
overlay_sljit_try_compile(entry,g_img,g_imgsz,g_base,&r);
if(!r.fn){ printf("0x%08X DECLINED\n",entry); return; }
fprintf(stderr,"RUN 0x%08X insns=%u\n",entry,r.insns); fflush(stderr);
CPUState c; memset(&c,0,sizeof c); cpu_wire(&c);
for(int i=1;i<32;i++) c.gpr[i]=0x01010101u*i + 0x100;
c.gpr[29]=0x801FE358; c.gpr[31]=0x80012000u; c.gpr[16]=0x801FC000u;
wr_word(0x801FE358+0x14,0x80012000u); wr_word(0x801FE358+0x10,0x801FC000u);
int calls0=g_calls; r.fn(&c); int calls=g_calls-calls0;
printf("0x%08X insns=%u calls=%d sp=0x%08X",entry,r.insns,calls,c.gpr[29]);
for(int i=1;i<32;i++) printf(" %08X",c.gpr[i]);
printf(" hi=%08X lo=%08X\n",c.hi,c.lo);
}
int main(int argc,char**argv){
if(argc<2){ printf("usage: %s <capture.json> [--dump] [entry_hex ...]\n",argv[0]); return 2; }
int dump=0, ai=2;
if(argc>2 && !strcmp(argv[2],"--dump")){ dump=1; ai=3; }
uint32_t deflt[]={0x8012E478,0x8012BC24,0x8012B424,0x8012B0A4,0x8012D2D0,0x8012C7E4,0x8012BD40,0x8012BEB0};
int n; static uint32_t list[8192];
if(argc>ai){ n=0; for(int i=ai;i<argc&&n<8192;i++) list[n++]=(uint32_t)strtoul(argv[i],NULL,16);
if(!load_capture(argv[1],list[0])) return 1; }
else if(dump){
/* auto-enumerate: every jal target + every jalr-reachable entry in the
* image (broad coverage for the old-vs-new diff). */
if(!load_capture(argv[1],deflt[0])) return 1;
n=0;
for(uint32_t off=0; off+4<=g_imgsz; off+=4){
uint32_t w=*(const uint32_t*)(g_img+off);
if((w>>26)==3){ /* jal */
uint32_t tgt=((g_base&0xF0000000)|((w&0x3FFFFFF)<<2));
if(((tgt&0x1FFFFFFF)>=(g_base&0x1FFFFFFF))&&((tgt&0x1FFFFFFF)<(g_base&0x1FFFFFFF)+g_imgsz)){
int dup=0; for(int k=0;k<n;k++) if(list[k]==tgt){dup=1;break;}
if(!dup&&n<8192) list[n++]=tgt;
}
}
}
/* sort ascending for stable diff */
for(int i=0;i<n;i++)for(int j=i+1;j<n;j++) if(list[j]<list[i]){uint32_t t=list[i];list[i]=list[j];list[j]=t;}
}
else { n=sizeof(deflt)/4; memcpy(list,deflt,sizeof deflt); if(!load_capture(argv[1],list[0])) return 1; }
for(int i=0;i<n;i++){
if(((list[i]&0x1FFFFFFF) < (g_base&0x1FFFFFFF)) || ((list[i]&0x1FFFFFFF) >= (g_base&0x1FFFFFFF)+g_imgsz)) continue;
if(dump) dump_entry(list[i]); else test_entry(list[i]);
}
if(!dump) printf("\noffline_diff: %d ok, %d DIVERGE, %d declined\n",g_ok,g_fail,g_decl);
return g_fail?1:0;
}