Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/tasreplays.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: TAS replay source checks
on:
pull_request:
branches: [tasreplays]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Both the pull_request and push triggers are scoped to branches: [tasreplays]. This workflow is added on the tasreplays branch and is meant to merge into main, but once it lands, no push or PR targeting main (or any other branch) will trigger it, so the asset-free TAS source checks (tas_bk2_intake, tas_replay_recipe) stop running entirely after the branch merges. Broaden the branch scope (e.g. include main or drop the branch filter) so the committed checks keep running after merge.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/tasreplays.yml, line 4:

<comment>Both the `pull_request` and `push` triggers are scoped to `branches: [tasreplays]`. This workflow is added on the tasreplays branch and is meant to merge into `main`, but once it lands, no push or PR targeting `main` (or any other branch) will trigger it, so the asset-free TAS source checks (`tas_bk2_intake`, `tas_replay_recipe`) stop running entirely after the branch merges. Broaden the branch scope (e.g. include `main` or drop the branch filter) so the committed checks keep running after merge.</comment>

<file context>
@@ -0,0 +1,29 @@
+name: TAS replay source checks
+on:
+  pull_request:
+    branches: [tasreplays]
+  push:
+    branches: [tasreplays]
</file context>

push:
branches: [tasreplays]
workflow_dispatch:
permissions:
contents: read
jobs:
windows-ucrt:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
install: >-
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-cmake
mingw-w64-ucrt-x86_64-ninja
mingw-w64-ucrt-x86_64-python
- name: Build and test without retail assets
shell: msys2 {0}
run: |
cmake -S recompiler -B build/tests -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
cmake --build build/tests --parallel 4
ctest --test-dir build/tests --output-on-failure -j 4
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,5 @@ recompiler/build-*/
# Project Studio GUI venv (customtkinter)
tools/new_project_layout/.venv/
tools/new_project_layout/project_studio_repos.json
# TAS setup, generated title, owned media and replay evidence are local output.
/build/
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# TAS replay branch

For the verified Tekken 3 TAS, follow the [clean-clone setup and replay guide](tools/tasreplays/README.md).
It builds from your USA disc and SCPH1001 BIOS, then verifies the original TAS through the 8.80-second victory.

<p align="center">
<img src="docs/assets/psxrecomp-logo.png" alt="PSXRecomp" width="640">
</p>
Expand Down
66 changes: 66 additions & 0 deletions docs/TAS_ACCURACY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# TAS accuracy profile and Tekken 3 validation

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Missing whitespace between numerals and the following word throughout this prose makes it hard to read. Examples: "contains7,974 inputs", "all512 4KiB RAM-page hashes ... reference;28 full 2MiB snapshots", "wins Arcade at8.80 seconds", "All57 resulting C files", "bounded at8,400 frames:7,974 original inputs plus426 neutral inputs", and "stopped the research tail after return10,704". Insert a space after each number (and after each comma-separated digit group) before the following word.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/TAS_ACCURACY.md, line 9:

<comment>Missing whitespace between numerals and the following word throughout this prose makes it hard to read. Examples: "contains7,974 inputs", "all512 4KiB RAM-page hashes ... reference;28 full 2MiB snapshots", "wins Arcade at8.80 seconds", "All57 resulting C files", "bounded at8,400 frames:7,974 original inputs plus426 neutral inputs", and "stopped the research tail after return10,704". Insert a space after each number (and after each comma-separated digit group) before the following word.</comment>

<file context>
@@ -0,0 +1,66 @@
+It builds on `f23c5ba1a220fe1ca8818cc48c026d6c2f7f2c64`, the base used for the
+measured campaign, with the final native integration's overlay ABI22.
+
+The unchanged Spikestuff movie contains7,974 inputs. The integrated native
+build wins Arcade at8.80 seconds. Every original-input return clock and all512
+4KiB RAM-page hashes match the independent Octoshock2.2.2 reference;28 full
</file context>


The `tasreplays` branch integrates the source and code-generation corrections
qualified during the September2026 Tekken 3 TAS campaign. The reproducible
entry point is [tools/tasreplays](../tools/tasreplays/README.md).
It builds on `f23c5ba1a220fe1ca8818cc48c026d6c2f7f2c64`, the base used for the
measured campaign, with the final native integration's overlay ABI22.

The unchanged Spikestuff movie contains7,974 inputs. The integrated native
build wins Arcade at8.80 seconds. Every original-input return clock and all512
4KiB RAM-page hashes match the independent Octoshock2.2.2 reference;28 full
2MiB snapshots are also byte-identical. This is stronger evidence for this
route than a clean process exit or simply consuming the input file. It is not
a proof of complete hardware or cross-title compatibility.

## Corrections integrated

Related corrections are grouped here by their shared mechanism. Snapshot
numbers in the original research included diagnostics and experiments; they
are not bug counts.

| Area | Corrected mechanisms |
|---|---|
| CPU/code generation | Uncached instruction fetch costs; copied BIOS/RAM alias admission and syscall EPC; exception transfer and post-SYS continuation; deferred multiply deadlines; isolated cache-tag stores; ROM/RAM precision guards and conservative block bounds; pending-load ownership across compiled/interpreted boundaries; signed arithmetic exceptions; GTE load cancellation, delayed register values, and COP2 interrupt ordering. |
| Interrupts | Execute the actual vector instructions; interrupted fetch and branch-delay context; EPC/Cause/BD/BT/TAR ownership; IRQ reentry/cooldown and repeated delivery after RFE; Cause CE bits; register read image and sampling; recognize IRQs after DMA halt; VBlank latching during a guest IRQ handler. |
| DMA | Live request/linked-list GPU transfer; upload/readback readiness and word ownership; cancellation at packet boundaries; initial credit and register-write order; OTC/CD CPU stalls and instruction fetch overlap; completion deadline invalidation; fractional-service/frontend ordering; SPU DMA initial/word/block timing. |
| GPU | One FIFO/service owner for command admission, dispatch and visible effects; phase-correct triangle rendering and queued environment state; A0/C0 transfers; polygons, textures/CLUT/cache, shading, masks and blending; fill/copy/rectangles; clipping and VRAM wrapping; field-aware row work; raster/status timing and automatic frame-return ownership. |
| CD-ROM | Implicit and explicit seek transitions; asynchronous sector IRQ presentation; firmware/cold tray state; TOC/seek/read pipeline and command clocks; reset; Pause ACK and head rewind/resume; trigger/read-head state. |
| Timers and SIO | Source raster field clock; timer1 HBlank and read sample; timer2 divider/IRQ/deadline; digital-pad ACK pulse; opt-in handling of an unrelated legacy card repair. |
| SPU | Delayed control/status visibility with independent sample service; key-on/envelope phase; ADPCM decode queue, END flags, pitch/filter state and guest register readback. |
| Overlay integration | ABI22 forwards live instruction-boundary callbacks and pending cycle ownership through generated DLLs, preserving replay exclusion and rejecting stale ABI caches. |

Most detailed timing changes are enabled by explicit `octoshock-2.2.2` profile
options; they model the exact emulator on which this TAS was authored.
General CPU/controller defects also have source-level corrections. The profile
does not contain a title-name switch, a precomputed game-state stream, or
delays chosen to force an opponent or victory.

The reference is the immutable
[BizHawk source commit](https://github.com/TASEmulators/BizHawk/tree/519e14aa1ad7a9d6df2edc7808c5ed687dfee046),
observed using ordinary authored guest programs and original movie playback.
The native CPU, devices and renderer execute independently. The isolated
external raw PRNG utility has a separate GPL license and file interface;
its arithmetic is not imported into the framework runtime.

## Reproduction and validation boundaries

The recipe generates the BIOS and title from owned assets using the repaired
emitters. All57 resulting C files must match the normalized fingerprints of the
verified integration. It builds dependencies from pinned archives and uses
software rendering in both visible and headless modes. Recompiler and authored
O0/O2 tests run without retail assets. The final replay verifier rejects an
input mismatch, a missing or reordered checkpoint, a different RAM-page hash,
a return-clock difference, or incomplete termination.

Historical C fixtures that require retained original-core captures are still
identified as fixtures by the registration checker. Their old private driver
names describe provenance, not a claim that those drivers ship in this branch.
Portable CTest registrations and the complete retail replay are the repeatable
checks available from the clone.

The ending is explicitly bounded at8,400 frames:7,974 original inputs plus426
neutral inputs, through the visible victory. Later CDDA Play seek remains
unqualified and stopped the research tail after return10,704. PAL, save-state
restoration, other input devices, other titles, complete audio/pixel equality,
and unrestricted post-victory playback are outside this qualification.
35 changes: 33 additions & 2 deletions recompiler/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -195,16 +195,22 @@ endif()
set(PSXRECOMP_CODEGEN_HASH_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..)
include(${CMAKE_CURRENT_SOURCE_DIR}/../runtime/codegen_hash_sources.cmake)
set(_baked_hash_hdr ${CMAKE_CURRENT_BINARY_DIR}/psxrecomp_baked_codegen_hash.h)
set(_runtime_hash_hdr ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/include/overlay_codegen_hash.h)
add_custom_command(
OUTPUT ${_baked_hash_hdr}
OUTPUT ${_baked_hash_hdr} ${_runtime_hash_hdr}
COMMAND ${CMAKE_COMMAND} -DOUT=${_baked_hash_hdr}
"-DSRCS=${PSXRECOMP_CODEGEN_HASH_SRCS}"
-P ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/hash_codegen.cmake
COMMAND ${CMAKE_COMMAND} -DOUT=${_runtime_hash_hdr}
"-DSRCS=${PSXRECOMP_CODEGEN_HASH_SRCS}"
-P ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/hash_codegen.cmake
DEPENDS ${PSXRECOMP_CODEGEN_HASH_SRCS}
${CMAKE_CURRENT_SOURCE_DIR}/../runtime/hash_codegen.cmake
COMMENT "Hashing codegen sources -> psxrecomp_baked_codegen_hash.h"
VERBATIM)
add_custom_target(psxrecomp_baked_codegen_hash DEPENDS ${_baked_hash_hdr})
# Overlay generation is usable immediately after a tools-only build, before
# configuring any runtime. Both consumers must have the same cache identity.
add_custom_target(psxrecomp_baked_codegen_hash DEPENDS ${_baked_hash_hdr} ${_runtime_hash_hdr})
add_dependencies(psxrecomp-game psxrecomp_baked_codegen_hash)
# Force main_psx.cpp to recompile whenever the hash header content changes
# (same OBJECT_DEPENDS hardening as runtime.cmake — __has_include depfile gap).
Expand Down Expand Up @@ -346,6 +352,17 @@ target_include_directories(emitter_directive_line_test PRIVATE
target_link_libraries(emitter_directive_line_test PRIVATE fmt rabbitizer)

if(BUILD_TESTING)
add_executable(source_cpu_block_bound_test
../runtime/tests/test_source_cpu_block_bound.c
../runtime/src/psx_icache.c)
target_include_directories(source_cpu_block_bound_test PRIVATE ../runtime/include)
target_compile_definitions(source_cpu_block_bound_test PRIVATE PSX_ENABLE_BLOCK_CYCLES=1)
if(MSVC)
target_compile_options(source_cpu_block_bound_test PRIVATE /UNDEBUG)
else()
target_compile_options(source_cpu_block_bound_test PRIVATE -UNDEBUG)
endif()
add_test(NAME source_cpu_block_bound_test COMMAND source_cpu_block_bound_test)
add_test(NAME emitter_directive_line_test
COMMAND emitter_directive_line_test)
add_executable(guest_tty_test
Expand Down Expand Up @@ -582,6 +599,7 @@ if(BUILD_TESTING)
runtime_overlay_guards
sdl3_main_single_include
spu_sample_scheduler_default
overlay_step_boundary
savestate_capture_boundary_guards
vk_build_default
vk_color_self_barrier
Expand Down Expand Up @@ -646,6 +664,19 @@ target_include_directories(reachable_discovery_test PRIVATE
target_link_libraries(reachable_discovery_test PRIVATE fmt)

# ---- test-registration completeness guard -----------------------------------
if(BUILD_TESTING)
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../tools/tasreplays/tests"
"${CMAKE_CURRENT_BINARY_DIR}/tas-models")
if(Python3_Interpreter_FOUND)
add_test(NAME tas_bk2_intake
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_CURRENT_SOURCE_DIR}/../tools/tasreplays/test_bk2_intake.py")
add_test(NAME tas_replay_recipe
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_CURRENT_SOURCE_DIR}/../tools/tasreplays/test_tekken3.py")
endif()
endif()

# Reads both CMakeLists.txt files as text, so it catches an orphan in either
# project from whichever one is being configured. Placed before the collision
# guard, which must stay last because it needs every target declared; this one
Expand Down
16 changes: 11 additions & 5 deletions recompiler/src/code_generator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1432,7 +1432,8 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
case 0x0C: // syscall
{
uint32_t syscall_code = (instr >> 6) & 0xFFFFF;
code = fmt::format("psx_syscall(cpu, {}); /* syscall {} */", syscall_code, syscall_code);
code = fmt::format("cpu->pc = 0x{:08X}u; if (psx_syscall(cpu, {})) return; /* syscall {} */",
addr, syscall_code, syscall_code);
}
break;
case 0x0D: // break
Expand Down Expand Up @@ -1767,14 +1768,19 @@ std::string CodeGenerator::translate_basic_block(
// emitted at cache-line LEADERS: a block leader / mid-block jump-table target (any
// address reachable other than by fall-through, i.e. a possibly-cold cache entry) OR
// a 16-byte-line start (addr&0xC==0, a sequential line crossing). Intra-line
// followers reached by fall-through are guaranteed hits — the leader's fetch
// refilled the line to its end — so they need no call (+0). Extra fetch points are
// followers reached by fall-through are hits only at cached virtual addresses;
// uncached instructions each require a fetch, including delay slots. Extra fetch points are
// harmless (a hit is +0); only UNDER-counting a cold entry would diverge, which the
// leader set prevents. The game runs at its KSEG0 load address, so `insn_addr` is
// already the runtime guest PC (matching the dirty-RAM interp's cpu->pc and Beetle).
auto emit_pre_icache = [&](uint32_t insn_addr, const std::string& indent) {
if (!(insn_addr == block.start_addr || (insn_addr & 0xCu) == 0 ||
extra_labels_.count(insn_addr))) return;
if (!(insn_addr >= 0xA0000000u || insn_addr == block.start_addr || (insn_addr & 0xCu) == 0 ||
extra_labels_.count(insn_addr))) {
ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
ss << indent << fmt::format("psx_cpu_step_boundary(cpu, 0x{:08X}u);\n", insn_addr);
ss << "#endif\n";
return;
}
ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
ss << indent << fmt::format("psx_icache_fetch(cpu, 0x{:08X}u);\n", insn_addr);
ss << "#endif\n";
Expand Down
9 changes: 9 additions & 0 deletions recompiler/src/control_flow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ std::set<uint32_t> ControlFlowAnalyzer::find_block_boundaries(const Function& fu

uint32_t instr = *instr_opt;

// A synchronous syscall can transfer to the guest exception vector.
// Its return is the following instruction (there is no delay slot),
// which must be independently re-enterable by the flat dispatcher.
if ((instr & 0xFC00003Fu) == 0x0000000Cu && addr + 4 < func.end_addr)
boundaries.insert(addr + 4);

if (is_control_flow(instr)) {
ControlFlowInstr cf = analyze_instruction(addr, instr);

Expand Down Expand Up @@ -387,6 +393,9 @@ ControlFlowGraph ControlFlowAnalyzer::analyze_function(const Function& func) {
if (!instr_opt) continue;
uint32_t instr = *instr_opt;

if ((instr & 0xFC00003Fu) == 0x0000000Cu)
add_boundary(addr + 4); // architectural post-syscall resume

if (is_control_flow(instr)) {
ControlFlowInstr cf = analyze_instruction(addr, instr);

Expand Down
Loading
Loading