From 9b129cd24586b121c193fe33210f44a460a20c19 Mon Sep 17 00:00:00 2001 From: Pegasus Date: Wed, 9 Sep 2026 15:08:10 +0300 Subject: [PATCH] Admit blended textured rectangles in source GPU comparison (cherry picked from commit 82e9f6d52a794c88412015850f331ad68d1a941d) --- docs/tasreplays/pepsiman-gpu-sprite.md | 46 ++++++++ recompiler/CMakeLists.txt | 13 +++ .../include/source_gpu_command_projection.h | 6 +- runtime/src/gpu_sw_renderer.c | 2 +- runtime/tests/test_source_gpu_sprite_blend.c | 100 ++++++++++++++++++ tools/tasreplays/source_sprite_oracle.py | 84 +++++++++++++++ 6 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 docs/tasreplays/pepsiman-gpu-sprite.md create mode 100644 runtime/tests/test_source_gpu_sprite_blend.c create mode 100644 tools/tasreplays/source_sprite_oracle.py diff --git a/docs/tasreplays/pepsiman-gpu-sprite.md b/docs/tasreplays/pepsiman-gpu-sprite.md new file mode 100644 index 00000000..615d4f95 --- /dev/null +++ b/docs/tasreplays/pepsiman-gpu-sprite.md @@ -0,0 +1,46 @@ +# Pepsiman textured rectangle admission + +CLAIM: the limited source-comparison GPU path rejected Pepsiman's GP0 `0x66` +textured semi-transparent rectangle. This is missing source-model coverage, +not evidence of a physical PS1 defect. The correction admits that packet to +the existing sprite timing and software pixel kernels. + +DERIVED-FROM: two fresh native attempts at framework `ef6c888d` matched all +512 RAM pages and master clocks for 1,420 completed source returns. Both then +stopped before return 1,421 on word `66808080` at master clock `803618048`. +The projection's packet and feedback predicates and the source software +renderer excluded `0x66`; its existing blend/mask work and pixel calculations +already express the operation. Original movie and device settings are retained. + +ORACLE: unmodified BizHawk 2.3 `octoshock.dll`, SHA256 +`749d6dd58430d010e46ae97c628e113adad5f420c05c2ada0ad35e58191781c0`. +Primary source at tag 2.3: +[gpu_sprite.cpp](https://github.com/TASEmulators/BizHawk/blob/2.3/psx/octoshock/psx/gpu_sprite.cpp) +and [gpu_common.inc](https://github.com/TASEmulators/BizHawk/blob/2.3/psx/octoshock/psx/gpu_common.inc). +The sprite table specifies four packet words and three feedback words. Its +dispatch and rectangle work retain setup, clipping, aligned blend/mask costs, +CLUT loads and texture-cache misses. The corpus's C11 GPU coverage notes already +classify this model as limited source compatibility. + +REPRODUCE: configure/build the recompiler with testing enabled and run CTest's +`tas_gpu_sprite_blend_O0` and `tas_gpu_sprite_blend_O2`. Each fixture makes +786 checks, including incomplete-packet/debt boundaries and 96 pixel cases. +Run either test executable with `--dump` in a fresh directory to emit full +authored VRAM snapshots. Then run `tools/tasreplays/source_sprite_oracle.py` +with `--core --native-dumps --output `. +It creates a fresh stock core for each case, sends ordinary guest GPU writes, +verifies entry into the authored wait loop, and compares the entire 1 MiB VRAM. +Synthetic ROM/RAM are fixture outputs, never retail playback inputs. + +RESULT: the old production predicate fails the new packet-length regression. +The correction passes all 786 checks at O0 and O2. All 96 full-VRAM cases at +each optimization match stock for three texture formats, four blend modes, +four mask settings, neutral and non-neutral modulation, transparent texels, +and opaque texels inside a blended command. Cache miss/hit work is checked by +the native fixture. The stock pixel oracle makes no clock claim; full title +RAM/clock comparison and Tekken regression remain separate required gates. + +BINDS: the source pixel receipt records the stock DLL, driver, all authored +packet words, synthetic ROM/RAM hashes, wait-loop observation, and both full +VRAM hashes. The title build and suite receipts separately bind the exact +framework, generated code, executable, unchanged movie, disc and BIOS. diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index cf3a70e9..9f8d3a08 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -383,6 +383,19 @@ if(BUILD_TESTING) target_include_directories(host_path_test PRIVATE include) add_test(NAME host_path_test COMMAND host_path_test) + if(CMAKE_C_COMPILER_ID STREQUAL "GNU") + foreach(_mode IN ITEMS O0 O2) + add_executable(tas_gpu_sprite_blend_${_mode} + ../runtime/tests/test_source_gpu_sprite_blend.c + ../runtime/src/gpu_sw_renderer.c + ../runtime/src/gpu_vram_dirty.c) + target_include_directories(tas_gpu_sprite_blend_${_mode} PRIVATE ../runtime/include) + target_compile_options(tas_gpu_sprite_blend_${_mode} PRIVATE -${_mode} -UNDEBUG) + target_link_libraries(tas_gpu_sprite_blend_${_mode} PRIVATE m) + add_test(NAME tas_gpu_sprite_blend_${_mode} COMMAND tas_gpu_sprite_blend_${_mode}) + endforeach() + endif() + add_executable(source_cpu_block_bound_test ../runtime/tests/test_source_cpu_block_bound.c ../runtime/src/psx_icache.c) diff --git a/runtime/include/source_gpu_command_projection.h b/runtime/include/source_gpu_command_projection.h index c7ea4ab2..53c968f8 100644 --- a/runtime/include/source_gpu_command_projection.h +++ b/runtime/include/source_gpu_command_projection.h @@ -62,17 +62,17 @@ static inline unsigned source_gpu_polygon_setup(unsigned opcode) { return opcode&4 ? (opcode&0x10 ? 450:180) : (opcode&0x10 ? 288:0); } static inline int source_gpu_block_supported(unsigned command) { - return command==2 || command==0x80 || command==0x60 || command==0x62 || command==0x64 || command==0x65; + return command==2 || command==0x80 || command==0x60 || command==0x62 || command==0x64 || command==0x65 || command==0x66; } static inline unsigned source_gpu_command_length(uint32_t word) { unsigned command=word>>24; - if(source_gpu_block_supported(command))return command==0x80 || command==0x64 || command==0x65?4:3; + if(source_gpu_block_supported(command))return command==0x80 || command==0x64 || command==0x65 || command==0x66?4:3; if(source_gpu_polygon_supported(command))return 1+3*source_gpu_polygon_stride(command)-!!(command&0x10); return command==0xa0 || command==0xc0 ? 3u : 1u; } static inline unsigned source_gpu_command_feedback_length(uint32_t word) { unsigned command=word>>24; - if(command==2 || (command>=0x60 && command<=0x65))return 3; + if(command==2 || (command>=0x60 && command<=0x66))return 3; return command==0x80 || command==1 || command==0xa0 || command==0xc0 || command==0xe1 || command==0xe2 || command==0xe6 ? 2u : 1u; } diff --git a/runtime/src/gpu_sw_renderer.c b/runtime/src/gpu_sw_renderer.c index 55cf2de1..34bbe763 100644 --- a/runtime/src/gpu_sw_renderer.c +++ b/runtime/src/gpu_sw_renderer.c @@ -436,7 +436,7 @@ int sw_draw_source_block(const SourceGPUBlock *block,int *extra_work) { } return 1; } - if(opcode!=0x60 && opcode!=0x62 && opcode!=0x64 && opcode!=0x65)return 0; + if(opcode!=0x60 && opcode!=0x62 && opcode!=0x64 && opcode!=0x65 && opcode!=0x66)return 0; int textured=!!(opcode&4);unsigned dimensions=words[textured?3:2]; int left=block->x,top=block->y,right=left+(int)(dimensions&1023u),bottom=top+(int)((dimensions>>16)&511u); SourceGPUTexture texture={0};SourceTriangleColors c={0}; diff --git a/runtime/tests/test_source_gpu_sprite_blend.c b/runtime/tests/test_source_gpu_sprite_blend.c new file mode 100644 index 00000000..50ab26d0 --- /dev/null +++ b/runtime/tests/test_source_gpu_sprite_blend.c @@ -0,0 +1,100 @@ +/* The original Octoshock2.3 sprite table admits 0x66 as a four-word packet + * with three-word FIFO feedback. DrawSprite charges 16 + clipped width + + * aligned halfword blend/mask work, plus the renderer's texture-cache work. + * These are source compatibility checks, not PS1 hardware timing claims. */ +#include "source_gpu_command_projection.h" +#include "gpu_sw_renderer.h" +#include +#include + +static unsigned checks; +static int dump_vram; +static uint16_t vram[1024*512]; +/* Enhancement-only integration is absent from this native sprite fixture. + * Fail if the tested path unexpectedly enters it. */ +int g_ws_bd_stretch_on, g_ws_bd_stretch_pct; +int psx_ws_prim_in_backdrop(void) { abort(); } +static void check(int okay,const char *message) { + checks++; + if(!okay){fprintf(stderr,"FAIL: %s\n",message);exit(1);} +} + +/* Per-channel reference arithmetic derived from the source PlotPixel and + * ModTexel contracts. Tested separately from the production pixel function. */ +static uint16_t expected_pixel(uint16_t back,uint16_t texel,unsigned color, + unsigned blend,unsigned mask) { + if(!texel || ((mask&2) && (back&0x8000)))return back; + unsigned pixel=texel&0x8000; + for(unsigned c=0;c<3;c++) { + int fore=(((texel>>(5*c))&31)*((color>>(8*c))&255))>>7; + if(fore>31)fore=31; + int bg=(back>>(5*c))&31; + if(texel&0x8000) { + if(blend==0)fore=(bg+fore)/2; + else if(blend==1)fore=bg+fore; + else if(blend==2)fore=bg-fore; + else fore=bg+fore/4; + } + if(fore<0)fore=0;if(fore>31)fore=31; + pixel|=(unsigned)fore<<(5*c); + } + return (uint16_t)(pixel|((mask&1)?0x8000:0)); +} + +static void pixels(unsigned mode,unsigned blend,unsigned mask,unsigned color) { + memset(vram,0,sizeof(vram)); + const uint16_t texels[]={0,0x801f,0x03e0,0xfc00}; + uint16_t original[4]; + for(unsigned i=0;i<4;i++) { + vram[300*1024+i]=texels[i]; + original[i]=vram[4*1024+4+i]=(uint16_t)(0x1234|((i&1)?0x8000:0)); + } + if(mode==0)vram[256*1024]=0x3210; + else if(mode==1){vram[256*1024]=0x0100;vram[256*1024+1]=0x0302;} + else memcpy(vram+256*1024,texels,sizeof(texels)); + sw_renderer_init(vram);sw_source_texture_control(0,0); + sw_set_mask_bits(mask&1,!!(mask&2)); + sw_set_semi_transparency(1,blend); + uint32_t words[]={0x66000000|color,0x00040004,300u<<22,0x00010004}; + SourceGPUBlock b={0};b.words=words;b.x=4;b.y=4;b.clip_right=1023;b.clip_bottom=511; + b.draw_mode=16|(mode<<7)|(blend<<5); + int extra=-1; + check(sw_draw_source_block(&b,&extra),"0x66 software renderer admission"); + check(extra==(mode==0?20:mode==1?260:4),"full CLUT/texture cache charge, including transparent or masked pixels"); + for(unsigned i=0;i<4;i++)check(vram[4*1024+4+i]==expected_pixel(original[i],texels[i],color,blend,mask), + "textured blend / opaque / transparent / destination mask pixel"); + check(vram[4*1024+3]==0 && vram[4*1024+8]==0,"rectangle does not write neighbors"); + if(dump_vram) { + char name[80];snprintf(name,sizeof(name),"m%u-b%u-k%u-c%06x.bin",mode,blend,mask,color); + FILE *f=fopen(name,"wb");check(f!=NULL,"open authored VRAM snapshot"); + check(fwrite(vram,1,sizeof(vram),f)==sizeof(vram) && fclose(f)==0,"write full authored VRAM snapshot"); + } + check(sw_draw_source_block(&b,&extra) && extra==0,"repeat uses retained texture/CLUT cache"); +} + +int main(int argc,char **argv) { + dump_vram=argc==2 && strcmp(argv[1],"--dump")==0; + const uint32_t words[]={0x66808080,0x00040004,300u<<22,0x00010004}; + check(source_gpu_command_length(words[0])==4,"0x66 packet length"); + check(source_gpu_command_feedback_length(words[0])==3,"0x66 FIFO feedback length"); + SourceGPUCommandProjection s;source_gpu_command_cold(&s); + s.clip_x1=1023;s.clip_y1=511;s.budget=100; + for(unsigned i=0;i<3;i++) { + check(source_gpu_command_write(&s,words[i]),"incomplete 0x66 packet is admitted"); + check(s.dispatch.kind==SOURCE_GPU_DISPATCH_NONE && s.budget==100,"no partial sprite dispatch or charge"); + } + check(!source_gpu_command_ready(&s),"three-word feedback blocks DMA before fourth word"); + check(source_gpu_command_write(&s,words[3]),"complete 0x66 packet dispatches"); + check(s.dispatch.kind==SOURCE_GPU_DISPATCH_COMMAND && s.dispatch.count==4 && !s.count && s.budget==76, + "four words dispatch once, with two setup clocks plus 22 sprite clocks"); + s.clip_x0=5;s.clip_x1=6;check(source_gpu_command_block_cost(&s,words)==20,"clipped odd-start blend work"); + s.clip_x0=0;s.clip_x1=1023;s.budget=-2; + for(unsigned i=0;i<4;i++)check(source_gpu_command_write(&s,words[i]),"negative-credit packet queues"); + check(s.count==4 && s.dispatch.kind==SOURCE_GPU_DISPATCH_NONE,"negative credit cannot render"); + check(source_gpu_command_update(&s,1) && s.budget==-24 && s.count==0,"exact zero credit releases sprite"); + for(unsigned mode=0;mode<3;mode++)for(unsigned blend=0;blend<4;blend++)for(unsigned mask=0;mask<4;mask++) { + pixels(mode,blend,mask,0x808080); + pixels(mode,blend,mask,0xe3942b); + } + printf("source_gpu_sprite_blend: %u checks passed\n",checks); +} diff --git a/tools/tasreplays/source_sprite_oracle.py b/tools/tasreplays/source_sprite_oracle.py new file mode 100644 index 00000000..63e4f338 --- /dev/null +++ b/tools/tasreplays/source_sprite_oracle.py @@ -0,0 +1,84 @@ +"""Fresh stock Octoshock2.3, authored guest GPU commands, full VRAM comparison. + +This fixture observes pixels only. It does not import runtime state or use the +patched observer DLL, and makes no CPU/GPU clock or hardware accuracy claim. +""" +from pathlib import Path +import argparse, ctypes as c, hashlib, json, struct +p=argparse.ArgumentParser(description=__doc__) +for name in ['core','native-dumps','output']: p.add_argument('--'+name,type=Path,required=True) +a=p.parse_args();core=a.core.resolve(strict=True);native_root=a.native_dumps.resolve(strict=True) +D=a.output.resolve();D.mkdir(exist_ok=False) +sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest() +assert sha(core)=='749d6dd58430d010e46ae97c628e113adad5f420c05c2ada0ad35e58191781c0' +dll=c.CDLL(str(core));ptr=c.c_void_p +def bind(name,args): + fn=getattr(dll,name);fn.restype=c.c_int;fn.argtypes=args;return fn +create=bind('shock_Create',[c.POINTER(ptr),c.c_int,ptr]);power=bind('shock_PowerOn',[ptr]) +step=bind('shock_Step',[ptr,c.c_int]);destroy=bind('shock_Destroy',[ptr]) +getmem=bind('shock_GetMemData',[ptr,c.POINTER(ptr),c.POINTER(c.c_int),c.c_int]) +trace_type=c.CFUNCTYPE(None,ptr,c.c_uint32,c.c_uint32,c.c_char_p) +settrace=bind('shock_SetTraceCallback',[ptr,ptr,ptr]) +def memory(console,kind,wanted): + address=ptr();length=c.c_int() + assert getmem(console,c.byref(address),c.byref(length),kind)==0 and length.value==wanted + return address.value +def words_into(data,offset,words): + for i,w in enumerate(words):struct.pack_into('>16),0x35290000|(word&65535),0xad091810] + # A marker after all CPU writes proves that the authored route executed. + program += [0x3c08a000,0x3c09feed,0x35291234,0xad090100,0x3c09bfc0,0x35290200,0x01200008,0] + rom=bytearray(512*1024);ram=bytearray(0x3000) + words_into(rom,0,[0x3c0bfffe,0x356b0130,0x240c0800,0xad6c0000,0x40806000, + 0x3c19a000,0x37390500,0x03200008,0]) + words_into(rom,0x200,[0x0bf00080,0]);words_into(ram,0x500,program) + (D/(name+'-rom.bin')).write_bytes(rom);(D/(name+'-ram.bin')).write_bytes(ram) + buffer=(c.c_ubyte*len(rom)).from_buffer_copy(rom);console=ptr() + assert create(c.byref(console),1,buffer)==0 + try: + assert power(console)==0 + address=memory(console,0,2097152);c.memmove(address,bytes(ram),len(ram)) + reached=[];trace_calls=[0] + @trace_type + def trace(_,pc,opcode,message): + trace_calls[0]+=1 + if pc==0xbfc00200: + reached.append(pc);settrace(console,None,None) + elif trace_calls[0]>1024:settrace(console,None,None) + assert settrace(console,None,trace)==0 + assert step(console,0)==0 + assert c.c_uint32.from_address(address+0x100).value==0xfeed1234 + assert reached==[0xbfc00200], 'authored program did not reach the BIOS wait loop' + raw=c.string_at(memory(console,3,1048576),1048576) + output=D/(name+'.bin');output.write_bytes(raw) + native=native_root/output.name + actual=native.read_bytes() + assert len(actual)==len(raw), 'native VRAM must be exactly 1 MiB' + changed=[i//2 for i in range(0,len(raw),2) if raw[i:i+2]!=actual[i:i+2]] + report['cases'].append({'case':name,'gp0':gpu,'wait_loop_pc':reached[0],'trace_calls':trace_calls[0],'source_sha256':sha(output), + 'native_sha256':sha(native),'changed_words':len(changed),'first_words':changed[:16], + 'authored_rom_sha256':sha(D/(name+'-rom.bin')),'authored_ram_sha256':sha(D/(name+'-ram.bin'))}) + (D/'receipt.json').write_text(json.dumps(report,indent=2)) + print(name,'PASS' if not changed else 'FAIL',len(changed),flush=True) + if changed:raise AssertionError('stock/native VRAM mismatch') + finally:destroy(console) +report['status']='pass';(D/'receipt.json').write_text(json.dumps(report,indent=2))