mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 10:59:16 -04:00
Admit blended textured rectangles in source GPU comparison
(cherry picked from commit 82e9f6d52a)
This commit is contained in:
committed by
Alexandros Mandravillis
parent
51e63d0130
commit
9b129cd245
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user