Prepare the Pepsiman source-bound TAS candidate and Japanese BIOS profile

(cherry picked from commit 6802d20103)
This commit is contained in:
Pegasus
2026-09-09 13:12:21 +03:00
committed by Alexandros Mandravillis
parent 0f3a34f403
commit f6ca922016
6 changed files with 3132 additions and 5 deletions
+76
View File
@@ -0,0 +1,76 @@
# Retail SCPH-5500 (Japan), SHA256 9c0421858e217805f4abe18698afea8d5aa36ff0727eb8484944e00eb5e7eadb.
# ROM [0x80,0x18000) equals the qualified SCPH1001 kernel bytes.
# The reset-vector area has 24 differing bytes; compile those target bytes.
# The seed list retains matching 64-byte kernel entries plus the explicit
# architectural reset vector. Shell code differs; PSX_SHELLWIN_INTERP=ON is
# required until its own native admission is qualified. No firmware is shipped.
[program]
name = "Sony SCPH-5500 BIOS (Japan, v3.0 09/09/96)"
id = "SCPH-5500"
rom = "bios/SCPH5500.BIN"
load_address = "0xBFC00000"
entry_pc = "0xBFC00000"
text_size = "0x80000"
[recompiler]
seeds = "recompiler/seeds/phase2_ghidra_seeds_SCPH5500.json"
out_dir = "generated"
out_stem = "SCPH5500"
strict = true
# The BIOS's boot-time bulk copies of CODE out of ROM into RAM. Single source
# of truth for every relocation window in the recompiler AND in the C it
# emits (the generated dispatch's normalize(), the kernel-bless guard, the
# game/shell-overlap tests). Each entry is a CLAIM that the copy is
# byte-verbatim; the runtime's kernel-bless memcmp enforces it for the
# bless window. All bounds are [lo, hi) with hi EXCLUSIVE.
#
# dispatch_key = "ram": functions in the window are keyed by RAM address
# (normalize folds ROM -> RAM).
# dispatch_key = "rom": functions keep their ROM key (normalize folds the
# RAM alias back to ROM).
[recompiler.address_model]
normalize_mask = "0x1FFFFFFF"
[[recompiler.address_model.copy]]
name = "Kernel Part 2"
rom_lo = "0x1FC10000"
rom_hi = "0x1FC18000"
ram_lo = "0x00000500"
runtime_base = "0x00000500" # KUSEG — the kernel executes uncached-region code cached at 0x500
dispatch_key = "ram"
kernel_bless = true
[[recompiler.address_model.copy]]
name = "Shell"
rom_lo = "0x1FC18000"
rom_hi = "0x1FC43000"
ram_lo = "0x00030000"
runtime_base = "0x80030000" # KSEG0 — LoadRunShell jumps to 0x80030000
dispatch_key = "rom"
kernel_bless = false
# NOTE (do not "fix" without evidence): runtime/src/bios_hle.c documents the
# actual LoadRunShell copy as 0x67FF0 bytes (ROM 0xBFC18000 -> RAM 0x30000..
# 0x97FF0). Every pre-profile window constant covered only 0x2B000 of that,
# and dirty_ram_interp.c tracks live RAM in [0x5B000,0x8F000) as an open
# class-B bug. Widening this window is a REAL BEHAVIOR CHANGE requiring its
# own evidence-backed commit — it is not a parameterization cleanup.
# Kernel-RAM PCs the BIOS overwrites with 4-instruction dispatch stubs at
# runtime (CLAUDE.md rule 18; docs/dynamic_handler_install.md). The emitter
# plants a dirty-check hook at these PCs so the installed stub executes.
[[recompiler.install_slots]]
ram_addr = "0x00000CF0" # SIO data-byte handler dispatch slot
# Per-image anchors couriered into the generated C (psx_bios_image) for the
# runtime HLE tier. Values are FACTS about this image, not tunables: the
# shell entry is where LoadRunShell (ROM 0xBFC06FF0) jumps after its copy;
# deliver_event_ret is the instruction after the kernel DeliverEvent loop's
# `jalr v0` (kernel RAM 0x1718), the $ra a delivered event callback returns
# through. Omit a key on a BIOS that has no such anchor — the runtime then
# treats that HLE feature as structurally unavailable instead of firing on a
# wrong address.
[recompiler.runtime_exports]
shell_entry_phys = "0x00030000"
deliver_event_ret = "0x80001720"
+3
View File
@@ -839,6 +839,9 @@ if(BUILD_TESTING)
add_test(NAME tas_replay_recipe
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_CURRENT_SOURCE_DIR}/../tools/tasreplays/test_tekken3.py")
add_test(NAME tas_pepsiman_admission
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_CURRENT_SOURCE_DIR}/../tools/tasreplays/test_pepsiman.py")
# The Python driver compiles test_source_ram_page_probe.c at O0/O2.
add_test(NAME tas_ram_long_route
COMMAND ${Python3_EXECUTABLE}
File diff suppressed because it is too large Load Diff
+8 -5
View File
@@ -2,16 +2,19 @@ cmake_minimum_required(VERSION 3.24)
project(Tekken3Tas LANGUAGES C CXX)
get_filename_component(PSXRECOMP_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
set(TAS_PROJECT_DIR "${PSXRECOMP_ROOT}/build/tekken3" CACHE PATH "Prepared title directory")
set(TAS_GAME_STEM "SLUS_004.02" CACHE STRING "Prepared boot-program filename stem")
set(TAS_EXE_NAME "Tekken3-TAS" CACHE STRING "Native player filename")
set(TAS_WINDOW_TITLE "Tekken 3 - Spikestuff TAS" CACHE STRING "Native player title")
set(PSXRECOMP_BIOS_STEMS "SCPH1001" CACHE STRING "BIOS")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPSX_ENABLE_BLOCK_CYCLES=1")
include("${PSXRECOMP_ROOT}/runtime/runtime.cmake")
file(GLOB TAS_GAME_SHARDS CONFIGURE_DEPENDS "${TAS_PROJECT_DIR}/generated/SLUS_004.02_full_*.c")
file(GLOB TAS_GAME_SHARDS CONFIGURE_DEPENDS "${TAS_PROJECT_DIR}/generated/${TAS_GAME_STEM}_full_*.c")
if(NOT TAS_GAME_SHARDS)
message(FATAL_ERROR "Run tools/tasreplays/tekken3.py setup first")
message(FATAL_ERROR "Generate the selected title before configuring its player")
endif()
psxrecomp_add_runtime_target(psx-runtime
EXE_NAME Tekken3-TAS
WINDOW_TITLE "Tekken 3 - Spikestuff TAS"
EXE_NAME "${TAS_EXE_NAME}"
WINDOW_TITLE "${TAS_WINDOW_TITLE}"
DEFAULT_GAME_CONFIG_PATH "${TAS_PROJECT_DIR}/game.toml"
GAME_GENERATED_FULL_C ${TAS_GAME_SHARDS}
GAME_GENERATED_DISPATCH_C "${TAS_PROJECT_DIR}/generated/SLUS_004.02_dispatch.c")
GAME_GENERATED_DISPATCH_C "${TAS_PROJECT_DIR}/generated/${TAS_GAME_STEM}_dispatch.c")
+233
View File
@@ -0,0 +1,233 @@
"""Prepare and compare the original Pepsiman 6044 movie with owned inputs.
The native profile remains experimental until its complete source comparison
passes. Stock-source completion must precede setup. No reference is generated
from a candidate, and no movie input is changed to correct a divergence.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import struct
import subprocess
import sys
import bk2_intake
from bk2_to_psxrti import convert
import tekken3
from compare_ram_pages import read_pages
HERE=Path(__file__).resolve().parent
ROOT=HERE.parent.parent
FRAMES=71806
MOVIE_SHA='de3fe8065a8adf068adf2cdbc2732b754df8e26aa08dc3a6f332c7dca7e5d578'
WORDS_SHA='025f465a3ff008c4bf70f9cef46ebae04c4f0c726b38a79b88bbd90e064da68d'
BIOS_SHA='9c0421858e217805f4abe18698afea8d5aa36ff0727eb8484944e00eb5e7eadb'
EXE_SHA='96960e14406b58dd71c6717bb374537313ff7b64215330671c989ba2926f3718'
TRACKS=[
(124164432,'6452e78fa5d65abec2a5084afa28878bf478c9c63d6ba739b6b542cad366eded'),
(10936800,'6cd94daf5173b9f84469683fd0270621a8ca2fbd614a6d8af1e75375c36f2531'),
(10936800,'2255e58929497be30bded16a81e3548613b89ff7b57501219eca15e9bf557600'),
(10936800,'8bfcd406e12e3cea8cd83f232272ef2a81581bafc0c98ffa2ed55608c6d2f8b9'),
(10936800,'2beda20a27697e13bf84921cbca5d7363c0455ce95400ffaacd8d799ad3f7121'),
(10936800,'bb784ddc940277585042209a07368995cc32470b9fa42e3c5245b199c5243e7d'),
(5630688,'fcc2bbb43914bac5b3dfa79e92baaa0a94d21fc57a5d7c4b5b144643cd5a75c4d'),
(32149488,'19c7d810996b3b56ce08ea46adcebcb6a57695b140d2d037fca409c2e409d196')]
digest=tekken3.digest
write=tekken3.write_json
command=tekken3.command
def cue_files(text):
lines=[line.strip() for line in text.splitlines() if line.strip()]
files=[]
for track in range(1,9):
if not lines: raise ValueError('missing CUE track')
match=re.fullmatch(r'FILE "([^"\r\n]+)" BINARY',lines.pop(0))
if not match: raise ValueError('invalid CUE file line')
name=match[1]
if Path(name).is_absolute() or '..' in Path(name).parts:
raise ValueError('CUE companion must remain below the CUE directory')
expected=[f'TRACK {track:02d} '+('MODE2/2352' if track==1 else 'AUDIO')]
expected+=['INDEX 01 00:00:00'] if track==1 else ['INDEX 00 00:00:00','INDEX 01 00:02:00']
if lines[:len(expected)]!=expected: raise ValueError('unqualified track order, mode or pregap')
del lines[:len(expected)]
files.append(name)
if lines or len(set(files))!=8: raise ValueError('extra or repeated disc companions')
return files
def media(cue,bios):
tekken3.require_hash(bios,BIOS_SHA)
tracks=[(cue.parent/name).resolve(strict=True) for name in cue_files(cue.read_text())]
for path,(length,sha) in zip(tracks,TRACKS):
if not path.is_relative_to(cue.parent): raise ValueError('disc companion escapes root')
if path.stat().st_size!=length: raise ValueError('wrong track size: '+str(path))
tekken3.require_hash(path,sha)
return tracks
def boot_program(track):
sys.path.insert(0,str(ROOT/'tools'))
from prepare_disc import parse_root_entries
with track.open('rb') as f:
def user(lba):
f.seek(lba*2352)
sector=f.read(2352)
if len(sector)!=2352 or sector[:12]!=b'\0'+b'\xff'*10+b'\0':
raise ValueError('invalid raw data sector')
return sector[24:2072]
def span(lba,size):
return b''.join(user(lba+i) for i in range((size+2047)//2048))[:size]
pvd=user(16)
if pvd[1:6]!=b'CD001': raise ValueError('missing ISO9660')
tree=parse_root_entries(span(struct.unpack_from('<I',pvd,158)[0],struct.unpack_from('<I',pvd,166)[0]))
cnf=span(*tree['SYSTEM.CNF'])
if b'BOOT = cdrom:SLPS_017.62;1' not in cnf: raise ValueError('wrong boot program')
data=span(*tree['SLPS_017.62'])
if hashlib.sha256(data).hexdigest()!=EXE_SHA: raise ValueError('wrong boot executable')
return data
def admit_source_control(path):
complete=json.loads((path/'complete.json').read_text())
end=json.loads((path/'exit.json').read_text())
semantic=json.loads((path/'semantic-review.json').read_text())
manifest=json.loads((path/'manifest.json').read_text())
if (complete.get('frame'),complete.get('original_inputs'),complete.get('full_movie'))!=(FRAMES,FRAMES,True):
raise ValueError('full original source control required')
if end.get('exit_code')!=0 or end.get('stop_reason') is not None:
raise ValueError('source did not complete cleanly')
if semantic.get('completion_observed') is not True or semantic.get('frame')!=FRAMES:
raise ValueError('source completion needs semantic review')
evidence=path/semantic['image']
if not evidence.resolve().is_relative_to(path) or digest(evidence)!=semantic['image_sha256']:
raise ValueError('source completion image identity changed')
if not any(x['sha256']==MOVIE_SHA for x in manifest['bindings']):
raise ValueError('source used a different movie')
with (path/'ram-frames.tsv').open() as rows:
if rows.readline().strip()!='frame\tlag_count\tpc\tram_sha256':
raise ValueError('unrecognized stock RAM capture')
count=0
for expected,row in enumerate(rows):
columns=row.rstrip('\r\n').split('\t')
if len(columns)!=4 or int(columns[0])!=expected or not re.fullmatch('[0-9A-Fa-f]{64}',columns[3]):
raise ValueError('incomplete or discontinuous stock RAM capture')
count+=1
if count!=FRAMES+1: raise ValueError('stock control misses original input boundaries')
return {name:{'path':str(path/name),'sha256':digest(path/name)} for name in
['complete.json','exit.json','semantic-review.json','manifest.json','ram-frames.tsv']}
def setup(args):
if os.name!='nt': raise ValueError('Windows UCRT build required for this candidate')
if subprocess.check_output(['git','-C',str(ROOT),'status','--porcelain'],text=True).strip():
raise ValueError('commit source changes before building')
for tool in ['gcc','g++','cmake','ninja']:
if not shutil.which(tool): raise ValueError('missing tool: '+tool)
macros=subprocess.check_output(['gcc','-dM','-E','-include','_mingw.h','-'],input='',text=True)
if not re.search(r'^#define _UCRT\b',macros,re.M): raise ValueError('UCRT compiler required')
control=admit_source_control(args.source_control.resolve(strict=True))
disc,bios,movie=[p.resolve(strict=True) for p in (args.disc,args.bios,args.movie)]
tracks=media(disc,bios)
route,intake=bk2_intake.inspect(movie.read_bytes())
if (intake['movie_sha256'],intake['frame_count'],intake['pad_words_le_sha256'])!=(MOVIE_SHA,FRAMES,WORDS_SHA):
raise ValueError('wrong Pepsiman movie')
project=args.project.resolve()
if project==ROOT or project.is_relative_to(ROOT): raise ValueError('put generated title/build output outside source')
project.mkdir(parents=True,exist_ok=False)
data=boot_program(tracks[0])
cache=args.cache.resolve()/EXE_SHA
cache.mkdir(parents=True,exist_ok=True)
exe=cache/'SLPS_017.62'
if exe.exists(): tekken3.require_hash(exe,EXE_SHA)
else: exe.write_bytes(data)
tape=project/'octoshock-cold-random.psxrng'
command([sys.executable,HERE/'external/source_random_tape.py',tape],project/'random-tape.log')
tekken3.require_hash(tape,tekken3.TAPE_SHA)
payload,receipt=convert(movie.read_bytes())
(project/'input.psxrti').write_bytes(payload)
write(project/'input.json',receipt)
tools=project/'tools'
common=['-G','Ninja','-DCMAKE_BUILD_TYPE=Release','-DCMAKE_C_COMPILER=gcc','-DCMAKE_CXX_COMPILER=g++']
command(['cmake','-S',ROOT/'recompiler','-B',tools,*common,'-DBUILD_TESTING=ON',
'-DPSXRECOMP_ENABLE_CHD=ON','-DPython3_EXECUTABLE='+sys.executable],project/'configure-tools.log')
command(['cmake','--build',tools,'--parallel',str(args.jobs)],project/'build-tools.log')
command(['ctest','--test-dir',tools,'--output-on-failure','-j',str(args.jobs)],project/'test-tools.log')
command([tools/'psxrecomp-toml.exe',exe,'--output',project/'census.toml','--seeds',project/'seeds.txt'],project/'census.log')
q=lambda p:json.dumps(p.as_posix())
game=project/'game.toml'
game.write_text(f'''[game]
name = "Pepsiman TAS"
id = "SLPS-01762"
exe = {q(exe)}
load_address = "0x80010000"
entry_pc = "0x80042C58"
text_size = "0x85800"
stack_base = "0x801FFFF0"
[recompiler]
seeds = {q(project/'seeds.txt')}
bios_config = {q(ROOT/'bios/SCPH5500.toml')}
strict = true
out_dir = {q(project/'generated')}
[runtime]
window_title = "Pepsiman - original TAS"
bios_hle = false
[video]
renderer = "software"
''',encoding='utf8')
command([tools/'psxrecomp-bios.exe','--config',ROOT/'bios/SCPH5500.toml','--rom',bios,
'--out-dir',ROOT/'generated'],project/'generate-bios.log')
command([tools/'psxrecomp-game.exe','--config',game],project/'generate-game.log')
native=project/'native'
command(['cmake','-S',HERE,'-B',native,*common,'-DTAS_PROJECT_DIR='+str(project),
'-DTAS_GAME_STEM=SLPS_017.62','-DTAS_EXE_NAME=Pepsiman-TAS','-DTAS_WINDOW_TITLE=Pepsiman TAS',
'-DPSXRECOMP_BIOS_STEMS=SCPH5500','-DPSX_SHELLWIN_INTERP=ON',
'-DPSX_RECOMP_UI=OFF','-DPSX_NETPLAY=OFF','-DPSX_REWIND=OFF','-DPSX_SETUP_WIZARD=OFF',
'-DPSX_DEBUG_TOOLS=ON','-DPSX_ENABLE_VULKAN=OFF','-DPSXRECOMP_SKIP_BIOS_STALE_CHECK=ON',
'-DCMAKE_DISABLE_FIND_PACKAGE_SDL3=TRUE','-DCMAKE_DISABLE_FIND_PACKAGE_ZLIB=TRUE'],project/'configure-native.log')
command(['cmake','--build',native,'--parallel',str(args.jobs)],project/'build-native.log')
files=[disc,bios,movie,exe,game,tape,project/'input.psxrti',project/'seeds.txt',*tracks]
build=native/'Pepsiman-TAS.exe'
generated={str(f.relative_to(ROOT)):digest(f) for f in (ROOT/'generated').glob('SCPH5500*') if f.is_file()}
generated.update({str(f):digest(f) for f in (project/'generated').glob('*') if f.is_file()})
info={'schema':'pepsiman-tas-candidate-v1','source_control':control,
'source_head':subprocess.check_output(['git','-C',str(ROOT),'rev-parse','HEAD'],text=True).strip(),
'source_tree':subprocess.check_output(['git','-C',str(ROOT),'rev-parse','HEAD^{tree}'],text=True).strip(),
'bindings':[{'path':str(f),'sha256':digest(f)} for f in files],
'generated':generated,'executable':str(build),'executable_sha256':digest(build),
'disc':str(disc),'bios':str(bios),'game':str(game),'route':str(project/'input.psxrti'),'tape':str(tape),
'compiler':subprocess.check_output(['gcc','--version'],text=True).splitlines()[0],
'qualification':'candidate only; source/native comparison pending'}
write(project/'setup.json',info)
print(json.dumps({'candidate':str(build),'sha256':digest(build)}))
def run(args):
info=json.loads((args.project/'setup.json').read_text())
for binding in info['bindings']: tekken3.require_hash(Path(binding['path']),binding['sha256'])
tekken3.require_hash(Path(info['executable']),info['executable_sha256'])
argv=[sys.executable,HERE/'run_native.py',args.output,'--exe',info['executable'],
'--game',info['game'],'--disc',info['disc'],'--bios',info['bios'],'--route',info['route'],
'--cd-source-clock-tape',info['tape'],'--neutral-tail','1','--timeout',str(args.timeout),
'--checkpoint-every','1200','--renderer','software',*tekken3.PROFILE]
# The same native implementations are an explicit candidate: all psx/
# source bytes match 2.2.2 -> 2.3, but this title still needs its own gates.
if args.show: argv.append('--show')
command(argv,args.output.parent/(args.output.name+'-launch.log'))
print('Native input run ended; compare against the admitted independent source before claiming a pass.')
def main():
p=argparse.ArgumentParser(description=__doc__)
sub=p.add_subparsers(dest='action',required=True)
s=sub.add_parser('setup')
for field in ['disc','bios','movie','project','cache','source-control']:
s.add_argument('--'+field,type=Path,required=True)
s.add_argument('--jobs',type=int,default=4)
r=sub.add_parser('run')
r.add_argument('--project',type=Path,required=True)
r.add_argument('--output',type=Path,required=True)
r.add_argument('--timeout',type=int,default=43200)
r.add_argument('--show',action='store_true')
args=p.parse_args()
(setup if args.action=='setup' else run)(args)
if __name__=='__main__': main()
+28
View File
@@ -0,0 +1,28 @@
"""Asset-free disc-topology admission tests for the second real TAS consumer."""
import unittest
import pepsiman
def cue():
parts=[]
for i in range(1,9):
parts += [f'FILE "track{i}.bin" BINARY',f' TRACK {i:02d} '+('MODE2/2352' if i==1 else 'AUDIO')]
parts += [' INDEX 01 00:00:00'] if i==1 else [' INDEX 00 00:00:00',' INDEX 01 00:02:00']
return '\n'.join(parts)+'\n'
class PepsimanAdmission(unittest.TestCase):
def test_exact_topology(self):
self.assertEqual(pepsiman.cue_files(cue()),[f'track{i}.bin' for i in range(1,9)])
def test_companion_and_topology_negatives(self):
original=cue()
bad=[original.replace('TRACK 02 AUDIO','TRACK 02 MODE2/2352'),
original.replace('INDEX 01 00:02:00','INDEX 01 00:01:00',1),
original.replace('INDEX 00 00:00:00\n','',1),
original.replace('track8.bin','track7.bin'),
original.replace('track1.bin','../outside.bin'),
original.replace('track1.bin','/outside.bin'),
original+'REM undeclared\n',original[:original.index('FILE "track8.bin"')]]
for text in bad:
with self.subTest(text=text):
with self.assertRaises(ValueError): pepsiman.cue_files(text)
if __name__=='__main__': unittest.main()