Admit blended textured rectangles in source GPU comparison

(cherry picked from commit 82e9f6d52a)
This commit is contained in:
Pegasus
2026-09-09 15:08:10 +03:00
committed by Alexandros Mandravillis
parent 51e63d0130
commit 9b129cd245
6 changed files with 247 additions and 4 deletions
+46
View File
@@ -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 <stock2.3 DLL> --native-dumps <directory> --output <fresh directory>`.
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.
+13
View File
@@ -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)
@@ -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;
}
+1 -1
View File
@@ -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};
@@ -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 <stdio.h>
#include <stdlib.h>
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);
}
+84
View File
@@ -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('<I',data,offset+4*i,w)
def upload(x,y,values):
assert len(values)%2==0
return [0xa0000000,x|(y<<16),len(values)|(1<<16)]+[values[i]|(values[i+1]<<16) for i in range(0,len(values),2)]
report={'core':str(core),'core_sha256':sha(core),'driver_sha256':sha(Path(__file__)),
'native_dumps':str(native_root),
'scope':'stock Octoshock2.3 full VRAM equality after ordinary authored GP0 commands; no timing claim','cases':[]}
texels=[0,0x801f,0x03e0,0xfc00]
for mode in range(3):
for blend in range(4):
for mask in range(4):
for color in [0x808080,0xe3942b]:
name=f'm{mode}-b{blend}-k{mask}-c{color:06x}'
# Direct fixture inputs are the same explicit uploads on both sides.
gpu=upload(0,300,texels)+upload(4,4,[0x1234,0x9234,0x1234,0x9234])
gpu+=upload(0,256,[0x3210,0] if mode==0 else [0x0100,0x0302] if mode==1 else texels)
gpu += [0xe3000000,0xe407ffff,0xe1000000|16|(mode<<7)|(blend<<5),0xe6000000|mask,
0x66000000|color,0x00040004,300<<22,0x00010004]
program=[0x3c081f80] # t0 = GPU register page
for word in gpu:
program += [0x3c090000|(word>>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))