diff --git a/.github/workflows/tasreplays.yml b/.github/workflows/tasreplays.yml
new file mode 100644
index 000000000..804188d74
--- /dev/null
+++ b/.github/workflows/tasreplays.yml
@@ -0,0 +1,29 @@
+name: TAS replay source checks
+on:
+ pull_request:
+ branches: [tasreplays]
+ 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
diff --git a/.gitignore b/.gitignore
index 608f8ac01..24d6e7ca1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
diff --git a/README.md b/README.md
index 18dbd6757..fdc752c72 100644
--- a/README.md
+++ b/README.md
@@ -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.
+
diff --git a/docs/TAS_ACCURACY.md b/docs/TAS_ACCURACY.md
new file mode 100644
index 000000000..5ae509152
--- /dev/null
+++ b/docs/TAS_ACCURACY.md
@@ -0,0 +1,66 @@
+# TAS accuracy profile and Tekken 3 validation
+
+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.
diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt
index 410076087..9604a767e 100644
--- a/recompiler/CMakeLists.txt
+++ b/recompiler/CMakeLists.txt
@@ -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).
@@ -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
@@ -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
@@ -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
diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp
index 94b3507e1..42a0dc9f6 100644
--- a/recompiler/src/code_generator.cpp
+++ b/recompiler/src/code_generator.cpp
@@ -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
@@ -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";
diff --git a/recompiler/src/control_flow.cpp b/recompiler/src/control_flow.cpp
index 68e452345..cd942a96e 100644
--- a/recompiler/src/control_flow.cpp
+++ b/recompiler/src/control_flow.cpp
@@ -191,6 +191,12 @@ std::set 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);
@@ -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);
diff --git a/recompiler/src/full_function_emitter.cpp b/recompiler/src/full_function_emitter.cpp
index c15f32d45..2b08df9f4 100644
--- a/recompiler/src/full_function_emitter.cpp
+++ b/recompiler/src/full_function_emitter.cpp
@@ -226,6 +226,24 @@ bool FullFunctionEmitter::emit_function(
// The function entry is always a leader.
block_leaders.insert(func.entry_addr);
+ // A normal syscall resumes at the following guest instruction. Register
+ // that interior address with the owning function, like a call return.
+ // Without it, the unknown-dispatch trampoline can resolve a JR directly
+ // and omit both instruction fetches and its delay-slot effects.
+ for (const auto& [addr, raw] : addr_to_raw) {
+ if ((raw & 0xFC00003Fu) != 0x0000000Cu || !addr_to_raw.count(addr + 4u))
+ continue;
+ const auto previous = addr_to_raw.find(addr - 4u);
+ if (previous != addr_to_raw.end()) {
+ const auto prior = StrictTranslator::translate(
+ PSXRecomp::MipsDecoder::decode(previous->second, previous->first));
+ if (prior.is_terminator &&
+ std::string(prior.terminator_kind ? prior.terminator_kind : "") != "rfe")
+ continue; // Delay-slot exception restart is a separate contract.
+ }
+ block_leaders.insert(addr + 4u);
+ }
+
// Collect continuation labels for this function (populated during emit).
std::vector local_continuations;
@@ -740,8 +758,9 @@ bool FullFunctionEmitter::emit_function(
// cache-line LEADERS: a block leader (any branch/dispatch entry — a possibly-cold
// cache entry; cross-function targets are inserted into block_leaders above) 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 refilled the line to its
- // end) → no call (+0). `rom_addr` is the ROM/compile-time address; relocate_ra maps
+ // reached by fall-through are hits only in cached address space. Uncached code
+ // pays a fetch for every instruction, including delay slots. `rom_addr` is the
+ // ROM/compile-time address; relocate_ra maps
// it to the RUNTIME guest PC the CPU actually fetches from (BIOS main stays in-place
// KSEG1 0xBFC..; relocated kernel Part 2 → 0x500+, shell → 0x80030000+), so the
// shared I-cache evolves identically to the dirty-RAM interp (cpu->pc) and Beetle —
@@ -749,9 +768,14 @@ bool FullFunctionEmitter::emit_function(
// relocation preserves bits[3:0], so the line-leader test is space-independent.
auto emit_icache_fetch = [&](uint32_t rom_addr) {
if (!per_insn_cycles) return;
- if (!(block_leaders.count(rom_addr) || (rom_addr & 0xCu) == 0)) return;
+ const uint32_t runtime_addr = relocate_ra(rom_addr);
+ if (!(runtime_addr >= 0xA0000000u || block_leaders.count(rom_addr) ||
+ (runtime_addr & 0xCu) == 0)) {
+ out += fmt::format("#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_cpu_step_boundary(cpu, 0x{:08X}u);\n#endif\n", runtime_addr);
+ return;
+ }
out += fmt::format("#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_icache_fetch(cpu, 0x{:08X}u);\n#endif\n",
- relocate_ra(rom_addr));
+ runtime_addr);
};
for (auto it = addr_to_raw.begin(); it != addr_to_raw.end(); ++it) {
@@ -780,6 +804,42 @@ bool FullFunctionEmitter::emit_function(
out += "#ifdef PSX_COSIM\n";
out += fmt::format(" cosim_block(0x{:08X}u);\n", normalize_address(addr));
out += "#endif\n";
+ // Existing opt-in slicing also covers BIOS ROM in the explicit
+ // original-source profile. The runtime guards that admission;
+ // ordinary BIOS execution keeps its compiled path.
+ // A leader reached as a live delay slot cannot start a new slice.
+ bool follows_branch = false;
+ auto prev = addr_to_raw.find(addr - 4u);
+ if (prev != addr_to_raw.end()) {
+ const auto p = StrictTranslator::translate(
+ PSXRecomp::MipsDecoder::decode(prev->second, prev->first));
+ const std::string kind = p.terminator_kind ? p.terminator_kind : "";
+ follows_branch = p.is_terminator &&
+ kind != "syscall" && kind != "break" && kind != "rfe";
+ }
+ const uint32_t execution_phys = relocate_ra(addr) & 0x1FFFFFFFu;
+ const bool bios_rom = execution_phys >= 0x1FC00000u &&
+ execution_phys < 0x1FC80000u;
+ if (per_insn_cycles && !follows_branch &&
+ (execution_phys < 0x00200000u || bios_rom)) {
+ uint32_t count = 0;
+ bool may_stall_or_change_irq = false;
+ for (auto scan = it; scan != addr_to_raw.end(); ++scan) {
+ if (scan != it && block_leaders.count(scan->first)) break;
+ ++count;
+ const uint32_t op = scan->second >> 26;
+ const uint32_t fn = scan->second & 63u;
+ may_stall_or_change_irq |= op >= 0x20u || op == 0x10u ||
+ op == 0x12u || (op == 0u && fn >= 0x10u && fn <= 0x1Bu);
+ }
+ // Current shared cache model: <=7 refill cycles plus one base
+ // cycle. This conservative guard budget is not a guest charge.
+ out += "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
+ out += fmt::format(
+ " if (psx_slice_block(cpu, 0x{:08X}u, {}u, {})) return;\n",
+ relocate_ra(addr), count * 8u, may_stall_or_change_irq ? 1 : 0);
+ out += "#endif\n";
+ }
// Phase 1.0e-d: advance guest cycles for this block. Macro-
// gated; when off, generated code matches pre-1.0e-d output.
// In per-instruction mode the charge is emitted per instruction
@@ -810,6 +870,10 @@ bool FullFunctionEmitter::emit_function(
// Decode and translate.
PSXRecomp::DecodedInstruction d = PSXRecomp::MipsDecoder::decode(raw, addr);
+ // SYSCALL publishes the guest exception PC. The ROM address remains
+ // the discovery/label identity, but copied code executes in RAM.
+ if ((raw & 0xFC00003Fu) == 0x0000000Cu)
+ d.address = relocate_ra(addr);
TranslateResult tr = StrictTranslator::translate(d);
if (!tr.supported) {
@@ -1836,6 +1900,7 @@ void FullFunctionEmitter::emit_dispatch(
out += "typedef struct {\n";
out += " uint32_t addr;\n";
out += " PsxRecompFunc func;\n";
+ out += " uint32_t runtime_pc; /* represented guest alias, not lookup key */\n";
out += "} DispatchEntry;\n\n";
const size_t total_entries = emitted_normalized.size() + vec_handlers.size();
@@ -1844,16 +1909,21 @@ void FullFunctionEmitter::emit_dispatch(
// Vector entries first (addresses 0xA0/0xB0/0xC0 < 0x500, always first).
for (const auto& vh : vec_handlers) {
- out += fmt::format(" {{ 0x{:08X}u, {} }},\n", vh.ram_addr, vh.func_name);
+ out += fmt::format(" {{ 0x{:08X}u, {}, 0x{:08X}u }},\n", vh.ram_addr, vh.func_name, vh.ram_addr);
}
+ std::map runtime_by_norm;
+ for (const auto& fn : dr.functions)
+ runtime_by_norm[normalize_address(fn.entry_addr)] = bios_runtime_pc(fn.entry_addr);
for (uint32_t norm : emitted_normalized) {
if (continuations.count(norm)) {
const auto& cl = continuations.at(norm);
- out += fmt::format(" {{ 0x{:08X}u, {} }},\n",
- norm, cont_sym(cl.parent_func_norm, cl.rom_addr));
+ out += fmt::format(" {{ 0x{:08X}u, {}, 0x{:08X}u }},\n",
+ norm, cont_sym(cl.parent_func_norm, cl.rom_addr), bios_runtime_pc(cl.rom_addr));
} else {
- out += fmt::format(" {{ 0x{:08X}u, {} }},\n", norm, fn_sym(norm));
+ const auto entry = runtime_by_norm.find(norm);
+ const uint32_t runtime = entry == runtime_by_norm.end() ? norm : entry->second;
+ out += fmt::format(" {{ 0x{:08X}u, {}, 0x{:08X}u }},\n", norm, fn_sym(norm), runtime);
}
}
out += "};\n\n";
@@ -1966,6 +2036,8 @@ void FullFunctionEmitter::emit_dispatch(
out += "static int psx_bios_try_native_call_stub(CPUState* cpu, "
"uint32_t addr) {\n";
out += " uint32_t phys = addr & 0x1FFFFFFFu;\n";
+ out += " if (g_psx_cpu_step_boundary_callback) return 0; /* fused stub has no intermediate retirement boundary */\n";
+ out += " if (addr >= 0xA0000000u) return 0; /* uncached RAM stubs need every fetch */\n";
out += " if (phys != 0xA0u && phys != 0xB0u && phys != 0xC0u) return 0;\n";
out += " uint32_t w0 = cpu->read_word(phys + 0u);\n";
out += " uint32_t w1 = cpu->read_word(phys + 4u);\n";
@@ -1998,6 +2070,7 @@ void FullFunctionEmitter::emit_dispatch(
out += "extern int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr);\n";
out += "extern int dirty_ram_is_dirty(uint32_t phys);\n";
+ out += "extern void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len);\n";
out += "extern int psx_kernel_bless_dispatchable(uint32_t phys);\n";
out += "extern void fntrace_record(CPUState* cpu, uint32_t target);\n";
out += "extern uint64_t g_dispatch_static_hits;\n";
@@ -2109,6 +2182,16 @@ void FullFunctionEmitter::emit_dispatch(
out += " while (lo <= hi) {\n";
out += " int mid = (lo + hi) / 2;\n";
out += " if (dispatch_table[mid].addr == phys) {\n";
+ out += " /* Normalization locates bytes; it does not change the guest\n";
+ out += " * execution alias. Fixed native bodies encode fetch/link/PC\n";
+ out += " * addresses. Interpret unrepresented RAM aliases as supplied. */\n";
+ out += " if ((addr & 0x1FFFFFFFu) < 0x200000u &&\n";
+ out += " addr != dispatch_table[mid].runtime_pc) {\n";
+ out += " /* A known entry also admits clean RAM to the interpreter.\n";
+ out += " * This marks host executable metadata, never guest bytes. */\n";
+ out += " dirty_ram_mark_executable_range(addr & 0x1FFFFFFFu, 4u);\n";
+ out += " break;\n";
+ out += " }\n";
if (addr_model().has_kbless()) {
out += fmt::format(
" /* Kernel-image bless guard (CLAUDE.md Rule 18). The keys in\n"
diff --git a/recompiler/tests/full_function_emitter_test.cpp b/recompiler/tests/full_function_emitter_test.cpp
index 747e977d4..19e026730 100644
--- a/recompiler/tests/full_function_emitter_test.cpp
+++ b/recompiler/tests/full_function_emitter_test.cpp
@@ -62,6 +62,7 @@ std::string read_file(const std::filesystem::path& path) {
struct RunResult {
EmitStats stats;
std::string dispatch;
+ std::string body;
};
RunResult run_case(const char* name, const std::vector& words,
@@ -86,6 +87,7 @@ RunResult run_case(const char* name, const std::vector& words,
rom, kBase, kBase + static_cast(rom.size()) - 1u,
discovery, "synthetic", out_dir.string(), stem);
result.dispatch = read_file(out_dir / (stem + "_dispatch.c"));
+ result.body = read_file(out_dir / (stem + "_full.c"));
std::filesystem::remove_all(out_dir);
return result;
}
@@ -94,7 +96,7 @@ void expect_entry_absent(const RunResult& result, uint32_t normalized,
const char* message) {
char needle[96];
std::snprintf(needle, sizeof(needle),
- "{ 0x%08Xu, Test_func_%08X }", normalized, normalized);
+ "{ 0x%08Xu, Test_func_%08X", normalized, normalized);
expect(result.dispatch.find(needle) == std::string::npos, message);
}
@@ -197,9 +199,35 @@ void complementary_lwl_lwr_stays_native() {
"complementary LWL/LWR remains native");
}
+void linear_fetches_follow_runtime_address(uint32_t runtime) {
+ const bool uncached = runtime >= 0xA0000000u;
+ const auto result = run_case(
+ uncached ? "uncached-fetch" : "relocated-cached-fetch",
+ {0x24080001u, 0x25080001u, 0x25080001u, 0x25080001u,
+ 0x03E00008u, 0x00000000u},
+ {function_at(kBase, kBase + 20u, {kBase})});
+ expect(result.stats.functions_emitted == 1, "synthetic fetch program remains native");
+ if (const char* output = std::getenv("PSX_TEST_FETCH_OUTPUT_DIR")) {
+ char name[64]; std::snprintf(name,sizeof name,"full-fetch-%08X.c",runtime);
+ std::ofstream generated(std::filesystem::path(output)/name);generated << result.body;
+ }
+ for (uint32_t offset = 0; offset < 24; offset += 4) {
+ char needle[96];
+ std::snprintf(needle, sizeof needle, "psx_icache_fetch(cpu, 0x%08Xu);", runtime + offset);
+ const auto first = result.body.find(needle);
+ const bool required = uncached || (offset & 15u) == 0;
+ expect((first != std::string::npos) == required,
+ uncached ? "every uncached instruction pays a fetch, including return slot"
+ : "relocated cached followers retain the existing leader policy");
+ if (first != std::string::npos)
+ expect(result.body.find(needle, first + 1) == std::string::npos,
+ "a fetch is not emitted twice for one instruction");
+ }
+}
+
} // namespace
-int main() {
+int main(int argc, char** argv) {
BiosConfig config{};
config.config_path = "test://full-function-emitter";
config.load_address = kBase;
@@ -216,11 +244,163 @@ int main() {
FunctionDiscovery::set_address_model(&model);
FullFunctionEmitter::set_address_model(&model);
+ if (argc == 3 && std::string(argv[2]).rfind("rom-irq", 0) == 0) {
+ config.address_copies.clear();
+ BiosAddressModel rom_model = BiosAddressModel::from_config(config);
+ FunctionDiscovery::set_address_model(&rom_model);
+ FullFunctionEmitter::set_address_model(&rom_model);
+ std::vector words(0x564u / 4u, 0u);
+ const uint32_t branch = std::string(argv[2]) == "rom-irq-not-taken" ?
+ 0x13000001u : std::string(argv[2]) == "rom-irq-jump" ? 0x0BF00155u : 0x17000001u;
+ const uint32_t body[] = {0x27180001u, 0x27180001u, 0x27180001u,
+ branch, 0u, 0x24190009u, 0xAE190000u, 0x03E00008u, 0u};
+ for (unsigned i = 0; i < 9; ++i) words[0x540u / 4u + i] = body[i];
+ std::vector rom;
+ for (uint32_t op : words) append_word(rom, op);
+ DiscoveryResult discovery{}; discovery.ok = true;
+ discovery.functions = {
+ function_at(kBase + 0x540u, kBase + 0x550u, {kBase + 0x540u}),
+ function_at(kBase + 0x554u, kBase + 0x560u, {kBase + 0x554u})};
+ std::filesystem::create_directories(argv[1]);
+ FullFunctionEmitter::emit(rom, kBase, kBase + (uint32_t)rom.size() - 1u,
+ discovery, "authored ROM IRQ fixture", argv[1], "Test");
+ return 0;
+ }
+
+ // A branch-slot return-address load crossing to a represented epilogue.
+ // The first target instruction also reads RA: retiring at handoff is early.
+ if (argc == 3 && (std::string(argv[2]) == "slice-load-return" ||
+ std::string(argv[2]) == "slice-load-return-dependent")) {
+ config.address_copies[0].runtime_base = 0x80000500u;
+ BiosAddressModel return_model = BiosAddressModel::from_config(config);
+ FunctionDiscovery::set_address_model(&return_model);
+ FullFunctionEmitter::set_address_model(&return_model);
+ std::vector words = {
+ 0x27BDFFE8u, 0x10000003u, 0x8FBF0014u, 0u, 0u,
+ std::string(argv[2]) == "slice-load-return-dependent" ? 0x03E08021u : 0x00008021u,
+ 0x27BD0018u, 0x03E00008u, 0x26310001u,
+ 0x26520001u, 0x0260F821u, 0x03E00008u, 0u};
+ std::vector rom;
+ for (uint32_t op : words) append_word(rom, op);
+ DiscoveryResult discovery{}; discovery.ok = true;
+ discovery.functions = {
+ function_at(kBase, kBase + 8u, {kBase}),
+ function_at(kBase + 0x14u, kBase + 0x20u, {kBase + 0x14u}),
+ function_at(kBase + 0x24u, kBase + 0x30u, {kBase + 0x24u})};
+ std::filesystem::create_directories(argv[1]);
+ FullFunctionEmitter::emit(rom, kBase, kBase + (uint32_t)rom.size() - 1u,
+ discovery, "authored slice return fixture", argv[1], "Test");
+ std::ofstream data(std::filesystem::path(argv[1]) / "authored-ram.bin", std::ios::binary);
+ data.write((const char*)rom.data(), rom.size());
+ return 0;
+ }
+
+ // Authored copied-BIOS timer IRQ recognition, with a single native group.
+ // Target/mode/mask are case inputs, never fitted to a retail outcome.
+ if (argc == 3 && std::string(argv[2]) == "irq-recognition") {
+ config.address_copies[0].runtime_base = 0x80000500u;
+ BiosAddressModel irq_model = BiosAddressModel::from_config(config);
+ FunctionDiscovery::set_address_model(&irq_model);
+ FullFunctionEmitter::set_address_model(&irq_model);
+ std::vector words = {
+ 0xA5690008u, 0xA56C0004u, 0x95680000u, 0x8E020000u, 0x8E180000u,
+ 0x00000000u, 0x2719FFFFu, 0x14400005u, 0xAE190000u,
+ 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u,
+ 0x03E00008u, 0x26310001u};
+ std::vector rom;
+ for (uint32_t op : words) append_word(rom, op);
+ DiscoveryResult discovery{}; discovery.ok = true;
+ discovery.functions = {
+ function_at(kBase, kBase + 0x20u, {kBase}),
+ function_at(kBase + 0x34u, kBase + 0x38u, {kBase + 0x34u})};
+ std::filesystem::create_directories(argv[1]);
+ FullFunctionEmitter::emit(rom, kBase, kBase + (uint32_t)rom.size() - 1u,
+ discovery, "authored timer IRQ fixture", argv[1], "Test");
+ std::ofstream data(std::filesystem::path(argv[1]) / "authored-ram.bin", std::ios::binary);
+ data.write((const char*)rom.data(), rom.size());
+ return 0;
+ }
+
+ // Authored uncached SYS resume and ordinary JAL continuation controls.
+ if (argc == 3 && std::string(argv[2]) == "syscall-resume") {
+ config.address_copies.clear();
+ BiosAddressModel resume_model = BiosAddressModel::from_config(config);
+ FunctionDiscovery::set_address_model(&resume_model);
+ FullFunctionEmitter::set_address_model(&resume_model);
+ std::vector words(0x48u / 4u, 0u);
+ words[0] = 0x0000000Cu; words[1] = 0x03E00008u; words[2] = 0x26310001u;
+ words[8] = 0x0FF00010u; words[10] = 0x02000008u; words[11] = 0x26310001u;
+ words[16] = 0x03E00008u;
+ std::vector rom;
+ for (uint32_t op : words) append_word(rom, op);
+ DiscoveryResult discovery{}; discovery.ok = true;
+ discovery.functions = {
+ function_at(kBase, kBase + 8u, {kBase}),
+ function_at(kBase + 0x20u, kBase + 0x2Cu, {kBase + 0x20u, kBase + 0x28u}),
+ function_at(kBase + 0x40u, kBase + 0x44u, {kBase + 0x40u})};
+ std::filesystem::create_directories(argv[1]);
+ FullFunctionEmitter::emit(rom, kBase, kBase + (uint32_t)rom.size() - 1u,
+ discovery, "authored syscall resume fixture", argv[1], "Test");
+ std::ofstream data(std::filesystem::path(argv[1]) / "authored-rom.bin", std::ios::binary);
+ data.write((const char*)rom.data(), rom.size());
+ return 0;
+ }
+
+ // Export only authored instructions for a real-runtime admission fixture.
+ if (argc == 2) {
+ std::vector words(0x130u / 4u, 0u);
+ const auto put = [&](uint32_t at, std::initializer_list ops) {
+ for (uint32_t op : ops) { words[(at - 0x500u) / 4u] = op; at += 4u; }
+ };
+ put(0x500u, {0x3C080000u, 0x25080598u, 0x01000008u, 0u});
+ put(0x540u, {0x0C000160u, 0u, 0x03E08021u, 0x02200008u, 0u});
+ put(0x580u, {0x03E00008u, 0u});
+ put(0x5C0u, {0x0000000Cu, 0x03E00008u, 0u});
+ put(0x600u, {0x10000007u, 0u});
+ put(0x620u, {0x01001021u, 0x03E00008u, 0u});
+ std::vector rom;
+ for (uint32_t op : words) append_word(rom, op);
+ DiscoveryResult discovery{}; discovery.ok = true;
+ discovery.functions = {
+ function_at(kBase, kBase + 12u, {kBase}),
+ function_at(kBase + 0x40u, kBase + 0x50u, {kBase + 0x40u, kBase + 0x48u}),
+ function_at(kBase + 0x80u, kBase + 0x84u, {kBase + 0x80u}),
+ function_at(kBase + 0xC0u, kBase + 0xC8u, {kBase + 0xC0u}),
+ function_at(kBase + 0x100u, kBase + 0x128u, {kBase + 0x100u, kBase + 0x120u})};
+ std::filesystem::create_directories(argv[1]);
+ FullFunctionEmitter::emit(rom, kBase, kBase + (uint32_t)rom.size() - 1u,
+ discovery, "authored alias fixture", argv[1], "Test");
+ std::ofstream data(std::filesystem::path(argv[1]) / "authored-ram.bin", std::ios::binary);
+ data.write((const char*)rom.data(), rom.size());
+ return 0;
+ }
+
delay_slot_load_falls_back();
label_split_load_falls_back();
fragment_split_load_falls_back();
noncomplementary_lwl_falls_back();
complementary_lwl_lwr_stays_native();
+ linear_fetches_follow_runtime_address(0x500u);
+ for (const uint32_t alias : {0x80010000u, 0xA0010000u}) {
+ config.address_copies[0].ram_lo = 0x00010000u;
+ config.address_copies[0].runtime_base = alias;
+ BiosAddressModel alias_model = BiosAddressModel::from_config(config);
+ FunctionDiscovery::set_address_model(&alias_model);
+ FullFunctionEmitter::set_address_model(&alias_model);
+ linear_fetches_follow_runtime_address(alias);
+ }
+ config.address_copies.clear();
+ BiosAddressModel uncached_model = BiosAddressModel::from_config(config);
+ FunctionDiscovery::set_address_model(&uncached_model);
+ FullFunctionEmitter::set_address_model(&uncached_model);
+ linear_fetches_follow_runtime_address(kBase);
+
+ const auto resume = run_case("syscall-resume", {0x0000000Cu, 0x03E00008u, 0x26310001u},
+ {function_at(kBase, kBase + 8u, {kBase})});
+ expect(resume.body.find("case 0xBFC00004u: goto label_BFC00004;") != std::string::npos,
+ "post-syscall resume enters its owning function at the exact instruction");
+ expect(resume.dispatch.find("{ 0x1FC00004u,") != std::string::npos,
+ "post-syscall interior instruction has a dispatch key");
if (failures != 0) {
std::fprintf(stderr, "%d full-function emitter test(s) failed\n", failures);
diff --git a/recompiler/tests/recompiler_patch_test.cpp b/recompiler/tests/recompiler_patch_test.cpp
index ab644eae4..d9ec6427a 100644
--- a/recompiler/tests/recompiler_patch_test.cpp
+++ b/recompiler/tests/recompiler_patch_test.cpp
@@ -1056,6 +1056,76 @@ void cfg_codegen_load_delay_test() {
"CFG codegen preserves MIPS-I dependent load-delay value semantics");
}
+void uncached_fetch_codegen_test() {
+ for (const uint32_t base : {0x00010000u, 0x80010000u, 0xA0010000u}) {
+ PSXRecomp::PS1Executable exe{};
+ exe.header.load_address = base;
+ exe.header.initial_pc = base;
+ exe.header.file_size = 24u;
+ for (const uint32_t word : {0x24080001u, 0x25080001u, 0x25080001u,
+ 0x25080001u, 0x03E00008u, 0x00000000u})
+ append_word(exe.code_data, word);
+ PSXRecomp::Function function{};
+ function.start_addr = base;
+ function.end_addr = base + 24u;
+ function.size = 24u;
+ function.name = "fetch_test";
+ PSXRecomp::ControlFlowAnalyzer analyzer(exe);
+ const auto cfg = analyzer.analyze_function(function);
+ PSXRecomp::CodeGenerator generator(exe);
+ const std::string code = generator.generate_function(function, cfg).full_code;
+ if (const char* output = std::getenv("PSX_TEST_FETCH_OUTPUT_DIR")) {
+ std::ofstream generated(fs::path(output)/fmt::format("cfg-fetch-{:08X}.c",base));
+ generated << "#include \"cpu_state.h\"\n#include \"psx_icache.h\"\n#include \"psx_cyc.h\"\n" << code;
+ }
+ for (uint32_t offset = 0; offset < 24; offset += 4) {
+ const std::string needle = fmt::format("psx_icache_fetch(cpu, 0x{:08X}u);", base + offset);
+ const size_t found = code.find(needle);
+ check((found != std::string::npos) == (base >= 0xA0000000u || (offset & 15u) == 0),
+ fmt::format("virtual fetch policy at {:08X}, including return slot", base + offset));
+ check(found == std::string::npos || code.find(needle, found + 1) == std::string::npos,
+ "one fetch per emitted instruction site");
+ }
+ }
+}
+
+void syscall_codegen_transfer_test() {
+ for (const uint32_t base : {0x80010000u, 0xA0010000u}) {
+ PSXRecomp::PS1Executable exe{};
+ exe.header.load_address = base; exe.header.initial_pc = base;
+ exe.header.file_size = 20;
+ for (uint32_t word : {0x24040001u, 0x0000000Cu, 0x25080001u,
+ 0x03E00008u, 0x00000000u}) append_word(exe.code_data, word);
+ PSXRecomp::Function function{};
+ function.start_addr=base; function.end_addr=base+20; function.size=20;
+ function.name=fmt::format("func_{:08X}",base);
+ PSXRecomp::ControlFlowAnalyzer analyzer(exe);
+ auto cfg=analyzer.analyze_function(function);
+ PSXRecomp::CodeGenerator generator(exe);
+ auto generated=generator.generate_function(function,cfg);
+ check(generated.full_code.find(fmt::format(
+ "cpu->pc = 0x{:08X}u; if (psx_syscall(cpu, 0)) return;",base+4))!=std::string::npos,
+ "game syscall sets executing virtual PC and preserves transfer");
+ check(cfg.blocks.count(base+8)==1 && generator.cps_continuations().count(base+8)==1,
+ "post-syscall instruction has a dispatchable continuation");
+ if (const char *dir=std::getenv("PSX_TEST_SYSCALL_OUTPUT_DIR")) {
+ std::ofstream out(fs::path(dir)/fmt::format("syscall-{:08X}.c",base));
+ out << "#include \n#include \n#include \"cpu_state.h\"\n"
+ "#define PSX_NO_DEBUG_TOOLS 1\n"
+ "static int transfer; static uint32_t seen;\n"
+ "void debug_server_log_call_entry(uint32_t x) {(void)x;}\n"
+ "void psx_check_interrupts_at(CPUState*c,uint32_t p){(void)c;(void)p;}\n"
+ "int psx_syscall(CPUState*c,uint32_t code){seen=c->pc;c->pc=transfer?0x80000080:0;return transfer;}\n";
+ out << generated.full_code;
+ out << fmt::format("\nint main(void){{CPUState c={{0}};c.gpr[31]=0x80090000;transfer=1;"
+ "func_{0:08X}(&c);assert(seen==0x{1:08X}u&&c.pc==0x80000080&&c.gpr[8]==0);"
+ "c.pc=0x{2:08X}u;func_{0:08X}(&c);assert(c.gpr[8]==1&&c.pc==c.gpr[31]);"
+ "c.pc=0;c.gpr[8]=0;transfer=0;func_{0:08X}(&c);assert(c.gpr[8]==1&&seen==0x{1:08X}u);return 0;}}\n",
+ base,base+4,base+8);
+ }
+ }
+}
+
} // namespace
int main() {
@@ -1071,6 +1141,8 @@ int main() {
gte_codegen_classification_tests();
jump_table_producer_codegen_test();
cfg_codegen_load_delay_test();
+ uncached_fetch_codegen_test();
+ syscall_codegen_transfer_test();
} catch (const std::exception& e) {
fmt::print(stderr, "FAIL unexpected exception: {}\n", e.what());
++failures;
diff --git a/recompiler/tests/test_aot_overlay_discovery.py b/recompiler/tests/test_aot_overlay_discovery.py
index 77d97acb4..ce9a92af2 100644
--- a/recompiler/tests/test_aot_overlay_discovery.py
+++ b/recompiler/tests/test_aot_overlay_discovery.py
@@ -1441,8 +1441,10 @@ def pause_after_manifest(src, dst):
import _ctypes
import ctypes
import shutil
- gcc = r'C:\msys64\mingw64\bin\gcc.exe'
- assert os.path.isfile(gcc), "real loaded-DLL regression needs MinGW gcc"
+ gcc = (r'C:\msys64\mingw64\bin\gcc.exe'
+ if os.path.isfile(r'C:\msys64\mingw64\bin\gcc.exe')
+ else shutil.which('gcc'))
+ assert gcc and os.path.isfile(gcc), "real loaded-DLL regression needs MinGW gcc"
with tempfile.TemporaryDirectory() as tmp:
# The actual host compiler must export the same 64-bit identity
# serialized in P; this catches width/decorated-name mistakes
@@ -2145,8 +2147,10 @@ def racing_link(_source, staged, *_args, **_kwargs):
if os.name == 'nt':
# Exact pairs racing to distinct names may both commit: the namespace
# lock makes the second writer observe the first pair's dedup identity.
- gcc = r'C:\msys64\mingw64\bin\gcc.exe'
- assert os.path.isfile(gcc)
+ gcc = (r'C:\msys64\mingw64\bin\gcc.exe'
+ if os.path.isfile(r'C:\msys64\mingw64\bin\gcc.exe')
+ else shutil.which('gcc'))
+ assert gcc and os.path.isfile(gcc)
with tempfile.TemporaryDirectory() as tmp:
pair_id = 0x1020304050607080
source = pathlib.Path(tmp) / 'capacity.c'
@@ -2924,7 +2928,9 @@ def capacity_full(roots):
f"P {pair_id:016X}\nF {entry:08X} {code_crc:08X} junk\n"
f"R {entry:08X} 8\n")
assert MOD.load_shard_entry_set(str(dll)) == set()
- outside = 0x80200000
+ # The loader also accepts high-bank code for 8 MiB modifications.
+ # Exercise the actual host-capacity boundary, not retail RAM size.
+ outside = 0x80000000 + MOD.PSX_RAM_SIZE
ranges.write_text(
f"P {pair_id:016X}\nF {outside:08X} {code_crc:08X}\n"
f"R {outside:08X} 4\n")
@@ -3421,7 +3427,7 @@ def check_full_candidate_cli_fastpath(recompiler):
leaf = (cache_root / 'CYCT-00101' / 'gcc' / MOD.cache_arch_abi() /
f'cg{MOD.codegen_ver(str(runtime_include))}_'
f'{MOD.codegen_hash(str(runtime_include)):08x}_'
- f'gc{MOD.overlay_config_hash(recompiler, str(game_toml)):08x}')
+ f'gc{MOD.overlay_config_hash(recompiler, str(game_toml)):08x}_f0')
leaf.mkdir(parents=True)
pair_id = 0x123456789ABCDEF0
captured_bytes = b'\x08\x00\xE0\x03\x00\x00\x00\x00'
@@ -3723,4 +3729,19 @@ def main():
if __name__ == "__main__":
- sys.exit(main())
+ if os.name == 'nt':
+ # This test deliberately loads malformed DLL bytes. CTest may clear
+ # the shell's inherited error mode, leaving LoadLibrary in a modal
+ # Windows error dialog instead of returning the expected OSError.
+ import ctypes
+ kernel = ctypes.WinDLL('kernel32', use_last_error=True)
+ kernel.GetErrorMode.restype = ctypes.c_uint
+ kernel.SetErrorMode.argtypes = [ctypes.c_uint]
+ previous_mode = kernel.GetErrorMode()
+ kernel.SetErrorMode(previous_mode | 0x0001 | 0x8000)
+ try:
+ sys.exit(main())
+ finally:
+ kernel.SetErrorMode(previous_mode)
+ else:
+ sys.exit(main())
diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt
index 18d206be3..7367c6692 100644
--- a/runtime/CMakeLists.txt
+++ b/runtime/CMakeLists.txt
@@ -23,6 +23,80 @@ psxrecomp_add_runtime_target(psx-runtime
# (ENHANCEMENTS.md G1.10). Titles get their _pgxp the same way.
if(BUILD_TESTING)
+ add_executable(compiled_ram_read_watch_test tests/test_compiled_ram_read_watch.c)
+ target_include_directories(compiled_ram_read_watch_test PRIVATE include)
+ target_compile_definitions(compiled_ram_read_watch_test PRIVATE PSX_ENABLE_BLOCK_CYCLES=1)
+ add_test(NAME compiled_ram_read_watch_test COMMAND compiled_ram_read_watch_test)
+ add_executable(input_update_clock_test tests/test_input_update_clock.c)
+ target_include_directories(input_update_clock_test PRIVATE include)
+ add_test(NAME input_update_clock_test COMMAND input_update_clock_test)
+ add_executable(input_instruction_histogram_test tests/test_input_instruction_histogram.c)
+ target_include_directories(input_instruction_histogram_test PRIVATE include)
+ add_test(NAME input_instruction_histogram_test COMMAND input_instruction_histogram_test)
+ if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
+ add_executable(critical_exception_test tests/test_critical_exception.c)
+ target_include_directories(critical_exception_test PRIVATE include)
+ target_compile_options(critical_exception_test PRIVATE -O3 -flto -fwhole-program)
+ target_link_options(critical_exception_test PRIVATE -flto)
+ add_test(NAME critical_exception_direct_test COMMAND critical_exception_test)
+ add_test(NAME critical_exception_vector_test COMMAND critical_exception_test exception)
+ add_test(NAME critical_exception_nested_reject_test COMMAND critical_exception_test exception nested)
+ set_tests_properties(critical_exception_nested_reject_test PROPERTIES WILL_FAIL TRUE)
+ add_executable(sio_card_repair_test tests/test_sio_card_repair.c)
+ target_include_directories(sio_card_repair_test PRIVATE include)
+ target_compile_options(sio_card_repair_test PRIVATE -O2 -flto -fwhole-program)
+ target_link_options(sio_card_repair_test PRIVATE -flto)
+ add_test(NAME sio_card_repair_test COMMAND sio_card_repair_test)
+ add_executable(sio_ack_timing_test tests/test_sio_ack_timing.c)
+ target_include_directories(sio_ack_timing_test PRIVATE include)
+ target_compile_options(sio_ack_timing_test PRIVATE -O2 -flto -fwhole-program)
+ target_link_options(sio_ack_timing_test PRIVATE -flto)
+ add_test(NAME sio_ack_timing_test COMMAND sio_ack_timing_test)
+ add_executable(dma_otc_timing_test tests/test_dma_otc_timing.c)
+ target_include_directories(dma_otc_timing_test PRIVATE include)
+ target_compile_options(dma_otc_timing_test PRIVATE -O2 -flto -fwhole-program)
+ target_link_options(dma_otc_timing_test PRIVATE -flto)
+ add_test(NAME dma_otc_timing_test COMMAND dma_otc_timing_test)
+ add_executable(dma_cd_source_test tests/test_dma_cd_source.c)
+ target_include_directories(dma_cd_source_test PRIVATE include)
+ target_compile_definitions(dma_cd_source_test PRIVATE PSX_TEST_SOURCE_CD_IMPLEMENTED)
+ target_compile_options(dma_cd_source_test PRIVATE -O2 -flto -fwhole-program)
+ target_link_options(dma_cd_source_test PRIVATE -flto)
+ add_test(NAME dma_cd_source_test COMMAND dma_cd_source_test)
+ endif()
+ add_executable(cdrom_boot_model_test tests/test_cdrom_boot_model.c src/psx_sha256.c)
+ target_include_directories(cdrom_boot_model_test PRIVATE include)
+ add_test(NAME cdrom_boot_model_test COMMAND cdrom_boot_model_test)
+ add_executable(cdrom_explicit_seek_test tests/test_cdrom_explicit_seek.c src/psx_sha256.c)
+ target_include_directories(cdrom_explicit_seek_test PRIVATE include)
+ add_test(NAME cdrom_explicit_seek_test COMMAND cdrom_explicit_seek_test)
+ add_executable(cdrom_source_explicit_timing_test tests/test_cdrom_source_explicit_timing.c src/psx_sha256.c)
+ target_include_directories(cdrom_source_explicit_timing_test PRIVATE include)
+ add_test(NAME cdrom_source_explicit_timing_test COMMAND cdrom_source_explicit_timing_test)
+ add_executable(cdrom_random_tape_test tests/test_cdrom_random_tape.c src/psx_sha256.c)
+ target_include_directories(cdrom_random_tape_test PRIVATE include)
+ add_test(NAME cdrom_random_tape_test COMMAND cdrom_random_tape_test)
+ add_executable(cdrom_implicit_seek_test tests/test_cdrom_implicit_seek.c src/psx_sha256.c)
+ target_include_directories(cdrom_implicit_seek_test PRIVATE include)
+ add_test(NAME cdrom_implicit_seek_test COMMAND cdrom_implicit_seek_test)
+ # This optional clock fixture takes a separately generated fixed tape path.
+ add_executable(cdrom_source_clock_test tests/test_cdrom_source_clock.c src/psx_sha256.c)
+ target_include_directories(cdrom_source_clock_test PRIVATE include)
+ add_executable(cdrom_source_reset_test tests/test_cdrom_source_reset.c src/psx_sha256.c)
+ target_include_directories(cdrom_source_reset_test PRIVATE include)
+
+ add_executable(input_route_file_test tests/test_input_route_file.c)
+ target_include_directories(input_route_file_test PRIVATE include)
+ add_test(NAME input_route_file_test COMMAND input_route_file_test)
+
+ find_package(Python3 REQUIRED COMPONENTS Interpreter)
+ add_executable(input_route_observer_test tests/test_input_route_observer.c
+ src/input_route_observer.c src/psx_sha256.c)
+ target_include_directories(input_route_observer_test PRIVATE include)
+ add_test(NAME input_route_observer_test COMMAND ${Python3_EXECUTABLE}
+ ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_input_route_observer.py
+ $)
+
add_executable(launcher_device_roundtrip_test
tests/test_launcher_device_roundtrip.cpp)
target_include_directories(launcher_device_roundtrip_test PRIVATE include)
@@ -478,6 +552,13 @@ if(BUILD_TESTING)
target_include_directories(psx_vblank_clock_test PRIVATE include)
add_test(NAME psx_vblank_clock_test COMMAND psx_vblank_clock_test)
+ add_executable(input_route_field_clock_test tests/test_input_route_field_clock.c)
+ target_include_directories(input_route_field_clock_test PRIVATE include)
+ add_test(NAME input_route_field_clock_test COMMAND input_route_field_clock_test)
+ add_executable(input_route_raster_clock_test tests/test_input_route_raster_clock.c)
+ target_include_directories(input_route_raster_clock_test PRIVATE include)
+ add_test(NAME input_route_raster_clock_test COMMAND input_route_raster_clock_test)
+
add_executable(spu_end_without_repeat_test
tests/test_spu_end_without_repeat.c)
target_include_directories(spu_end_without_repeat_test PRIVATE include)
@@ -490,6 +571,10 @@ if(BUILD_TESTING)
add_test(NAME spu_pitch_zero_test
COMMAND spu_pitch_zero_test)
+ add_executable(spu_status_visibility_test tests/test_spu_status_visibility.c)
+ target_include_directories(spu_status_visibility_test PRIVATE include)
+ add_test(NAME spu_status_visibility_test COMMAND spu_status_visibility_test)
+
add_executable(fps_readout_test tests/test_fps_readout.c)
target_include_directories(fps_readout_test PRIVATE include)
add_test(NAME fps_readout_test COMMAND fps_readout_test)
diff --git a/runtime/check_test_registration.cmake b/runtime/check_test_registration.cmake
index a26d87f36..32d648fa0 100644
--- a/runtime/check_test_registration.cmake
+++ b/runtime/check_test_registration.cmake
@@ -44,6 +44,39 @@ set(_PSXRECOMP_TESTREG_DIR "${CMAKE_CURRENT_LIST_DIR}")
# Every entry needs a reason. An entry with no reason is a silent orphan with
# extra steps, which is the thing this file exists to prevent.
set(PSXRECOMP_TESTS_NOT_REGISTERED
+ "test_irq_cache_block.c|Private TAS handoff work/check_irq_cache58.py links retained runtime objects and drives the unchanged original-core timer/branch controls; kernel76 and ROM85 variants share this driver"
+ "test_irq_recognition.c|Authored copied-BIOS timer IRQ fixture driven by shared tas-replay-research/verify_irq_recognition.py; source-qualified bounded emitted/load/return cases, not general precise continuation coverage"
+ "test_dma_completion_deadline.c|Authored actual-scheduler delayed-completion fixture driven by tas-replay-research/verify_dma_completion_deadline.py; O0/O2 deadline/cache/IRQ/timer controls"
+ "test_spu_status_scheduler.c|actual cycle/SPU integration with exact production sample-event extraction driven by tas-replay-research/verify_spu_scheduler.py"
+ "test_bios_syscall_resume.c|authored generated SYS resume and ordinary JAL control driven by tas-replay-research/verify_bios_syscall_resume.py"
+ "test_bios_alias_admission.c|authored generated program plus retained runtime integration driven by tas-replay-research/verify_alias_runtime.py"
+ "test_irq_prefetch.c|actual production IRQ poll and authored guest handler linked to retained runtime objects; original-core oracle driven by tas-replay-research/verify_irq_prefetch.py"
+ "test_compiled_dma_read_wait.c|authored fixture driven by tas-replay-research/verify_gpu_upload.py, with exact-source vectors and separate O0/O2 runs"
+ "test_dma_cd_deadline.c|authored fixture driven by tas-replay-research/verify_cd_dma_deadline.py, with exact-source vectors and separate O0/O2 runs"
+ "test_dma_gpu_linked_list_source.c|Authored controller fixture driven by tas-replay-research/verify_gpu_linked_list.py; source startup expectations and state/order negatives"
+ "test_source_gpu_command_projection.c|Standalone scalar projection driven by tas-replay-research/verify_gpu_command_projection.py; retained original-core checkpoints, no automatic scheduler or rendering claim"
+ "test_source_gpu_service_clock.c|Standalone event clock driven by tas-replay-research/verify_gpu_service_clock.py; cold authored NTSC service, excludes runtime scheduling and frame-end rescheduling"
+ "test_gpu_frame_retirement.c|Shared TAS verify_gpu_frame_retirement.py links actual RAM interpreter/IRQ/device objects; six source cases, tracing off/on. GPU service and generated paths are separate gates"
+ "test_cpu_step_boundary.c|Shared TAS verify_cpu_step_generated.py compiles authored output from both emitters against production objects; cold cached/uncached boundaries and callback/trace off/on"
+ "test_source_gpu_frame_request.c|Shared TAS verify_gpu_frame_request.py compares two automatic request/return streams against five authored original-core display-range controls"
+ "test_source_gpu_pending_frame.c|Shared TAS verify_gpu_pending_frame.py compares source pending quad across automatic frame request/return; supplied CPU steps, standalone service"
+ "test_source_gpu_runtime.c|Shared TAS verify_gpu_runtime.py links production scheduler and actual generated/interpreted CPU paths with independent source pending-quad state and tracing off/on"
+ "test_source_gpu_dma_runtime.c|Shared TAS verify_gpu_dma_runtime.py checks actual GPU reader, DMA controller and scheduler against retained authored scalar/readback states"
+ "test_source_gpu_environment.c|Shared TAS verify_gpu_environment.py compares cold ordinary environment/reset history through the actual GPU/DMA scheduler with original-source scalar states"
+ "test_source_gpu_cpu.c|Shared TAS verify_gpu_cpu.py executes authored RAM programs through production dirty-RAM CPU, GPU/DMA service and VRAM observations after a declared synthetic prologue"
+ "test_source_gpu_reset_projection.c|Shared TAS verify_gpu_reset_projection.py checks four ordinary original-source reset states at O0/O2 and unsupported draw-state rejection"
+ "test_dma_gpu_upload_source.c|authored fixture driven by tas-replay-research/verify_gpu_upload.py, with exact-source vectors and separate O0/O2 runs"
+ "test_icache_isolated_store.c|authored fixture driven by tas-replay-research/verify_cache_store.py, with exact-source vectors and separate O0/O2 runs"
+ "test_input_histogram_start.c|authored fixture driven by tas-replay-research/verify_histogram_start.py, with exact-source vectors and separate O0/O2 runs"
+ "test_irq_vector_entry.c|authored fixture driven by tas-replay-research/verify_irq_vector.py, with exact-source vectors and separate O0/O2 runs"
+ "test_memory_dma_read_wait.c|authored fixture driven by tas-replay-research/verify_gpu_upload.py, with exact-source vectors and separate O0/O2 runs"
+ "test_mmio_read_sample_order.c|authored fixture driven by tas-replay-research/verify_mmio_read_order.py, with exact-source vectors and separate O0/O2 runs"
+ "test_muldiv_deferred.c|authored fixture driven by tas-replay-research/verify_muldiv.py, with exact-source vectors and separate O0/O2 runs"
+ "test_timer1_raster_integration.c|authored fixture driven by tas-replay-research/verify_timer1.py, with exact-source vectors and separate O0/O2 runs"
+ "test_timer1_source_clock.c|authored fixture driven by tas-replay-research/verify_timer1.py, with exact-source vectors and separate O0/O2 runs"
+ "test_timer2_divider_native.c|exploratory divider probe documented in tas-replay-research/TIMER2-SOURCE-CLOCK.md; not driven by the portable verify_timer2.py regression"
+ "test_timer2_source_clock.c|authored fixture driven by tas-replay-research/verify_timer2.py, with exact-source vectors and separate O0/O2 runs"
+ "test_timer2_source_scheduler.c|authored fixture driven by tas-replay-research/verify_timer2.py, with exact-source vectors and separate O0/O2 runs"
"test_overlay_posix.c|built and run by tests/run_overlay_posix_test.sh, which stages the dlopen fixture tree; that script is registered as overlay_posix_test on UNIX"
)
diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h
index fa223777e..db7422096 100644
--- a/runtime/include/cdrom.h
+++ b/runtime/include/cdrom.h
@@ -152,6 +152,8 @@ void cdrom_tick(void);
/* DMA channel 3 interface */
uint32_t cdrom_dma_read(void);
+/* Source-compatible DMA byte drain: unavailable bytes are zero. */
+uint32_t cdrom_dma_read_padded(void);
int cdrom_dma_ready(void);
uint32_t cdrom_dma_sector_word_count(void);
@@ -235,6 +237,8 @@ typedef struct CDROMSectorDebugState {
typedef struct CDROMTraceEntry {
uint64_t seq;
+ uint64_t cycle;
+ uint32_t guest_ra;
uint32_t addr;
uint32_t val;
uint32_t func;
@@ -299,6 +303,9 @@ typedef struct CDROMCommandHistoryEntry {
uint8_t pending_pending;
uint8_t queued_cmd;
uint8_t queued_pending;
+ uint8_t source_clock;
+ uint32_t source_random_cursor;
+ uint32_t source_random_calls;
} CDROMCommandHistoryEntry;
typedef struct CDROMSectorHistoryEntry {
diff --git a/runtime/include/cdrom_random_tape.h b/runtime/include/cdrom_random_tape.h
new file mode 100644
index 000000000..c02ddcd1e
--- /dev/null
+++ b/runtime/include/cdrom_random_tape.h
@@ -0,0 +1,70 @@
+#ifndef PSX_CDROM_RANDOM_TAPE_H
+#define PSX_CDROM_RANDOM_TAPE_H
+/* Generic bounded word source for explicit device-clock experiments.
+ * This reader contains no generator algorithm, seed, title or movie data. */
+#include
+#include
+#include
+#include
+#include "psx_sha256.h"
+
+typedef struct CdRandomTape {
+ uint8_t *bytes;
+ uint32_t count, cursor;
+ uint8_t sha256[32];
+} CdRandomTape;
+
+static uint32_t cd_tape_le32(const uint8_t *p) {
+ return (uint32_t)p[0] | (uint32_t)p[1]<<8 | (uint32_t)p[2]<<16 | (uint32_t)p[3]<<24;
+}
+
+static int cd_tape_load(CdRandomTape *tape, const char *path) {
+ static const uint8_t magic[16] = "PSX-CD-RNG1";
+ uint8_t header[20];
+ CdRandomTape next = {0};
+ FILE *file = fopen(path,"rb");
+ if (!file) return 0;
+ if (fread(header,1,sizeof(header),file)!=sizeof(header) || memcmp(header,magic,16)) {
+ fclose(file); return 0;
+ }
+ next.count=cd_tape_le32(header+16);
+ if (!next.count || next.count>1048576u) { fclose(file); return 0; }
+ size_t size=(size_t)next.count*4u;
+ next.bytes=(uint8_t *)malloc(size);
+ if (!next.bytes) { fclose(file); return 0; }
+ if (fread(next.bytes,1,size,file)!=size || fgetc(file)!=EOF || ferror(file)) {
+ free(next.bytes); fclose(file); return 0;
+ }
+ fclose(file);
+ psx_sha256_ctx hash;
+ psx_sha256_init(&hash);
+ psx_sha256_update(&hash,header,sizeof(header));
+ psx_sha256_update(&hash,next.bytes,size);
+ psx_sha256_final(&hash,next.sha256);
+ free(tape->bytes);
+ *tape=next;
+ return 1;
+}
+
+static int cd_tape_bounded(CdRandomTape *tape, uint32_t maximum, uint32_t *value) {
+ uint32_t mask=maximum;
+ for (unsigned shift=1;shift<32;shift*=2) mask |= mask>>shift;
+ while (tape->cursorcount) {
+ uint32_t candidate=cd_tape_le32(tape->bytes+(size_t)tape->cursor++*4u)&mask;
+ if (candidate<=maximum) { *value=candidate; return 1; }
+ }
+ return 0; /* Exhaustion never wraps or supplies a made-up value. */
+}
+
+static int cd_tape_restore(CdRandomTape *tape, const uint8_t identity[32],
+ uint32_t count, uint32_t cursor) {
+ if (count!=tape->count || cursor>count || memcmp(identity,tape->sha256,32)) return 0;
+ tape->cursor=cursor;
+ return 1;
+}
+
+static void cd_tape_reset(CdRandomTape *tape) { tape->cursor=0; }
+static void cd_tape_free(CdRandomTape *tape) {
+ free(tape->bytes); memset(tape,0,sizeof(*tape));
+}
+#endif
diff --git a/runtime/include/debug_server.h b/runtime/include/debug_server.h
index 9b7a6eb55..1f10109b9 100644
--- a/runtime/include/debug_server.h
+++ b/runtime/include/debug_server.h
@@ -83,6 +83,10 @@ typedef struct {
* port=0 uses the default (4370). */
void debug_server_init(int port);
+/* Opt-in PSXRTI1 route, validated before guest execution. First record is
+ * consumed at the first normal VBlank input-sampling boundary. */
+int debug_server_preload_input_route(const char *path);
+
/* Current listener status for heartbeat diagnostics. error is the platform
* socket error captured by init, or 0 when the listener is active. */
void debug_server_get_status(int *listening, int *port, int *error);
@@ -129,6 +133,7 @@ extern uint32_t g_debug_current_func_addr;
* writes into the 1M-entry ring buffer with function + frame attribution. */
void debug_server_trace_write_check(uint32_t phys, uint32_t old_val,
uint32_t new_val, uint8_t width);
+uint16_t debug_server_update_poll(int slot, uint16_t buttons, int analog);
/* MMIO write trace — separate ring buffer for 0x1F801xxx writes. */
void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width);
@@ -202,6 +207,8 @@ int debug_server_get_axis_override(unsigned char st[4]);
/* TCP-controlled turbo mode. When enabled the frontend skips presentation and
* wall-clock pacing at vblank, matching the keyboard TAB turbo path. */
int debug_server_turbo_enabled(void);
+/* File playback only: assert actual P1 SIO state after normal VBlank sampling. */
+void debug_server_note_input_applied(void);
/* TCP-controlled turbo mode. When enabled the frontend skips presentation and
* wall-clock pacing at vblank, matching the keyboard TAB turbo path. */
@@ -233,6 +240,8 @@ void debug_server_send_fmt(const char *fmt, ...);
* ring head. Forensic value is in the bulk of older entries which are
* stable. */
void debug_server_freeze_dump_wtrace_all_json(FILE *f, uint32_t max_count);
+/* Passive scalar filter of existing write history; no new recording or RAM reads. */
+void debug_server_dump_watched_writes(FILE *f, const uint32_t *u16_addresses, uint32_t count);
void debug_server_freeze_dump_wtrace_json(FILE *f, uint32_t max_count);
void debug_server_freeze_dump_mmio_json(FILE *f, uint32_t max_count);
void debug_server_freeze_dump_frame_history_json(FILE *f, uint32_t max_count);
diff --git a/runtime/include/dma.h b/runtime/include/dma.h
index dccddd3da..dc2e50e4b 100644
--- a/runtime/include/dma.h
+++ b/runtime/include/dma.h
@@ -18,12 +18,24 @@
#define PSXRECOMP_DMA_H
#include
-
#ifdef __cplusplus
extern "C" {
#endif
+/* Source service owner: GPU, CD, SPU and OTC channel work at the supplied actual
+ * runtime cycle, including DMA writes and frame returns between periodic
+ * events. No register/input/state override is performed. */
+void dma_source_gpu_service_at(uint64_t cycle);
+
void dma_init(void);
+uint32_t dma_cpu_read_penalty(void); /* Optional source upload's bus-read wait. */
+/* Source-profile CPU owner calls after prior-instruction service, before fetch.
+ * Direct device fixtures retain the live wait until this owner is entered. */
+void dma_cpu_read_wait_boundary(void);
+/* Source-profile CPU owns the halt between instruction fetch attempts. */
+int dma_cpu_otc_halted(void);
+/* OTC or manual, non-chopped CD DMA in the source profile. */
+int dma_cpu_source_halted(void);
uint32_t dma_read(uint32_t addr);
void dma_write(uint32_t addr, uint32_t val);
void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask);
diff --git a/runtime/include/event_ring.h b/runtime/include/event_ring.h
index 1f82f31a3..115b40038 100644
--- a/runtime/include/event_ring.h
+++ b/runtime/include/event_ring.h
@@ -23,6 +23,7 @@
#define PSXRECOMP_EVENT_RING_H
#include
+#include
#ifdef __cplusplus
extern "C" {
@@ -114,6 +115,9 @@ void event_ring_record_aux(uint16_t kind, uint8_t detail, uint32_t aux);
/* Write the whole live window (oldest->newest) as a JSON array to `path`.
* Returns entries written, or -1 on open failure. No TCP size limit. */
int event_ring_dump_file(const char *path);
+/* Same retained window, written to a caller-owned stream. Does not close it.
+ * Allows capture tools to enforce exclusive creation before exporting. */
+int event_ring_dump_stream(FILE *stream);
/* Emit a bounded JSON tail (most-recent `max_entries`, oldest->newest) into
* `out`. Returns bytes written. For quick TCP inspection. */
diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h
index bee1e5759..dd81ab7b5 100644
--- a/runtime/include/gpu.h
+++ b/runtime/include/gpu.h
@@ -16,6 +16,10 @@ extern "C" {
#endif
void gpu_init(void);
+uint32_t gpu_dma_vram_upload_words(void); /* Active GP0(A0) payload remaining. */
+/* Experimental LL gate: 1 idle, 0 qualified mono-polyline wait, -1 outside
+ * scope. This does not represent source draw budget or FIFO occupancy. */
+int gpu_dma_source_ll_ready(void);
uint32_t gpu_read_gpustat(void); /* 0x1F801814 read */
uint32_t gpu_read_gpuread(void); /* 0x1F801810 read */
void gpu_write_gp0(uint32_t val); /* 0x1F801810 write */
@@ -52,6 +56,7 @@ void gpu_texture_correction_stats(uint64_t *attempts, uint64_t *armed,
* both wastes bandwidth and force-flushes when UP_RECTS_MAX is hit (MotK FMV). */
int gpu_display_is_depth24(void);
int gpu_video_standard_is_pal(void); /* GP1(08h) bit 3: 0 NTSC, 1 PAL */
+int gpu_display_is_interlaced(void); /* GP1(08h) bit 5, passive accessor */
void gpu_display_pixel_rgb(const GpuDisplayInfo* di, uint32_t x, uint32_t y,
uint8_t* r, uint8_t* g, uint8_t* b);
uint32_t gpu_display_pixel_argb(const GpuDisplayInfo* di, uint32_t x, uint32_t y);
diff --git a/runtime/include/gpu_render.h b/runtime/include/gpu_render.h
index ace31f119..b0e623925 100644
--- a/runtime/include/gpu_render.h
+++ b/runtime/include/gpu_render.h
@@ -12,6 +12,7 @@
* (a requested backend that fails to initialize falls back to software). */
#include
+#include "source_gpu_texture.h"
#ifdef __cplusplus
extern "C" {
@@ -56,6 +57,13 @@ void gr_set_precise_triangle(int enabled,
void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2);
/* Primitives */
+/* Experimental native-software source-profile polygon path; full RGB888
+ * interpolation, field skip and shared directed span/work traversal. */
+int gr_draw_source_block(const SourceGPUBlock *block,int *extra_work);
+void gr_source_texture_control(unsigned action,uint32_t page);
+int gr_draw_source_triangle(const int *x,const int *y,const uint32_t *colors,
+ int shaded,int dither,int interlace,unsigned skip_field,
+ const SourceGPUTexture *texture,int *extra_work);
void gr_fill_rect(int x, int y, int w, int h, uint16_t color);
void gr_copy_rect(int src_x, int src_y, int dst_x, int dst_y, int w, int h);
void gr_draw_flat_triangle(int x0, int y0, int x1, int y1, int x2, int y2,
diff --git a/runtime/include/gpu_sw_renderer.h b/runtime/include/gpu_sw_renderer.h
index b8cea0e3a..6089b36b2 100644
--- a/runtime/include/gpu_sw_renderer.h
+++ b/runtime/include/gpu_sw_renderer.h
@@ -2,6 +2,7 @@
#define PSX_GPU_SW_RENDERER_H
#include
+#include "source_gpu_texture.h"
#ifdef __cplusplus
extern "C" {
@@ -11,6 +12,11 @@ extern "C" {
#define SW_MAX_INTERNAL_SCALE 4
/* Initialize software renderer */
+int sw_draw_source_block(const SourceGPUBlock *block,int *extra_work);
+void sw_source_texture_control(unsigned action,uint32_t page);
+int sw_draw_source_triangle(const int *x,const int *y,const uint32_t *colors,
+ int shaded,int dither,int interlace,unsigned skip_field,
+ const SourceGPUTexture *texture,int *extra_work);
void sw_renderer_init(uint16_t* vram);
/* Internal-resolution supersampling (SSAA).
diff --git a/runtime/include/input_instruction_histogram.h b/runtime/include/input_instruction_histogram.h
new file mode 100644
index 000000000..a0953517b
--- /dev/null
+++ b/runtime/include/input_instruction_histogram.h
@@ -0,0 +1,13 @@
+#ifndef PSX_INPUT_INSTRUCTION_HISTOGRAM_H
+#define PSX_INPUT_INSTRUCTION_HISTOGRAM_H
+#include
+#ifdef __cplusplus
+extern "C" {
+#endif
+extern int g_input_instruction_histogram_active;
+extern void (*g_input_instruction_histogram_callback)(uint32_t pc);
+void input_instruction_histogram_sample(uint32_t pc);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/runtime/include/input_instruction_histogram_impl.h b/runtime/include/input_instruction_histogram_impl.h
new file mode 100644
index 000000000..f366e9efe
--- /dev/null
+++ b/runtime/include/input_instruction_histogram_impl.h
@@ -0,0 +1,151 @@
+/* Private debug-server implementation. Test-configured passive fetch counts.
+ * No opcodes, registers, input words or guest memory are changed. */
+#include "input_instruction_histogram.h"
+static void input_instruction_histogram_record(uint32_t pc);
+typedef struct {uint32_t pc,count;int used;uint64_t first_sequence,first_cycle,first_effective_cycle;uint32_t first_cache_tag;int first_cache_active;} InputInstructionHistogramRow;
+static InputInstructionHistogramRow s_instruction_rows[16384];
+static uint32_t s_instruction_first,s_instruction_last,s_instruction_frame;
+static int s_instruction_configured;
+static int s_instruction_partial;
+static void input_instruction_histogram_boundary(uint32_t frame);
+static void input_instruction_histogram_exit(void) {
+ if(!g_input_instruction_histogram_active)return;
+ s_instruction_partial=1;
+ input_instruction_histogram_boundary(UINT32_MAX);
+}
+static uint64_t s_instruction_events,s_instruction_dropped,s_instruction_cycle;
+typedef struct {uint32_t pc;uint64_t sequence,cycle,effective_cycle;uint32_t cache_tag;int cache_active;} InputInstructionSiteRow;
+typedef struct {int precise,in_exception;uint32_t istat,imask,last_block,last_committed;uint64_t slices,takes;} InputInstructionContextRow;
+static InputInstructionContextRow s_instruction_context_rows[8192];
+extern int g_precise_mode;
+extern uint32_t i_stat,i_mask,g_slice_last_block,g_slice_last_committed;
+extern uint64_t g_slice_fired,g_slice_irq_taken;
+extern int psx_get_in_exception(void);
+extern uint32_t g_psx_cyc_batch;
+extern uint32_t *g_psx_cyc_local_acc;
+extern uint32_t g_psx_icache_tv[1024];
+extern int g_psx_icache_active;
+/* Observe the pending clock without publishing it or servicing a device. */
+static uint64_t input_instruction_effective_cycle(void) {
+ return psx_cycle_count + g_psx_cyc_batch +
+ (g_psx_cyc_local_acc ? (uint64_t)*g_psx_cyc_local_acc : 0u);
+}
+static uint32_t s_instruction_sites[16],s_instruction_site_count;
+static InputInstructionSiteRow s_instruction_site_rows[8192];
+static uint32_t s_instruction_site_retained,s_instruction_site_dropped;
+static int input_instruction_sites_parse(const char *p) {
+ s_instruction_site_count=0;
+ if(!p)return 1;
+ for(;;) {
+ uint32_t pc=0;
+ if(s_instruction_site_count==16)return 0;
+ for(unsigned i=0;i<8;i++) {
+ unsigned c=(unsigned char)*p++,v;
+ if(c>='0' && c<='9')v=c-'0';
+ else if(c>='A' && c<='F')v=c-'A'+10;
+ else if(c>='a' && c<='f')v=c-'a'+10;
+ else return 0;
+ pc=(pc<<4)|v;
+ }
+ if(pc&3)return 0;
+ for(unsigned i=0;i7 || last>60000)return 0;
+ if(!input_instruction_sites_parse(sites))return 0;
+ if(!s_instruction_configured && atexit(input_instruction_histogram_exit))return 0;
+ s_instruction_first=first;s_instruction_last=last;s_instruction_configured=1;
+ g_input_instruction_histogram_callback=input_instruction_histogram_record;
+ /* Configuration runs before guest execution. Interval zero ends at the
+ * first runtime VBlank; it previously fell outside every fetch capture. */
+ if(first==0)input_instruction_histogram_boundary(0);
+ return 1;
+}
+static void input_instruction_histogram_record(uint32_t pc) {
+ if(!g_input_instruction_histogram_active || g_ls_replay_active)return;
+ if(s_instruction_events==1000000){s_instruction_dropped++;return;}
+ for(unsigned i=0;i>2],g_psx_icache_active};
+ s_instruction_context_rows[n]=(InputInstructionContextRow){g_precise_mode,psx_get_in_exception(),i_stat,i_mask,g_slice_last_block,g_slice_last_committed,g_slice_fired,g_slice_irq_taken};
+ }
+ break;
+ }
+ uint32_t slot=((pc>>2)*2654435761u)&16383u;
+ for(unsigned n=0;n<16384;n++,slot=(slot+1)&16383u) {
+ InputInstructionHistogramRow *row=&s_instruction_rows[slot];
+ if(!row->used || row->pc==pc){
+ if(!row->used){row->first_sequence=s_instruction_events;row->first_cycle=psx_cycle_count;row->first_effective_cycle=input_instruction_effective_cycle();row->first_cache_tag=g_psx_icache_tv[(pc & 0xFFCu)>>2];row->first_cache_active=g_psx_icache_active;}
+ row->used=1;row->pc=pc;row->count++;s_instruction_events++;return;
+ }
+ }
+ s_instruction_dropped++;
+}
+static void input_instruction_histogram_boundary(uint32_t frame) {
+ if(g_input_instruction_histogram_active) {
+ const char *prefix=s_instruction_partial?"partial-":"";
+ char name[64];snprintf(name,sizeof(name),"%sinstructions-%06u.tsv",prefix,s_instruction_frame);
+ FILE *f=input_route_observer_output(name);
+ fprintf(f,"# retained=%llu dropped=%llu start_cycle=%llu end_cycle=%llu\npc\tcount\n",
+ (unsigned long long)s_instruction_events,(unsigned long long)s_instruction_dropped,
+ (unsigned long long)s_instruction_cycle,(unsigned long long)psx_cycle_count);
+ for(unsigned i=0;i<16384;i++)if(s_instruction_rows[i].used)
+ fprintf(f,"%08X\t%u\n",s_instruction_rows[i].pc,s_instruction_rows[i].count);
+ if(fclose(f)){fprintf(stderr,"instruction histogram close failed\n");exit(4);}
+ snprintf(name,sizeof(name),"%sinstruction-first-%06u.tsv",prefix,s_instruction_frame);
+ f=input_route_observer_output(name);
+ fprintf(f,"# first observed cache-fetch site per PC; not complete retired execution; effective clock includes unpublished charges\npc\tfirst_sequence\tfirst_cycle\teffective_cycle\tcache_tag\tcache_active\n");
+ for(unsigned i=0;i<16384;i++)if(s_instruction_rows[i].used)
+ fprintf(f,"%08X\t%llu\t%llu\t%llu\t%08X\t%d\n",s_instruction_rows[i].pc,
+ (unsigned long long)s_instruction_rows[i].first_sequence,
+ (unsigned long long)s_instruction_rows[i].first_cycle,
+ (unsigned long long)s_instruction_rows[i].first_effective_cycle,
+ s_instruction_rows[i].first_cache_tag,s_instruction_rows[i].first_cache_active);
+ if(fclose(f)){fprintf(stderr,"histogram first-site close failed\n");exit(4);}
+ if(s_instruction_site_count) {
+ snprintf(name,sizeof(name),"%sinstruction-sites-%06u.tsv",prefix,s_instruction_frame);
+ f=input_route_observer_output(name);
+ fprintf(f,"# selected cache-fetch sites; retained=%u dropped=%u; cycles before fetch; effective clock includes unpublished charges\npc\tsequence\tcycle\teffective_cycle\tcache_tag\tcache_active\n",s_instruction_site_retained,s_instruction_site_dropped);
+ for(unsigned i=0;ipc,(unsigned long long)row->sequence,(unsigned long long)row->cycle,(unsigned long long)row->effective_cycle,row->cache_tag,row->cache_active);
+ }
+ if(fclose(f)){fprintf(stderr,"instruction sites close failed\n");exit(4);}
+ snprintf(name,sizeof(name),"%sinstruction-context-%06u.tsv",prefix,s_instruction_frame);
+ f=input_route_observer_output(name);
+ fprintf(f,"# passive context at selected pre-fetch sites; same sequence as instruction-sites; no device service\npc\tsequence\tprecise\tin_exception\ti_stat\ti_mask\tslice_block\tslice_committed\tslices\ttakes\n");
+ for(unsigned i=0;ipc,(unsigned long long)site->sequence,row->precise,row->in_exception,row->istat,row->imask,row->last_block,row->last_committed,(unsigned long long)row->slices,(unsigned long long)row->takes);
+ }
+ if(fclose(f)){fprintf(stderr,"instruction context close failed\n");exit(4);}
+ }
+ snprintf(name,sizeof(name),"%sdma-at-%06u.json",prefix,s_instruction_frame);
+ f=input_route_observer_output(name);
+ debug_server_freeze_dump_dma_trace_json(f,128);
+ if(fclose(f)){fprintf(stderr,"histogram DMA context close failed\n");exit(4);}
+ snprintf(name,sizeof(name),"%ssio-at-%06u.json",prefix,s_instruction_frame);
+ f=input_route_observer_output(name);
+ debug_server_freeze_dump_sio_pc_json(f,128);
+ if(fclose(f)){fprintf(stderr,"histogram SIO context close failed\n");exit(4);}
+ g_input_instruction_histogram_active=0;
+ }
+ if(s_instruction_configured && frame>=s_instruction_first && frame<=s_instruction_last) {
+ memset(s_instruction_rows,0,sizeof(s_instruction_rows));
+ s_instruction_frame=frame;s_instruction_events=s_instruction_dropped=0;
+ s_instruction_site_retained=s_instruction_site_dropped=0;
+ s_instruction_cycle=psx_cycle_count;g_input_instruction_histogram_active=1;
+ }
+}
diff --git a/runtime/include/input_route_field_clock.h b/runtime/include/input_route_field_clock.h
new file mode 100644
index 000000000..15530365c
--- /dev/null
+++ b/runtime/include/input_route_field_clock.h
@@ -0,0 +1,43 @@
+#ifndef PSX_INPUT_ROUTE_FIELD_CLOCK_H
+#define PSX_INPUT_ROUTE_FIELD_CLOCK_H
+
+#include
+
+/* Cold-boot comparison clock, independent of guest memory and input contents.
+ * Behavioral constants: original Octoshock 2.2.2 GPU, NTSC hardware clock
+ * ratio 103896/65536; alternating 3412/3413 GPU clocks per scanline;
+ * 263 progressive lines, 263/262 interlaced lines. This is a field-duration
+ * profile, not its complete scanline/IRQ/CPU timing implementation.
+ * Independently written arithmetic; no reference implementation copied.
+ */
+typedef struct InputRouteFieldClock {
+ uint32_t remainder;
+ uint32_t line_phase;
+ uint32_t field;
+ uint32_t current_cycles;
+} InputRouteFieldClock;
+
+static inline void input_route_field_clock_select(InputRouteFieldClock *s,
+ int interlaced) {
+ uint32_t lines = interlaced ? 263u - s->field : 263u;
+ uint32_t gpu_clocks = lines * 3412u + (lines + s->line_phase) / 2u;
+ uint64_t numerator = (uint64_t)gpu_clocks * 65536u;
+ /* Keep cumulative deadlines at ceil(total GPU clocks / clock ratio).
+ * Represent the remainder as unused fractional CPU-cycle credit. */
+ numerator -= s->remainder;
+ s->current_cycles = (uint32_t)((numerator + 103895u) / 103896u);
+ s->remainder = (uint32_t)((uint64_t)s->current_cycles * 103896u - numerator);
+ s->line_phase = (s->line_phase + lines) & 1u;
+}
+
+static inline void input_route_field_clock_reset(InputRouteFieldClock *s) {
+ s->remainder = s->line_phase = s->field = 0u;
+ input_route_field_clock_select(s, 0);
+}
+
+static inline void input_route_field_clock_next(InputRouteFieldClock *s,
+ int interlaced) {
+ s->field = interlaced ? !s->field : 0u;
+ input_route_field_clock_select(s, interlaced);
+}
+#endif
diff --git a/runtime/include/input_route_file.h b/runtime/include/input_route_file.h
new file mode 100644
index 000000000..676d1a918
--- /dev/null
+++ b/runtime/include/input_route_file.h
@@ -0,0 +1,52 @@
+#ifndef PSX_INPUT_ROUTE_FILE_H
+#define PSX_INPUT_ROUTE_FILE_H
+
+/* Bounded PSXRTI1 digital route reader. Source-owned, no retail data.
+ * Parse into caller staging storage; publish only after the complete file
+ * passes. No guest state, clocks or live input are touched by this parser. */
+#include
+#include
+#include
+
+#define INPUT_ROUTE_MAX_STEPS 4096u
+#define INPUT_ROUTE_MAX_FRAMES 1000000u
+typedef struct { uint32_t frames; uint16_t buttons; } InputRouteStep;
+
+static inline uint32_t input_route_le32(const unsigned char *p)
+{
+ return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
+ ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
+}
+
+static inline const char *input_route_read(FILE *f, InputRouteStep *steps,
+ uint32_t *step_count,
+ uint32_t *frame_count)
+{
+ unsigned char h[24], r[8];
+ uint32_t n, count = 0;
+ *step_count = *frame_count = 0;
+ if (!f || fread(h, 1, sizeof(h), f) != sizeof(h)) return "short header";
+ if (memcmp(h, "PSXRTI1\0", 8) || input_route_le32(h+8) != 1 ||
+ input_route_le32(h+12) != 8) return "header identity";
+ n = input_route_le32(h+16);
+ if (input_route_le32(h+20) || !n || n > INPUT_ROUTE_MAX_FRAMES)
+ return "frame count";
+ for (uint32_t i = 0; i < n; ++i) {
+ uint16_t pad;
+ if (fread(r, 1, sizeof(r), f) != sizeof(r)) return "short record";
+ if (input_route_le32(r) != i+1 || r[6] || r[7])
+ return "record sequence/reserved";
+ pad = (uint16_t)((unsigned)r[4] | ((unsigned)r[5] << 8));
+ if (count && steps[count-1].buttons == pad) ++steps[count-1].frames;
+ else {
+ if (count == INPUT_ROUTE_MAX_STEPS) return "step capacity";
+ steps[count].buttons = pad;
+ steps[count++].frames = 1;
+ }
+ }
+ if (fgetc(f) != EOF || ferror(f)) return "trailing bytes/read error";
+ *step_count = count;
+ *frame_count = n;
+ return NULL;
+}
+#endif
diff --git a/runtime/include/input_route_observer.h b/runtime/include/input_route_observer.h
new file mode 100644
index 000000000..23453eaa2
--- /dev/null
+++ b/runtime/include/input_route_observer.h
@@ -0,0 +1,11 @@
+#ifndef PSX_INPUT_ROUTE_OBSERVER_H
+#define PSX_INPUT_ROUTE_OBSERVER_H
+#include
+#include
+FILE *input_route_observer_output(const char *name);
+int input_route_observer_init(uint32_t total);
+void input_route_observer_set_end(uint32_t total);
+void input_route_observer_boundary(uint32_t completed, uint64_t runtime_frame);
+void input_route_observer_input(uint16_t buttons);
+void input_route_observer_applied(uint16_t buttons, int connected, int analog);
+#endif
diff --git a/runtime/include/input_route_raster_clock.h b/runtime/include/input_route_raster_clock.h
new file mode 100644
index 000000000..cd2b0cc3e
--- /dev/null
+++ b/runtime/include/input_route_raster_clock.h
@@ -0,0 +1,108 @@
+#ifndef PSX_INPUT_ROUTE_RASTER_CLOCK_H
+#define PSX_INPUT_ROUTE_RASTER_CLOCK_H
+#include
+#include
+
+/* Independently expressed original Octoshock 2.2.2 NTSC raster timing.
+ * Optional comparison clock only: no GPU rendering, implicit timer synchronization,
+ * readiness flags, input policy or guest memory is implemented here.
+ * Source gpu.cpp constants/transition rules; no reference source text copied.
+ */
+typedef struct InputRouteRasterClock {
+ uint64_t cycle, last_rise;
+ uint32_t fraction, remaining, phase, alternate;
+ uint32_t scanline, lines, field, mode, start, end, blank;
+ uint32_t rises;
+ uint32_t y_start, y_offset, readout_y, readout_field;
+} InputRouteRasterClock;
+
+static inline void input_route_raster_reset(InputRouteRasterClock *s) {
+ memset(s, 0, sizeof(*s));
+ s->remaining=3212; s->lines=263; s->start=16; s->end=256; s->blank=1;
+}
+
+typedef void (*InputRouteRasterEvent)(void *, uint64_t, unsigned, int);
+/* Events: 1 = H-retrace rising edge, 2 = VBlank level change. The callback
+ * observes ordered edge deadlines. Null keeps the original scalar-only path. */
+static inline void input_route_raster_advance_observed(InputRouteRasterClock *s, uint32_t cycles, InputRouteRasterEvent event, void *context) {
+ uint64_t end_cycle=s->cycle+cycles;
+ uint64_t total=(uint64_t)s->fraction+(uint64_t)cycles*103896u;
+ uint64_t ticks=total>>16;
+ s->fraction=(uint32_t)(total&65535u);
+ while (ticks) {
+ uint32_t step=ticksremaining ? (uint32_t)ticks : s->remaining;
+ ticks-=step; s->remaining-=step;
+ if (s->remaining) continue;
+ s->phase^=1;
+ if (s->phase) {
+ if(event) event(context,end_cycle-(ticks*65536u+s->fraction)/103896u,1,s->blank);
+ s->remaining=200; continue;
+ }
+ s->remaining=3212+s->alternate; s->alternate^=1;
+ s->scanline=(s->scanline+1)%s->lines;
+ if (s->scanline==s->lines-1) s->field=(s->mode&32) ? !s->field : 0;
+ if (!s->scanline) {
+ if (s->mode&32) s->lines=263-s->field;
+ else { s->field=0; s->lines=263; }
+ }
+ uint32_t old_blank=s->blank;
+ if (s->scanline==s->end && !s->blank) {
+ s->blank=1; s->y_offset=0;
+ s->readout_field=(s->mode&0x24)==0x24 ? !s->field : 0;
+ }
+ if (s->scanline==s->start && s->blank) s->blank=0;
+ if(event && old_blank!=s->blank)
+ event(context,end_cycle-(ticks*65536u+s->fraction)/103896u,2,s->blank);
+ if (!old_blank && s->blank) {
+ s->rises++;
+ s->last_rise=end_cycle-(ticks*65536u+s->fraction)/103896u;
+ }
+ if ((s->mode&0x24)==0x24)
+ s->readout_y=(s->y_start+2*s->y_offset+(s->blank ? 0 : s->readout_field))&511u;
+ else s->readout_y=(s->y_start+s->y_offset)&511u;
+ if (!s->blank) s->y_offset=(s->y_offset+1)&511u;
+ }
+ s->cycle=end_cycle;
+}
+
+static inline void input_route_raster_advance(InputRouteRasterClock *s, uint32_t cycles) {
+ input_route_raster_advance_observed(s,cycles,0,0);
+}
+
+/* Call after the existing device-MMIO synchronization. Unknown non-raster
+ * GP1 commands leave this clock unchanged. PAL is an explicit rejection.
+ */
+static inline int input_route_raster_gp1(InputRouteRasterClock *s, uint32_t word) {
+ uint32_t command=(word>>24)&63u;
+ if (!command) { s->mode=0; s->start=16; s->end=256; s->y_start=0; }
+ else if (command==5) s->y_start=(word>>10)&511u;
+ else if (command==7) { s->start=word&1023u; s->end=(word>>10)&1023u; }
+ else if (command==8) {
+ if (word&8) return 0;
+ s->mode=word&255u;
+ }
+ return 1;
+}
+
+/* Independent field and display-line parity bits from the raster state.
+ * This does not assert command/FIFO/DMA readiness or alter the raster clock.
+ */
+static inline uint32_t input_route_raster_status(const InputRouteRasterClock *s) {
+ return ((s->readout_y&1u)<<31) | ((!s->field)<<13);
+}
+
+/* Deadline search over a copy. Cache this at the runtime boundary, invalidate
+ * on GP1 reset/range/mode writes and after a rising edge. No fabricated edge
+ * for an invalid/empty display range: UINT32_MAX means none within two fields.
+ */
+static inline uint32_t input_route_raster_until_rise(const InputRouteRasterClock *s) {
+ InputRouteRasterClock t=*s;
+ for (uint32_t i=0; i<1060; ++i) {
+ uint64_t numerator=(uint64_t)t.remaining*65536u-t.fraction;
+ uint32_t cycles=(uint32_t)((numerator+103895u)/103896u);
+ input_route_raster_advance(&t,cycles);
+ if (t.rises!=s->rises) return (uint32_t)(t.last_rise-s->cycle);
+ }
+ return UINT32_MAX;
+}
+#endif
diff --git a/runtime/include/input_update_clock.h b/runtime/include/input_update_clock.h
new file mode 100644
index 000000000..b1c2b7616
--- /dev/null
+++ b/runtime/include/input_update_clock.h
@@ -0,0 +1,134 @@
+#ifndef PSX_INPUT_UPDATE_CLOCK_H
+#define PSX_INPUT_UPDATE_CLOCK_H
+#include
+/* Experimental two-stage controller clock. No guest state and no title data.
+ * The caller must verify the packet at its configured acceptance marker.
+ * A poll prepares one packet ahead; the later marker commits the older one. */
+typedef struct {
+ const uint16_t *words;
+ uint32_t count, accepted;
+ int primed, predicted, failed;
+ uint64_t last_poll, last_accept;
+ uint16_t queued_word;
+ const uint8_t *contexts; /* Optional source event types: 0 normal, 1 auxiliary. */
+ int compatible_contexts, context_failed;
+ int neutral_refresh_stage; /* 1 hold poll, 2 held acceptance, 3 refresh poll. */
+ uint64_t neutral_hold_frame;
+ uint32_t neutral_refreshes;
+ uint16_t protected_raw_mask;
+ uint16_t source_raw_hold, source_raw_press, native_raw_hold, native_raw_press;
+ uint8_t last_source_context, last_native_context;
+} InputUpdateClock;
+/* Optional prediction only. The later packet/context checks remain authoritative.
+ * A gate of one may be reset by a continuing main loop after the early poll. */
+static inline int input_update_predict(const InputUpdateClock *s, uint64_t frame,
+ uint32_t gate, int previous_accept) {
+ return gate==0 || (previous_accept && s->primed && gate==1 &&
+ frame>s->last_accept && frame-s->last_accept==1);
+}
+static inline int input_update_init(InputUpdateClock *s, const uint16_t *w, uint32_t n) {
+ *s = (InputUpdateClock){0};
+ if (!w || n < 2 || w[0] != 0xffff || w[1] != 0xffff) return 0;
+ s->words=w; s->count=n;
+ s->source_raw_hold=s->native_raw_hold=0xffff; return 1;
+}
+static inline int input_update_set_contexts(InputUpdateClock *s, const uint8_t *contexts) {
+ if (!contexts || s->primed || s->failed || contexts[0] || contexts[1]) return 0;
+ for (uint32_t i=0;icount;i++) if (contexts[i]>1) return 0;
+ s->contexts=contexts; return 1;
+}
+static inline int input_update_poll(InputUpdateClock *s, uint64_t frame, int open, uint16_t *word) {
+ *word=0xffff;
+ if (s->failed) return 0;
+ if (!s->primed || s->accepted == s->count) return 1;
+ /* A prediction may be wrong without changing the packet: equal adjacent
+ * held/neutral samples require no timing decision. Fail only when the
+ * packet already in flight would differ from the required sample. */
+ if ((s->predicted && s->queued_word != s->words[s->accepted]) ||
+ frame <= s->last_poll) { s->failed=1; return 0; }
+ s->last_poll=frame; s->predicted=!!open;
+ uint32_t next=s->accepted + (open ? 1u : 0u);
+ if (next < s->count) *word=s->words[next];
+ s->queued_word=*word;
+ return 1;
+}
+/* Modes 2/3 are a declared pair, never a prediction heuristic. Only a
+ * neutral normal sample before a protected normal press can arm the hold. */
+static inline int input_update_poll_mode(InputUpdateClock *s,uint64_t frame,int mode,uint16_t *word) {
+ if(mode==2) {
+ uint16_t raw_difference=(uint16_t)((s->source_raw_hold^s->native_raw_hold)|
+ (s->source_raw_press^s->native_raw_press));
+ if(s->neutral_refresh_stage || !s->primed || !s->compatible_contexts ||
+ s->accepted+1>=s->count || s->words[s->accepted]!=0xffff ||
+ s->contexts[s->accepted] || s->contexts[s->accepted+1] ||
+ !((uint16_t)~s->words[s->accepted+1]&s->protected_raw_mask) ||
+ (raw_difference & s->protected_raw_mask) ||
+ (raw_difference && (s->last_source_context!=0 || s->last_native_context!=1))) {s->failed=1;return 0;}
+ /* The pending ordinary neutral can release a preceding held sample
+ * later in this frame. A just-accepted compatible auxiliary may leave
+ * unprotected raw fields different; the mandatory ordinary release
+ * reconciles those fields before the all-zero refresh checks. Other
+ * unequal predecessors and any protected difference still reject. */
+ if(!input_update_poll(s,frame,0,word))return 0;
+ s->neutral_refresh_stage=1;s->neutral_hold_frame=frame;return 1;
+ }
+ if(mode==3) {
+ if(s->neutral_refresh_stage!=2 || frame!=s->neutral_hold_frame+1) {s->failed=1;return 0;}
+ if(!input_update_poll(s,frame,0,word))return 0;
+ s->neutral_refresh_stage=3;return 1;
+ }
+ if(s->neutral_refresh_stage || mode<0 || mode>1) {s->failed=1;return 0;}
+ return input_update_poll(s,frame,mode,word);
+}
+static inline int input_update_neutral_refresh(InputUpdateClock *s,uint64_t frame,uint16_t word,
+ uint32_t old_held,uint32_t new_held,uint32_t pressed) {
+ if(s->neutral_refresh_stage!=3 || frame!=s->last_poll || word!=0xffff ||
+ old_held || new_held || pressed || s->source_raw_hold!=0xffff ||
+ s->native_raw_hold!=0xffff || s->source_raw_press || s->native_raw_press) {
+ s->failed=1;return 0;
+ }
+ s->neutral_refresh_stage=0;++s->neutral_refreshes;return 1;
+}
+static inline int input_update_accept(InputUpdateClock *s, uint64_t frame, uint16_t word) {
+ if (s->failed || s->accepted == s->count || s->neutral_refresh_stage>=2) return 0;
+ if (!s->primed) {
+ if (word != 0xffff) { s->failed=1; return 0; }
+ s->primed=1; s->last_poll=frame;
+ } else if (frame != s->last_poll || frame == s->last_accept ||
+ s->queued_word != (s->neutral_refresh_stage==1 ? 0xffff :
+ s->accepted + 1 < s->count ? s->words[s->accepted + 1] : 0xffff)) {
+ s->failed=1; return 0;
+ }
+ if (word != s->words[s->accepted]) { s->failed=1; return 0; }
+ if(s->neutral_refresh_stage==1) {
+ if(word!=0xffff || frame!=s->neutral_hold_frame) {s->failed=1;return 0;}
+ s->neutral_refresh_stage=2;
+ }
+ ++s->accepted; s->predicted=0; s->last_accept=frame; return 1;
+}
+static inline int input_update_auxiliary(InputUpdateClock *s, uint16_t word, uint32_t held) {
+ if (s->failed || word != 0xffff || held != 0) { s->failed=1; return 0; }
+ return 1; /* Declared neutral refresh; never advances the normal-update clock. */
+}
+static inline int input_update_context_accept(InputUpdateClock *s, uint64_t frame,
+ uint16_t word, uint8_t context) {
+ if (!s->contexts || s->accepted==s->count || context>1 ||
+ (!s->compatible_contexts && context!=s->contexts[s->accepted])) {
+ s->context_failed=s->failed=1; return 0;
+ }
+ if(s->neutral_refresh_stage==1 && context) {s->failed=1;return 0;}
+ uint8_t source=s->contexts[s->accepted];
+ uint16_t sh=source ? s->source_raw_hold : word;
+ uint16_t sp=source ? s->source_raw_press : (uint16_t)(s->source_raw_hold & ~word);
+ uint16_t nh=context ? s->native_raw_hold : word;
+ uint16_t np=context ? s->native_raw_press : (uint16_t)(s->native_raw_hold & ~word);
+ if (s->compatible_contexts && (((sh^nh)|(sp^np)) & s->protected_raw_mask)) {
+ s->context_failed=s->failed=1; return 0;
+ }
+ if (!input_update_accept(s,frame,word)) return 0;
+ s->source_raw_hold=sh;s->source_raw_press=sp;
+ s->native_raw_hold=nh;s->native_raw_press=np;
+ s->last_source_context=source;s->last_native_context=context;
+ return 1;
+}
+#endif
diff --git a/runtime/include/input_update_replay.h b/runtime/include/input_update_replay.h
new file mode 100644
index 000000000..b262d78f8
--- /dev/null
+++ b/runtime/include/input_update_replay.h
@@ -0,0 +1,223 @@
+/* Included privately by debug_server.c after the ordinary route fields.
+ * Test-supplied configuration only; no game addresses are compiled in. */
+#include "input_update_clock.h"
+#include "psx_sha256.h"
+static InputUpdateClock s_update;
+static uint32_t s_update_config[12];
+static int s_update_enabled;
+static int s_update_previous_accept;
+static uint32_t s_refresh_guard[4]; /* pressed RAM/PC, raw-held RAM, raw-pressed RAM */
+static int s_refresh_guard_enabled,s_refresh_wait_pressed;
+static FILE *s_update_log;
+static uint64_t s_update_last_accept;
+static psx_sha256_ctx s_update_hash;
+static uint32_t s_update_decision_frames[256],s_update_decision_open[256],s_update_decision_count;
+static uint16_t update_peek16(uint32_t a) {
+ return (uint16_t)(g_psx_ram[a] | ((uint16_t)g_psx_ram[a+1]<<8));
+}
+static void update_fail(const char *reason) {
+ if (s_update_log) {
+ fprintf(s_update_log,"{\"kind\":\"failure\",\"frame\":%llu,\"accepted\":%u,\"reason\":\"%s\"}\n",
+ (unsigned long long)s_frame_count,s_update.accepted,reason);
+ fflush(s_update_log);
+ /* Preserve the existing bounded recorder on fail-stop as well as EOF.
+ * This observes only test-configured fields and never resumes execution. */
+ const uint32_t watched[] = {s_update_config[0],s_update_config[3],
+ s_update_config[4],s_update_config[5]};
+ FILE *access = input_route_observer_output("update-failure-access.jsonl");
+ if (access) {
+ debug_server_dump_watched_writes(access,watched,4);
+ fclose(access);
+ }
+ }
+ fprintf(stderr,"accepted-update replay failed: %s\n",reason); exit(4);
+}
+/* marker RAM, marker PC, marker RA, gate RAM(u32), packet RAM(u16),
+ * header RAM(u16), expected header, total VBlank watchdog, stalled watchdog. */
+static int update_configure(const InputRouteStep *steps,uint32_t count,uint32_t total) {
+ const char *p=getenv("PSX_INPUT_UPDATE_CLOCK");
+ if (!p) return 1;
+ for (unsigned i=0;i<12;i++) {
+ char *end; if (*p<'0'||*p>'9') return 0;
+ unsigned long long v=strtoull(p,&end,0);
+ if (v>0xffffffffull || end==p || (i==11 ? *end!=0 : *end!=',')) return 0;
+ s_update_config[i]=(uint32_t)v; p=end+(i<11);
+ }
+ const unsigned ram_fields[]={0,3,4,5};
+ for (unsigned i=0;i<4;i++) {
+ uint32_t a=s_update_config[ram_fields[i]];
+ if (a>0x1ffffcu || (a&1u)) return 0;
+ }
+ if ((s_update_config[1]&3)||(s_update_config[2]&3)||
+ (s_update_config[3]&3)||s_update_config[6]>0xffffu||
+ !s_update_config[7]||s_update_config[7]>60000||
+ !s_update_config[8]||s_update_config[8]>s_update_config[7]||s_update_config[9]>31) return 0;
+ uint16_t *words=malloc(total*sizeof(*words)); if (!words) return 0;
+ uint32_t n=0;
+ for (uint32_t i=0;i0xffff) return 0;
+ s_update.compatible_contexts=1;s_update.protected_raw_mask=(uint16_t)v;
+ }
+ const char *predictor=getenv("PSX_INPUT_UPDATE_PREDICTOR");
+ if (predictor) {
+ if (strcmp(predictor,"previous-accept")) return 0;
+ s_update_previous_accept=1;
+ }
+ const char *guard=getenv("PSX_INPUT_UPDATE_REFRESH_GUARD");
+ if(guard) {
+ if(!s_update.compatible_contexts)return 0;
+ for(unsigned i=0;i<4;i++) {
+ char *end;unsigned long v=strtoul(guard,&end,0);
+ if(end==guard || (i==3?*end!=0:*end!=',') || v>0xfffffffful ||
+ (i==1 ? (v&3u)!=0 : (v>0x1ffffeu || (v&1u))))return 0;
+ s_refresh_guard[i]=(uint32_t)v;guard=end+(i<3);
+ }
+ s_refresh_guard_enabled=1;
+ }
+ const char *decisions=getenv("PSX_INPUT_UPDATE_DECISIONS");
+ if (decisions) {
+ FILE *f=fopen(decisions,"rb"); char line[128]; uint32_t previous=0;
+ if (!f) return 0;
+ if (!fgets(line,sizeof(line),f)) {fclose(f);return 0;}
+ int version2=!strcmp(line,"PSX-UPDATE-DECISIONS2\n");
+ if(!version2 && strcmp(line,"PSX-UPDATE-DECISIONS1\n")) {fclose(f);return 0;}
+ while (fgets(line,sizeof(line),f)) {
+ unsigned frame,open; char extra;
+ if (sscanf(line,"%u %u %c",&frame,&open,&extra)!=2 || frame<=previous ||
+ frame>s_update_config[7] || open>(version2?3u:1u) || s_update_decision_count==256) {fclose(f);return 0;}
+ previous=frame;
+ s_update_decision_frames[s_update_decision_count]=frame;
+ s_update_decision_open[s_update_decision_count++]=open;
+ }
+ if(ferror(f)){fclose(f);return 0;} if(fclose(f))return 0;
+ }
+ for(uint32_t i=0;i=2 && !s_refresh_guard_enabled)return 0;
+ if(mode==2 && (i+1==s_update_decision_count || s_update_decision_open[i+1]!=3 ||
+ s_update_decision_frames[i+1]!=s_update_decision_frames[i]+1))return 0;
+ if(mode==3 && (!i || s_update_decision_open[i-1]!=2 ||
+ s_update_decision_frames[i]!=s_update_decision_frames[i-1]+1))return 0;
+ }
+ s_update_enabled=1; psx_sha256_init(&s_update_hash); return 1;
+}
+uint16_t debug_server_update_poll(int slot,uint16_t buttons,int analog) {
+ if (!s_update_enabled || slot!=0) return buttons;
+ if (analog) update_fail("non-digital P1 poll");
+ uint32_t a=s_update_config[3];
+ uint32_t gate=update_peek16(a)|((uint32_t)update_peek16(a+2)<<16);
+ uint16_t word;
+ int open=input_update_predict(&s_update,s_frame_count,gate,s_update_previous_accept),override=0;
+ for(uint32_t i=0;igpr[s_update_config[9]] : UINT32_MAX;
+ if (selector!=s_update_config[10]) {
+ fprintf(s_update_log,"{\"kind\":\"auxiliary_store\",\"frame\":%llu,\"word\":%u,\"old_held\":%u,\"new_held\":%u,\"selector\":%u}\n",
+ (unsigned long long)s_frame_count,word,old_val,new_val,selector);
+ fflush(s_update_log);
+ if (selector!=s_update_config[11]) update_fail("unexpected auxiliary context/input");
+ if (!s_update.contexts) {
+ if (!input_update_auxiliary(&s_update,word,new_val)) update_fail("unexpected auxiliary context/input");
+ return;
+ }
+ }
+ if(s_update.neutral_refresh_stage==3) {
+ if(!s_refresh_guard_enabled || s_refresh_wait_pressed || selector!=s_update_config[11] ||
+ update_peek16(s_refresh_guard[2]) || update_peek16(s_refresh_guard[3]) ||
+ !input_update_neutral_refresh(&s_update,s_frame_count,word,old_val,new_val,update_peek16(s_refresh_guard[0])))
+ update_fail("declared neutral refresh mismatch");
+ s_refresh_wait_pressed=1;return;
+ }
+ if(s_refresh_wait_pressed)update_fail("unexpected acceptance before refresh pressed store");
+ uint8_t context=selector==s_update_config[10] ? 0u : 1u;
+ if (s_update.contexts && !s_update.compatible_contexts && s_update.contexts[s_update.accepted]!=context)
+ update_fail("source event context mismatch");
+ if (!(s_update.contexts ? input_update_context_accept(&s_update,s_frame_count,word,context) :
+ input_update_accept(&s_update,s_frame_count,word))) {
+ if (s_update.primed && !s_update.context_failed && !s_update.predicted && s_update.last_poll==s_frame_count && word==s_update.words[s_update.accepted]) {
+ fprintf(s_update_log,"{\"kind\":\"decision_failure\",\"decision_frame\":%llu,\"observed_open\":1,\"accepted\":%u}\n",
+ (unsigned long long)s_frame_count,s_update.accepted);fflush(s_update_log);
+ }
+ if(s_refresh_guard_enabled && s_update.context_failed && context==1 &&
+ s_update.accepted && s_update.last_accept+1==s_frame_count &&
+ s_update.words[s_update.accepted-1]==0xffff && !s_update.contexts[s_update.accepted] &&
+ ((uint16_t)~word&s_update.protected_raw_mask) && !old_val &&
+ !update_peek16(s_refresh_guard[0]) && !update_peek16(s_refresh_guard[2]) && !update_peek16(s_refresh_guard[3])) {
+ fprintf(s_update_log,"{\"kind\":\"neutral_refresh_candidate\",\"hold_frame\":%llu,\"refresh_frame\":%llu,\"accepted\":%u}\n",
+ (unsigned long long)s_update.last_accept,(unsigned long long)s_frame_count,s_update.accepted);fflush(s_update_log);
+ }
+ update_fail(s_update.context_failed ? "protected raw input effect mismatch" : "accepted packet/prediction mismatch");
+ }
+ uint8_t b[2]={(uint8_t)word,(uint8_t)(word>>8)};
+ psx_sha256_update(&s_update_hash,b,2); s_update_last_accept=s_frame_count;
+ fprintf(s_update_log,"{\"kind\":\"accept\",\"frame\":%llu,\"sequence\":%u,\"word\":%u}\n",
+ (unsigned long long)s_frame_count,s_update.accepted,word);
+ if (s_update.contexts)
+ fprintf(s_update_log,"{\"kind\":\"accepted_context\",\"frame\":%llu,\"sequence\":%u,\"context\":%u}\n",
+ (unsigned long long)s_frame_count,s_update.accepted,context);
+ if (fflush(s_update_log)) update_fail("log flush");
+ if (s_update.accepted==s_update.count) {
+ uint8_t hash[32]; char hex[65]; psx_sha256_final(&s_update_hash,hash);
+ for(unsigned i=0;i<32;i++) sprintf(hex+2*i,"%02x",hash[i]);
+ FILE *f=input_route_observer_output("update-input-end.json");
+ fprintf(f,"{\"clock\":\"accepted-update-experimental\",\"frame\":%u,\"accepted_samples\":%u,\"accepted_words_sha256\":\"%s\"}\n",
+ s_input_route_consumed,s_update.accepted,hex);
+ if(fclose(f)) update_fail("end receipt close");
+ input_route_observer_set_end(s_input_route_consumed);
+ }
+}
+static void update_boundary(void) {
+ if (!s_update_enabled || s_update.accepted==s_update.count) return;
+ if (s_frame_count>s_update_config[7] ||
+ s_frame_count-s_update_last_accept>s_update_config[8]) update_fail("accepted-update watchdog");
+}
diff --git a/runtime/include/interrupts.h b/runtime/include/interrupts.h
index 5be1f54e3..ee35d762b 100644
--- a/runtime/include/interrupts.h
+++ b/runtime/include/interrupts.h
@@ -49,6 +49,9 @@ void psx_check_interrupts(struct CPUState* cpu);
* raised by psx_advance_cycles(); this reports whether the comparatively
* expensive delivery/scheduler path can have an architectural effect now. */
int psx_interrupt_delivery_needed(const struct CPUState* cpu);
+/* Existing runtime compatibility delay, shared by precise take prediction.
+ * This reports policy state only; it does not advance or alter guest time. */
+int psx_interrupt_cooldown_active(void);
void psx_interrupt_delivery_diag(uint64_t *need_defer, uint64_t *need_irq,
uint64_t *skip_none, uint64_t *skip_sr,
uint64_t *skip_cooldown, uint64_t *skip_nested);
@@ -66,6 +69,10 @@ void psx_interrupt_check_path_diag(uint64_t *entry, uint64_t *fast_sr,
/* Interrupt check with the compiled guest PC to resume if a game-installed
* handler later RFEs to the sentinel outside the synchronous host window. */
void psx_check_interrupts_at(struct CPUState* cpu, uint32_t resume_pc);
+/* Source-profile precise interpreter: accept a pending IRQ at a branch's
+ * unexecuted delay slot, fetching that slot while saving the branch as EPC. */
+int psx_check_interrupts_delay_slot(struct CPUState* cpu,uint32_t slot_pc,
+ uint32_t target,int taken,uint32_t instruction);
int psx_interrupts_checked_at_current_cycle(uint32_t resume_pc);
/* Dispatch-entry check for re-enterable compiled PCs. De-dupes an immediately
* preceding check at the same guest PC/cycle so generated transfers can keep
@@ -80,6 +87,10 @@ void psx_check_interrupts_dispatch_entry(struct CPUState* cpu, uint32_t resume_p
void interrupts_advance_cycles(uint32_t cycles);
void interrupts_service_scheduled_events(void);
uint32_t interrupts_cycles_to_vblank(void);
+/* Optional source raster comparison: called after an already synchronized GP1
+ * write. Inactive profiles return immediately. No guest RAM is modified. */
+void interrupts_raster_gp1(uint32_t word);
+int interrupts_raster_gpu_status(uint32_t *bits);
/* While IRQ9 is enabled, expose the next 44.1-kHz sample as a first-class
* device deadline so the CPU can observe and acknowledge an IRQ before the
* following sample. UINT32_MAX means inactive. PSX_SPU_SAMPLE_EVENTS=0 is a
@@ -101,6 +112,10 @@ uint32_t cycles_to_next_event(void);
/* Query whether we are currently inside an exception handler dispatch. */
int psx_get_in_exception(void);
+/* The source CPU's pending-interrupt opcode table still executes COP2.
+ * Unknown instruction addresses and the default profile remain eligible. */
+int psx_irq_opcode_eligible(uint32_t pc);
+
/* Most recent block-leader IRQ-check guest PC / compiled resume latch.
* Used by the post-savestate freeze probe (vblank-time "where was the game"). */
uint32_t psx_last_irq_check_pc(void);
diff --git a/runtime/include/overlay_api.h b/runtime/include/overlay_api.h
index 2ceda1518..1633a9957 100644
--- a/runtime/include/overlay_api.h
+++ b/runtime/include/overlay_api.h
@@ -93,7 +93,9 @@
* host/DLL flavor mix — base-flavour DLLs and hosts are untouched, so
* the version stays. The emit-content change (PGXP_*() macros in all
* generated C) is covered by the codegen hash + CODEGEN_VER below. */
-#define PSX_OVERLAY_ABI_VERSION 21
+/* v22: instruction-boundary observation forwards to the host, including
+ * cached followers. DLL batches flush before observation; replay is excluded. */
+#define PSX_OVERLAY_ABI_VERSION 22
/* Process-lifetime overlay candidate capacity. Every accepted manifest F
* record consumes one slot, even when another DLL carries an identical
@@ -317,6 +319,10 @@ typedef struct {
* load-bearing. The ABI version bump that arms this ships with the
* emitter change (Phase 2 of ENHANCEMENTS.md G1 value propagation). */
const PGXPHooks *pgxp;
+ /* ABI v22. include_replay=1 queries installation for the fused-stub
+ * admission guard; zero queries whether a boundary must be observed now. */
+ int (*cpu_step_boundary_enabled)(int include_replay);
+ void (*cpu_step_boundary)(CPUState *cpu, uint32_t address);
} OverlayCallbacks;
#ifdef __cplusplus
diff --git a/runtime/include/overlay_dispatch_preamble.c.inc b/runtime/include/overlay_dispatch_preamble.c.inc
index ac0e9fc5f..b55c85aa8 100644
--- a/runtime/include/overlay_dispatch_preamble.c.inc
+++ b/runtime/include/overlay_dispatch_preamble.c.inc
@@ -4,6 +4,11 @@
static OverlayCallbacks g_cbs;
static uint32_t s_pending_cycles;
+/* Generated fused-stub admission reads this as a predicate. Query the host's
+ * live installation state, including replay, rather than a DLL-local copy. */
+#define g_psx_cpu_step_boundary_callback \
+ (g_cbs.cpu_step_boundary_enabled && g_cbs.cpu_step_boundary_enabled(1))
+
#ifdef _WIN32
__declspec(dllexport)
#else
@@ -117,6 +122,13 @@ void psx_icache_fetch(CPUState *cpu, uint32_t addr) {
overlay_flush_cycles();
if (g_cbs.icache_fetch) g_cbs.icache_fetch(cpu, addr);
}
+void psx_cpu_step_boundary(CPUState *cpu, uint32_t addr) {
+ if (g_cbs.cpu_step_boundary_enabled &&
+ g_cbs.cpu_step_boundary_enabled(0) && g_cbs.cpu_step_boundary) {
+ overlay_flush_cycles();
+ g_cbs.cpu_step_boundary(cpu, addr);
+ }
+}
void psx_muldiv_set(CPUState *cpu, uint32_t latency) {
overlay_flush_cycles();
if (g_cbs.muldiv_set) g_cbs.muldiv_set(cpu, latency);
diff --git a/runtime/include/psx_cyc.h b/runtime/include/psx_cyc.h
index 4239c1236..3e8729139 100644
--- a/runtime/include/psx_cyc.h
+++ b/runtime/include/psx_cyc.h
@@ -216,6 +216,10 @@ extern uint8_t *g_psx_ram;
extern int g_psx_load_delay;
extern int g_ls_mode;
extern volatile int g_ds_recording;
+/* Active source upload DMA requires the host helper's live bus-read charge. */
+extern uint32_t g_dma_cpu_read_wait;
+extern int g_ram_read_watch_active;
+void debug_server_trace_ram_read_watch(uint32_t phys, uint32_t val);
int psx_load_delay_enabled(void);
uint32_t psx_cyc_load_word_slow(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask);
uint16_t psx_cyc_load_half_slow(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask);
@@ -235,7 +239,7 @@ static inline uint32_t psx_cyc_load_word(CPUState* cpu, uint32_t addr,
uint32_t rt, uint32_t reg_mask) {
#ifdef PSX_ENABLE_BLOCK_CYCLES
uint32_t phys = addr & 0x1FFFFFFFu;
- if (g_ls_mode == 0 && !g_ds_recording && phys < 0x00800000u) {
+ if (g_ls_mode == 0 && !g_ds_recording && !g_dma_cpu_read_wait && phys < 0x00800000u) {
if (g_psx_load_delay < 0) (void)psx_load_delay_enabled();
if (g_psx_load_delay) {
psx_cyc_base(cpu);
@@ -251,6 +255,8 @@ static inline uint32_t psx_cyc_load_word(CPUState* cpu, uint32_t addr,
}
uint32_t value;
memcpy(&value, g_psx_ram + (phys & 0x1FFFFFu), sizeof(value));
+ if (g_ram_read_watch_active)
+ debug_server_trace_ram_read_watch(phys & 0x1FFFFFu, value);
return value;
}
return psx_cyc_load_word_slow(cpu, addr, rt, reg_mask);
@@ -265,7 +271,7 @@ static inline uint16_t psx_cyc_load_half(CPUState* cpu, uint32_t addr,
uint32_t rt, uint32_t reg_mask) {
#ifdef PSX_ENABLE_BLOCK_CYCLES
uint32_t phys = addr & 0x1FFFFFFFu;
- if (g_ls_mode == 0 && !g_ds_recording && phys < 0x00800000u) {
+ if (g_ls_mode == 0 && !g_ds_recording && !g_dma_cpu_read_wait && phys < 0x00800000u) {
if (g_psx_load_delay < 0) (void)psx_load_delay_enabled();
if (g_psx_load_delay) {
psx_cyc_base(cpu);
@@ -281,6 +287,8 @@ static inline uint16_t psx_cyc_load_half(CPUState* cpu, uint32_t addr,
}
uint16_t value;
memcpy(&value, g_psx_ram + (phys & 0x1FFFFFu), sizeof(value));
+ if (g_ram_read_watch_active)
+ debug_server_trace_ram_read_watch(phys & 0x1FFFFFu, value);
return value;
}
return psx_cyc_load_half_slow(cpu, addr, rt, reg_mask);
diff --git a/runtime/include/psx_icache.h b/runtime/include/psx_icache.h
index e1d1b722c..f26bd4364 100644
--- a/runtime/include/psx_icache.h
+++ b/runtime/include/psx_icache.h
@@ -2,6 +2,8 @@
#define PSXRECOMP_PSX_ICACHE_H
#include "cpu_state.h"
+#include "input_instruction_histogram.h"
+#include "psx_cycles.h"
#ifdef __cplusplus
extern "C" {
@@ -10,17 +12,40 @@ extern "C" {
extern uint32_t g_psx_icache_tv[1024];
extern int g_psx_icache_active;
extern int g_ls_replay_active;
+/* Optional functional instruction-boundary consumer, independent of tracing.
+ * Called before a guest fetch (also for coalesced cached followers) after
+ * publishing completed prior instructions. Replay shadows never call it. */
+extern void (*g_psx_cpu_step_boundary_callback)(CPUState *,uint32_t,uint64_t);
+#ifdef PSX_OVERLAY_DLL_BUILD
+/* The DLL owns its pending cycle batch; its shim flushes that batch before
+ * forwarding to the host's observer. Host globals must not be copied. */
+void psx_cpu_step_boundary(CPUState *cpu,uint32_t address);
+#else
+static inline void psx_cpu_step_boundary(CPUState *cpu,uint32_t address) {
+ if(g_psx_cpu_step_boundary_callback && !g_ls_replay_active) {
+ psx_cyc_batch_flush();
+ g_psx_cpu_step_boundary_callback(cpu,address,psx_get_cycle_count());
+ }
+}
+#endif
+void psx_cpu_step_boundary_fn(CPUState *cpu,uint32_t address);
+int psx_cpu_step_boundary_enabled(int include_replay);
void psx_icache_reset(void);
void psx_icache_fetch(CPUState *cpu, uint32_t addr);
void psx_icache_fetch_miss(CPUState *cpu, uint32_t addr);
+/* Called only for guest stores with CP0.SR.IsC set. Updates timing tags for
+ * the BIOS tag-test flush; cache data and guest BIU-disabled fetches are not
+ * modeled by this helper. Address is the original virtual store address. */
+void psx_icache_isolated_store(uint32_t addr, uint32_t cache_control);
/* Keep the interpreter's steady-state tag hit inside its translation unit.
* Misses use the shared slow path, preserving exact cache evolution/timing. */
-static inline void psx_icache_fetch_interp(CPUState *cpu, uint32_t addr) {
+static inline void psx_icache_fetch_interp_after_boundary(CPUState *cpu, uint32_t addr) {
+ if (g_input_instruction_histogram_active) input_instruction_histogram_sample(addr);
#ifdef PSX_ENABLE_BLOCK_CYCLES
if (g_ls_replay_active) return;
if (g_psx_icache_active < 0) {
- psx_icache_fetch(cpu, addr);
+ psx_icache_fetch_miss(cpu, addr);
return;
}
if (!g_psx_icache_active) return;
@@ -33,6 +58,11 @@ static inline void psx_icache_fetch_interp(CPUState *cpu, uint32_t addr) {
#endif
}
+static inline void psx_icache_fetch_interp(CPUState *cpu, uint32_t addr) {
+ psx_cpu_step_boundary(cpu,addr);
+ psx_icache_fetch_interp_after_boundary(cpu,addr);
+}
+
#ifdef __cplusplus
}
#endif
diff --git a/runtime/include/psx_memory.h b/runtime/include/psx_memory.h
index df150f66c..2880dcce6 100644
--- a/runtime/include/psx_memory.h
+++ b/runtime/include/psx_memory.h
@@ -24,6 +24,10 @@ extern "C" {
#define PSX_MAIN_RAM_MASK (PSX_MAIN_RAM_BYTES - 1u)
#define PSX_MAIN_RAM_WORD_MASK (PSX_MAIN_RAM_MASK & ~3u)
+/* Read instruction bytes from the native RAM/BIOS backing only. No MMIO,
+ * data-read observers, device service or cycle charge. Returns 0 when invalid. */
+int memory_peek_instruction_word(uint32_t address, uint32_t *value);
+
/* Strip KUSEG/KSEG0/KSEG1 and canonicalize within the 8 MiB DRAM decode
* window. Retail targets fold all four aliases; expanded targets preserve all
* 23 address bits. */
diff --git a/runtime/include/source_cpu_block_bound.h b/runtime/include/source_cpu_block_bound.h
new file mode 100644
index 000000000..9740c5482
--- /dev/null
+++ b/runtime/include/source_cpu_block_bound.h
@@ -0,0 +1,48 @@
+#ifndef PSX_SOURCE_CPU_BLOCK_BOUND_H
+#define PSX_SOURCE_CPU_BLOCK_BOUND_H
+/* A generated block's bcyc is its instruction count. It excludes cache
+ * refills, data waits and coprocessor interlocks, so it cannot alone prove
+ * that a pending IRQ lies beyond the block. This source-profile bound only
+ * selects the existing instruction interpreter; it never charges cycles.
+ * The caller restricts this to ordinary compiled RAM blocks without MMIO
+ * visibility side effects. Their bytes have already passed native-text checks. */
+static uint32_t source_cpu_block_bound(CPUState *cpu,uint32_t pc,uint32_t count,uint32_t deadline) {
+ uint64_t now=psx_get_cycle_count();
+ uint64_t stalls=(cpu->gte_ts_done>now?cpu->gte_ts_done-now:0u)+
+ (cpu->muldiv_ts_done>now?cpu->muldiv_ts_done-now:0u);
+ /* Per instruction: base1 + maximum cache refill7; data read maximum
+ * region36 (SPU word) + ReadFudge2 + completion2 + DMA steal200;
+ * newly issued GTE maximum43 and multiply/divide maximum37. Summing
+ * mutually exclusive costs is intentionally conservative. */
+ uint64_t bound=(uint64_t)count*(8u+240u+43u+37u)+stalls;
+ if(bound0x80000u || phys>=0x200000u || (uint64_t)phys+4ull*count>0x200000u)return UINT32_MAX;
+ bound=(uint64_t)count*8u+stalls;
+ for(uint32_t i=0;i>26,fn=word&63u;
+ if((op>=0x20u && op<=0x26u) || op==0x32u)bound+=240u;
+ if(op==0x12u && (word&(1u<<25)))bound+=psx_gte_cmd_latency(word);
+ if(!op && fn>=0x18u && fn<=0x1bu)bound+=37u;
+ }
+ return bound>UINT32_MAX?UINT32_MAX:(uint32_t)bound;
+}
+
+/* Existing generated MFC2/CFC2 bodies assign the GPR eagerly. Their block
+ * count includes branch delay slots, so inspect the validated live bytes and
+ * select the precise value owner before executing such a block. This also
+ * covers a move in JR's delay slot whose consumer is in the caller. Selection
+ * changes neither guest bytes nor cycles. CU2/profile admission is the caller's
+ * responsibility, just as for the timing bound above. */
+static int source_cpu_block_gte_value_delay(uint32_t pc,uint32_t count) {
+ uint32_t phys=pc&0x1fffffffu;
+ if(count>0x80000u || phys>=0x200000u || (uint64_t)phys+4ull*count>0x200000u)return 1;
+ for(uint32_t i=0;i>26,rs=(word>>21)&31u,rt=(word>>16)&31u;
+ if(op==0x12u && (rs==0u || rs==2u) && rt!=0u)return 1;
+ }
+ return 0;
+}
+#endif
diff --git a/runtime/include/source_cpu_boundary_probe.h b/runtime/include/source_cpu_boundary_probe.h
new file mode 100644
index 000000000..2a3b1353d
--- /dev/null
+++ b/runtime/include/source_cpu_boundary_probe.h
@@ -0,0 +1,59 @@
+#ifndef PSX_SOURCE_CPU_BOUNDARY_PROBE_H
+#define PSX_SOURCE_CPU_BOUNDARY_PROBE_H
+#include
+#include
+/* One passive snapshot per original-model frontend return. Unlike input-route
+ * fields, these boundaries include the CPU instruction's event overshoot. */
+static void source_cpu_return_probe(CPUState *cpu,uint32_t pc,uint64_t cycle,unsigned frame) {
+ static int initialized,enabled;
+ static FILE *stream;
+ if(!initialized) {
+ initialized=1;
+ const char *setting=getenv("PSX_SOURCE_CPU_RETURN_PROBE");
+ enabled=setting && setting[0]=='1' && !setting[1];
+ }
+ if(!enabled)return;
+ if(!stream) {
+ char path[4096];const char *directory=getenv("PSX_INPUT_ROUTE_CAPTURE_DIR");
+ if(!directory || snprintf(path,sizeof(path),"%s/cpu-return.tsv",directory)>=(int)sizeof(path))abort();
+ stream=fopen(path,"wx");if(!stream)abort();
+ fprintf(stream,"frame\tpc\tcycle\tsr\tcause\tepc");
+ for(unsigned i=0;i<32;i++)fprintf(stream,"\tr%u",i);
+ fputc('\n',stream);
+ }
+ if(frame>20000u)abort();
+ fprintf(stream,"%u\t%08X\t%llu\t%08X\t%08X\t%08X",frame,pc,(unsigned long long)cycle,cpu->cop0[12],cpu->cop0[13],cpu->cop0[14]);
+ for(unsigned i=0;i<32;i++)fprintf(stream,"\t%08X",cpu->gpr[i]);
+ fputc('\n',stream);fflush(stream);
+}
+/* Passive, bounded diagnostic at the existing functional pre-fetch boundary.
+ * It reads CPU/cache scalars only; never reads guest memory or services devices. */
+static void source_cpu_boundary_probe(CPUState *cpu,uint32_t pc,uint64_t cycle) {
+ static int initialized;
+ static unsigned long long low,high;
+ static unsigned rows;
+ static FILE *stream;
+ if(!initialized) {
+ initialized=1;
+ const char *range=getenv("PSX_SOURCE_CPU_BOUNDARY_WINDOW");
+ if(range && (sscanf(range,"%llu,%llu",&low,&high)!=2 || high<=low || high-low>1000000u))abort();
+ }
+ if(!high || cyclehigh) {if(stream){fclose(stream);stream=0;}high=0;return;}
+ if(!stream) {
+ char path[4096];const char *directory=getenv("PSX_INPUT_ROUTE_CAPTURE_DIR");
+ if(!directory || snprintf(path,sizeof(path),"%s/cpu-boundary.tsv",directory)>=(int)sizeof(path))abort();
+ stream=fopen(path,"wx");if(!stream)abort();
+ fprintf(stream,"pc\tcycle\tcache_tag\tcache_active\tsr\tcause\tepc");
+ for(unsigned i=0;i<32;i++)fprintf(stream,"\tr%u",i);
+ fprintf(stream,"\tread_fudge\tld_absorb\tld_which\tread_absorb_which\tdma_read_wait\tprecise\tdirty\ti_stat\ti_mask\tslice_pc\tslice_deadline\tslice_bound\tslice_cycle\tslice_takes\n");
+ }
+ if(++rows>500000u)abort();
+ fprintf(stream,"%08X\t%llu\t%08X\t%d\t%08X\t%08X\t%08X",pc,(unsigned long long)cycle,g_psx_icache_tv[(pc&0xffcu)>>2],g_psx_icache_active,cpu->cop0[12],cpu->cop0[13],cpu->cop0[14]);
+ for(unsigned i=0;i<32;i++)fprintf(stream,"\t%08X",cpu->gpr[i]);
+ extern int g_precise_mode,g_dirty_interp_active;
+ extern uint32_t i_stat,i_mask,g_slice_probe_pc,g_slice_probe_deadline,g_slice_probe_bound;
+ extern uint64_t g_slice_probe_cycle,g_slice_irq_taken;
+ fprintf(stream,"\t%u\t%u\t%u\t%u\t%u\t%d\t%d\t%08X\t%08X\t%08X\t%u\t%u\t%llu\t%llu\n",cpu->read_fudge,cpu->ld_absorb,cpu->ld_which_t,cpu->read_absorb_which,dma_cpu_read_penalty(),g_precise_mode,g_dirty_interp_active,i_stat,i_mask,g_slice_probe_pc,g_slice_probe_deadline,g_slice_probe_bound,(unsigned long long)g_slice_probe_cycle,(unsigned long long)g_slice_irq_taken);
+}
+#endif
diff --git a/runtime/include/source_gpu_command_projection.h b/runtime/include/source_gpu_command_projection.h
new file mode 100644
index 000000000..c7ea4ab21
--- /dev/null
+++ b/runtime/include/source_gpu_command_projection.h
@@ -0,0 +1,290 @@
+#ifndef PSX_SOURCE_GPU_COMMAND_PROJECTION_H
+#define PSX_SOURCE_GPU_COMMAND_PROJECTION_H
+#include
+#include
+#include "source_gpu_polygon_projection.h"
+
+/* Experimental, independently expressed Octoshock 2.2.2 command timing.
+ * This is NOT a renderer or a hardware timing claim. The synchronous renderer
+ * returns native texture-cache work, charged to this same queue before its
+ * next phase. Cache data and tags remain owned by the native renderer. Call update only at an
+ * explicitly qualified source GPU service event; reads do not advance it.
+ * Current scope: NOP/cache-clear, drawing environment, A0/C0 transfers,02 fills,80 copies, variable rectangles,
+ * observed untextured/textured polygons, including general opaque flat quads,
+ * with inclusive clipping. Vertex coordinates after offsets must stay in the
+ * signed 11-bit range; clipping stays inside VRAM.
+ * Interlaced row skipping requires explicitly supplied live readout parity.
+ * Other command families and state restoration remain unsupported. Ordinary GP1 reset
+ * preserves nonnegative credit and clamps debt, rather than granting new work.
+ * Original source: gpu.cpp ProcessFIFO/Update; gpu_polygon.cpp draw costs.
+ */
+enum { SOURCE_GPU_DISPATCH_NONE, SOURCE_GPU_DISPATCH_COMMAND,
+ SOURCE_GPU_DISPATCH_QUAD_FIRST, SOURCE_GPU_DISPATCH_QUAD_SECOND,
+ SOURCE_GPU_DISPATCH_UPLOAD_WORD };
+typedef struct SourceGPUCommandDispatch {
+ unsigned kind, count;
+ uint32_t words[12];
+} SourceGPUCommandDispatch;
+typedef struct SourceGPUCommandProjection {
+ int32_t budget;
+ uint32_t queue[32], count, phase, command;
+ uint64_t last_update;
+ int clip_x0, clip_y0, clip_x1, clip_y1;
+ int offset_x,offset_y;
+ uint32_t draw_mode,texture_window,mask_bits,display_mode,dma_direction;
+ unsigned field_valid,skip_field;
+ unsigned first_triangles, second_triangles;
+ uint32_t polygon_words[12];
+ uint32_t transfer_words;
+ SourceGPUCommandDispatch dispatch;
+ int error;
+} SourceGPUCommandProjection;
+
+enum { SOURCE_GPU_COMMAND_UNSUPPORTED=1, SOURCE_GPU_COMMAND_OVERFLOW=2,
+ SOURCE_GPU_COMMAND_REVERSE_TIME=3 };
+
+static inline void source_gpu_command_cold(SourceGPUCommandProjection *s) {
+ memset(s,0,sizeof(*s));
+}
+
+static inline unsigned source_gpu_polygon_stride(unsigned opcode) {
+ return 1u+!!(opcode&0x10)+!!(opcode&4);
+}
+static inline int source_gpu_polygon_supported(unsigned opcode) {
+ switch(opcode) {
+ case 0x20:case 0x21:case 0x28:case 0x29:case 0x30:case 0x38:case 0x3a:
+ case 0x24:case 0x2c:case 0x2d:case 0x2e:case 0x2f:
+ case 0x34:case 0x3c:case 0x3e:return 1;
+ default:return 0;
+ }
+}
+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;
+}
+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_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;
+ return command==0x80 || command==1 || command==0xa0 || command==0xc0 || command==0xe1 || command==0xe2 || command==0xe6 ? 2u : 1u;
+}
+
+static inline int source_gpu_command_ready(const SourceGPUCommandProjection *s) {
+ if(s->error) return -1;
+ if(s->phase==2) return 0;
+ if(s->count && (s->phase==4 || s->phase==8))return 0;
+ /* Feedback length differs from packet length. Cache clear and ordinary
+ * environment commands admit one queued word while DMA-ready stays set. */
+ return s->count==0 || s->countqueue[0]);
+}
+
+static inline uint32_t source_gpu_command_pop(SourceGPUCommandProjection *s) {
+ uint32_t word=s->queue[0];
+ --s->count;
+ memmove(s->queue,s->queue+1,s->count*sizeof(s->queue[0]));
+ return word;
+}
+
+static inline int source_gpu_command_coord(uint32_t word,unsigned shift) {
+ unsigned v=(word>>shift)&2047u;
+ return (int)(v^1024u)-1024;
+}
+
+static inline int source_gpu_command_polygon_cost(const SourceGPUCommandProjection *s,const uint32_t *words,int second) {
+ unsigned opcode=words[0]>>24,stride=source_gpu_polygon_stride(opcode);
+ int x[3],y[3];
+ if(s->clip_y0>511 || s->clip_y1>511 ||
+ ((s->display_mode&0x24)==0x24 && !(s->draw_mode&0x400) && !s->field_valid))return -1;
+ for(unsigned i=0;i<3;i++) {
+ unsigned v=i+(second?1:0);
+ x[i]=source_gpu_command_coord(words[1+stride*v],0)+s->offset_x;
+ y[i]=source_gpu_command_coord(words[1+stride*v],16)+s->offset_y;
+ }
+ return source_poly_cost(x,y,s->clip_x0,s->clip_y0,s->clip_x1,s->clip_y1,
+ !!(opcode&0x14),!!((opcode&2)||(s->mask_bits&2)),
+ (s->display_mode&0x24)==0x24 && !(s->draw_mode&0x400),s->skip_field);
+}
+
+/* Sprite coordinates wrap after adding the drawing offset. Polygon vertex
+ * admission and fill/copy addressing have different rules. */
+static inline int source_gpu_sprite_origin(uint32_t word,unsigned shift,int offset) {
+ return source_gpu_command_coord((uint32_t)(source_gpu_command_coord(word,shift)+offset),0);
+}
+static inline int source_gpu_command_block_cost(const SourceGPUCommandProjection *s,const uint32_t *words) {
+ unsigned opcode=words[0]>>24;
+ if(opcode==0x80) {
+ unsigned w=words[3]&1023u,h=(words[3]>>16)&511u;
+ return 2*(w?w:1024)*(h?h:512);
+ }
+ int interlace=(s->display_mode&0x24)==0x24 && !(s->draw_mode&0x400);
+ if(interlace && !s->field_valid)return -1;
+ int cost=opcode==2?46:16;
+ if(opcode==2) {
+ unsigned w=((words[2]&1023u)+15u)&~15u,h=(words[2]>>16)&511u;
+ for(unsigned y=0;y>16))&511u;
+ if(!interlace || (row&1u)!=s->skip_field)cost+=(w>>3)+9;
+ }
+ return cost;
+ }
+ int x=source_gpu_sprite_origin(words[1],0,s->offset_x),y=source_gpu_sprite_origin(words[1],16,s->offset_y);
+ unsigned size=words[(opcode&4)?3:2];
+ int right=x+(int)(size&1023u),bottom=y+(int)((size>>16)&511u);
+ if(xclip_x0)x=s->clip_x0;if(yclip_y0)y=s->clip_y0;
+ if(right>s->clip_x1+1)right=s->clip_x1+1;if(bottom>s->clip_y1+1)bottom=s->clip_y1+1;
+ int width=right-x;
+ if(width>0)for(int row=y;rowskip_field)continue;
+ cost+=width;
+ if((opcode&2) || (s->mask_bits&2))cost+=(((right+1)&~1)-(x&~1))/2;
+ }
+ return cost;
+}
+
+static inline int source_gpu_command_process(SourceGPUCommandProjection *s) {
+ s->dispatch.kind=SOURCE_GPU_DISPATCH_NONE;
+ if(s->error) return 0;
+ if(!s->count) return 1;
+ if(s->phase==8)return 1; /* Queued work waits for ordinary GPUREAD. */
+ if(s->phase==4) {
+ s->dispatch.kind=SOURCE_GPU_DISPATCH_UPLOAD_WORD;s->dispatch.count=1;
+ s->dispatch.words[0]=source_gpu_command_pop(s);
+ if(--s->transfer_words==0)s->phase=0;
+ return 1; /* Upload data bypasses draw debt and ordinary dispatch cost. */
+ }
+ if(s->phase==2) {
+ if(s->budget<0) return 1;
+ unsigned stride=source_gpu_polygon_stride(s->command);
+ unsigned first=source_gpu_command_length(s->polygon_words[0]),total=first+stride;
+ if(s->countpolygon_words[first+i]=s->queue[i];
+ int cost=source_gpu_command_polygon_cost(s,s->polygon_words,1);
+ if(cost<0){s->error=SOURCE_GPU_COMMAND_UNSUPPORTED;return 0;}
+ s->dispatch.kind=SOURCE_GPU_DISPATCH_QUAD_SECOND;s->dispatch.count=total;
+ memcpy(s->dispatch.words,s->polygon_words,total*sizeof(uint32_t));
+ for(unsigned i=0;ibudget-=46+source_gpu_polygon_setup(s->command)+cost;
+ s->phase=0;++s->second_triangles;return 1;
+ }
+
+ unsigned command=s->queue[0]>>24;
+ if(source_gpu_block_supported(command)) {
+ unsigned n=source_gpu_command_length(s->queue[0]);
+ if(s->budget<0 || s->countqueue);
+ if(cost<0){s->error=SOURCE_GPU_COMMAND_UNSUPPORTED;return 0;}
+ s->dispatch.kind=SOURCE_GPU_DISPATCH_COMMAND;s->dispatch.count=n;
+ for(unsigned i=0;idispatch.words[i]=source_gpu_command_pop(s);
+ s->budget-=2+cost;return 1;
+ }
+ if(source_gpu_polygon_supported(command)) {
+ unsigned n=source_gpu_command_length(s->queue[0]);
+ if(s->budget<0 || s->countqueue,0);
+ if(cost<0){s->error=SOURCE_GPU_COMMAND_UNSUPPORTED;return 0;}
+ s->dispatch.kind=(command&8)?SOURCE_GPU_DISPATCH_QUAD_FIRST:SOURCE_GPU_DISPATCH_COMMAND;
+ s->dispatch.count=n;
+ for(unsigned i=0;ipolygon_words[i]=s->dispatch.words[i]=source_gpu_command_pop(s);
+ s->budget-=84+source_gpu_polygon_setup(command)+cost;
+ if(command&4)s->draw_mode=(s->draw_mode&~0x1ffu)|((s->polygon_words[4+!!(command&0x10)]>>16)&0x1ffu);
+ if(command&8){s->phase=2;s->command=command;++s->first_triangles;}
+ return 1;
+ }
+ if(command!=0 && command!=1 && !(command>=0xe1 && command<=0xe6) && command!=0xa0 && command!=0xc0) {
+ s->error=SOURCE_GPU_COMMAND_UNSUPPORTED; return 0;
+ }
+ unsigned ordinary_environment=command==0xe1 || command==0xe2 || command==0xe6;
+ if(ordinary_environment && s->budget<0)return 1;
+ if(command==0xa0 || command==0xc0) {
+ if(s->budget<0 || s->count<3)return 1;
+ unsigned width=s->queue[2]&1023u;
+ unsigned height=(s->queue[2]>>16)&(command==0xa0?511u:1023u);
+ if(!width)width=1024;
+ if(command==0xa0 && !height)height=512;
+ if(command==0xc0 && height>512)height&=511u;
+ s->dispatch.kind=SOURCE_GPU_DISPATCH_COMMAND;s->dispatch.count=3;
+ for(unsigned i=0;i<3;i++)s->dispatch.words[i]=source_gpu_command_pop(s);
+ s->budget-=2;
+ s->transfer_words=(width*height+1u)/2u;
+ if(s->transfer_words)s->phase=command==0xa0?4u:8u;
+ } else if(command==1) {
+ if(s->budget<0)return 1;
+ s->dispatch.kind=SOURCE_GPU_DISPATCH_COMMAND;s->dispatch.count=1;
+ s->dispatch.words[0]=source_gpu_command_pop(s);s->budget-=2;
+ /* Cache invalidation is a synchronous native renderer side effect. */
+ } else {
+ uint32_t word=source_gpu_command_pop(s);
+ s->dispatch.kind=SOURCE_GPU_DISPATCH_COMMAND;s->dispatch.count=1;
+ s->dispatch.words[0]=word;
+ if(ordinary_environment)s->budget-=2;
+ /* Original NULLCMD and clip commands are ss_cmd: neither block on
+ * negative work credit nor charge the ordinary dispatch cost.
+ * Draw mode, texture window and mask commands use ordinary dispatch. */
+ if(command==0xe3) { s->clip_x0=word&1023; s->clip_y0=(word>>10)&1023; }
+ if(command==0xe4) { s->clip_x1=word&1023; s->clip_y1=(word>>10)&1023; }
+ if(command==0xe1) s->draw_mode=word&0x3fffu;
+ if(command==0xe2) s->texture_window=word&0xfffffu;
+ if(command==0xe5) {s->offset_x=source_gpu_command_coord(word,0);s->offset_y=source_gpu_command_coord(word,11);}
+ if(command==0xe6) s->mask_bits=word&3u;
+ }
+ return 1;
+}
+
+static inline int source_gpu_command_gp1(SourceGPUCommandProjection *s,uint32_t word) {
+ s->dispatch.kind=SOURCE_GPU_DISPATCH_NONE;
+ if(s->error)return 0;
+ unsigned command=word>>24;
+ if(command==0 || command==1) {
+ if(s->budget<0)s->budget=0;
+ s->count=0;s->phase=0;s->transfer_words=0;
+ if(!command) {
+ s->clip_x0=s->clip_y0=s->clip_x1=s->clip_y1=0;
+ s->offset_x=s->offset_y=0;s->draw_mode=s->texture_window=s->mask_bits=0;
+ s->display_mode=s->dma_direction=0;
+ }
+ } else if(command==4)s->dma_direction=word&3u;
+ else if(command==8) {
+ if(word&8u){s->error=SOURCE_GPU_COMMAND_UNSUPPORTED;return 0;}
+ s->display_mode=word&255u;
+ } else if(command!=2 && command!=3 && command!=5 && command!=6 && command!=7 && command!=0x10) {
+ s->error=SOURCE_GPU_COMMAND_UNSUPPORTED;return 0;
+ }
+ return 1;
+}
+
+static inline void source_gpu_command_read(SourceGPUCommandProjection *s) {
+ s->dispatch.kind=SOURCE_GPU_DISPATCH_NONE;
+ if(s->phase==8 && --s->transfer_words==0)s->phase=0;
+}
+
+static inline int source_gpu_command_write(SourceGPUCommandProjection *s,uint32_t word) {
+ s->dispatch.kind=SOURCE_GPU_DISPATCH_NONE;
+ if(s->error) return 0;
+ /* Idle FIFO extension follows the front command's feedback length;
+ * active split quads have no extension. Overflow is a scope stop. */
+ if(s->count>=16 && (s->phase || s->count-16>=source_gpu_command_feedback_length(s->queue[0]))) {
+ s->error=SOURCE_GPU_COMMAND_OVERFLOW; return 0;
+ }
+ s->queue[s->count++]=word;
+ return source_gpu_command_process(s);
+}
+
+static inline int source_gpu_command_update(SourceGPUCommandProjection *s,uint64_t cycle) {
+ s->dispatch.kind=SOURCE_GPU_DISPATCH_NONE;
+ if(s->error) return 0;
+ if(cyclelast_update) { s->error=SOURCE_GPU_COMMAND_REVERSE_TIME; return 0; }
+ uint64_t elapsed=cycle-s->last_update;
+ if(!elapsed) return 1;
+ int64_t missing=256-(int64_t)s->budget;
+ s->budget=elapsed>=(uint64_t)((missing+1)/2) ? 256 : s->budget+(int32_t)(2*elapsed);
+ s->last_update=cycle;
+ return source_gpu_command_process(s);
+}
+#endif
diff --git a/runtime/include/source_gpu_polygon_projection.h b/runtime/include/source_gpu_polygon_projection.h
new file mode 100644
index 000000000..5f0b1da07
--- /dev/null
+++ b/runtime/include/source_gpu_polygon_projection.h
@@ -0,0 +1,76 @@
+#ifndef PSX_SOURCE_GPU_POLYGON_PROJECTION_H
+#define PSX_SOURCE_GPU_POLYGON_PROJECTION_H
+#include
+
+/* Independent work estimator for untextured source-profile triangles.
+ * Pixel values are intentionally absent. Directed edge traversal matters for
+ * entering-clip work and fixed-point rounding, even when coverage looks equal. */
+static inline int64_t source_poly_edge(int x) {
+ return (int64_t)x*INT64_C(4294967296)+INT64_C(4294965248);
+}
+static inline int64_t source_poly_slope(int dx,int dy) {
+ return ((int64_t)dx*INT64_C(4294967296)+(dx<0?-(dy-1):dx>0?dy-1:0))/dy;
+}
+typedef void (*SourcePolySpan)(void *,int,int,int);
+static inline int source_poly_walk(const int *input_x,const int *input_y,
+ int clip_left,int clip_top,int clip_right,int clip_bottom,
+ int doubled,int masked_or_blended,int interlace,unsigned skip_field,
+ SourcePolySpan visit,void *context) {
+ struct {int x,y,original;} v[3];
+ int core= input_x[1]<=input_x[0] ? (input_x[2]<=input_x[1]?2:1) :
+ input_x[2] 1023 || v[i].y < -1024 || v[i].y > 1023)return -1;
+ }
+ const unsigned pairs[3][2]={{1,2},{0,1},{1,2}};
+ for(unsigned i=0;i<3;i++) {
+ unsigned a=pairs[i][0],b=pairs[i][1];
+ if(v[a].y>v[b].y) {
+ int x=v[a].x,y=v[a].y,o=v[a].original;
+ v[a]=v[b];v[b].x=x;v[b].y=y;v[b].original=o;
+ }
+ }
+ for(int i=0;i<3;i++)if(v[i].original==core){core=i;break;}
+ if(v[2].y==v[0].y || v[2].y-v[0].y>=512)return 0;
+ for(int i=0;i<3;i++)for(int j=i+1;j<3;j++)
+ if(v[i].x-v[j].x>=1024 || v[j].x-v[i].x>=1024)return 0;
+ int area=(v[1].x-v[0].x)*(v[2].y-v[0].y)-(v[2].x-v[0].x)*(v[1].y-v[0].y);
+ if(!area)return 0;
+ int64_t long_step=source_poly_slope(v[2].x-v[0].x,v[2].y-v[0].y);
+ int right= v[1].y==v[0].y ? v[1].x>v[0].x :
+ source_poly_slope(v[1].x-v[0].x,v[1].y-v[0].y)>long_step;
+ int cost=0;
+ for(int half=0;half<2;half++) {
+ int lo=half,hi=half+1;
+ if(v[hi].y==v[lo].y)continue;
+ int down=half==0?core==0:core!=2;
+ int anchor=down?lo:hi;
+ int64_t short_step=source_poly_slope(v[hi].x-v[lo].x,v[hi].y-v[lo].y);
+ int begin=down?v[lo].y:v[hi].y-1,end=down?v[hi].y:v[lo].y-1,dy=down?1:-1;
+ for(int y=begin;y!=end;y+=dy) {
+ if(down?y>clip_bottom:yclip_bottom){cost+=2;continue;}
+ if(interlace && ((unsigned)y&1u)==skip_field)continue;
+ int lx=(int)((source_poly_edge(v[0].x)+long_step*(y-v[0].y))>>32);
+ int sx=(int)((source_poly_edge(v[anchor].x)+short_step*(y-v[anchor].y))>>32);
+ int left=right?lx:sx,bound=right?sx:lx;
+ int width=bound-left;
+ /* Source signs the left endpoint before applying its clip. */
+ left=((left&2047)^1024)-1024;
+ if(leftclip_right+1)width=clip_right+1-left;
+ if(width>0) {
+ cost+=doubled?2*width:masked_or_blended?width+(width+1)/2:width;
+ if(visit)visit(context,y,left,width);
+ }
+ }
+ }
+ return cost;
+}
+static inline int source_poly_cost(const int *x,const int *y,int l,int t,int r,int b,
+ int doubled,int masked,int interlace,unsigned skip) {
+ return source_poly_walk(x,y,l,t,r,b,doubled,masked,interlace,skip,0,0);
+}
+#endif
diff --git a/runtime/include/source_gpu_runtime.h b/runtime/include/source_gpu_runtime.h
new file mode 100644
index 000000000..90027119c
--- /dev/null
+++ b/runtime/include/source_gpu_runtime.h
@@ -0,0 +1,28 @@
+#ifndef PSX_SOURCE_GPU_RUNTIME_H
+#define PSX_SOURCE_GPU_RUNTIME_H
+#include "source_gpu_service_clock.h"
+#include "source_gpu_command_projection.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+/* Experimental source-profile service bridge. Initialization is cold-only;
+ * default code remains inactive. Reads copy scalar state without servicing it.
+ * The GPU installs a sink so phase dispatch owns renderer/environment effects. */
+typedef int (*SourceGPUDispatchSink)(const SourceGPUCommandDispatch *);
+void source_gpu_runtime_set_dispatch_sink(SourceGPUDispatchSink);
+void source_gpu_runtime_init(void);
+int source_gpu_runtime_active(void);
+int source_gpu_runtime_ready(void);
+uint32_t source_gpu_runtime_status_bits(void);
+void source_gpu_runtime_advance(void);
+uint32_t source_gpu_runtime_cycles_to_event(void);
+void source_gpu_runtime_dma_write(void);
+void source_gpu_runtime_gp0(uint32_t word);
+void source_gpu_runtime_read(void);
+void source_gpu_runtime_gp1(uint32_t word);
+void source_gpu_runtime_copy(SourceGPUServiceClock *,SourceGPUCommandProjection *);
+void source_gpu_runtime_copy_return(SourceGPUServiceClock *,SourceGPUCommandProjection *);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/runtime/include/source_gpu_service_clock.h b/runtime/include/source_gpu_service_clock.h
new file mode 100644
index 000000000..7217301a3
--- /dev/null
+++ b/runtime/include/source_gpu_service_clock.h
@@ -0,0 +1,95 @@
+#ifndef PSX_SOURCE_GPU_SERVICE_CLOCK_H
+#define PSX_SOURCE_GPU_SERVICE_CLOCK_H
+#include "input_route_raster_clock.h"
+
+/* Standalone source event clock, not yet connected to runtime scheduling.
+ * Original DMA_Update services the GPU on its fixed 128-cycle cadence and
+ * before DMA register writes. GPU's own event reschedules from the earlier
+ * of 128 clocks and the next raster phase boundary. A DMA caller does not
+ * replace that own-event deadline. Only NTSC cold state is represented here.
+ * Placement of frame-end ForceEventUpdates, mode writes and reset/restore
+ * remain separate integration gates. Callback duplicates are intentional:
+ * the command projection must make zero-elapsed Update a no-op.
+ */
+typedef struct SourceGPUServiceClock {
+ InputRouteRasterClock raster;
+ uint64_t cycle, gpu_deadline, dma_deadline;
+ uint64_t frame_request_cycle;
+ unsigned zero_reached,frame_pending,frame_returns;
+} SourceGPUServiceClock;
+typedef void (*SourceGPUServiceEvent)(void *,uint64_t,unsigned);
+enum { SOURCE_GPU_EVENT_OWN=1, SOURCE_GPU_EVENT_DMA=2, SOURCE_GPU_EVENT_WRITE=3,
+ SOURCE_GPU_EVENT_FRAME_END=4 };
+static inline void source_gpu_service_cold(SourceGPUServiceClock *s) {
+ memset(s,0,sizeof(*s)); input_route_raster_reset(&s->raster);
+ s->gpu_deadline=s->dma_deadline=128;
+}
+static inline uint64_t source_gpu_service_next(const SourceGPUServiceClock *s) {
+ return s->gpu_deadlinedma_deadline ? s->gpu_deadline : s->dma_deadline;
+}
+static inline uint32_t source_gpu_service_until_phase(const SourceGPUServiceClock *s) {
+ uint64_t clocks=(uint64_t)s->raster.remaining*65536u-s->raster.fraction;
+ return (uint32_t)((clocks+103895u)/103896u);
+}
+/* Original frontend requests its return only after passing scanline zero.
+ * Visible/final-line fallbacks and the >=232 VBlank rule are distinct from
+ * the hardware IRQ edge. The CPU boundary consumes the request later. */
+static inline void source_gpu_service_raster(SourceGPUServiceClock *s,uint32_t elapsed) {
+ uint32_t old_line=s->raster.scanline,old_blank=s->raster.blank;
+ input_route_raster_advance(&s->raster,elapsed);
+ if(s->raster.scanline==old_line) return;
+ uint32_t line=s->raster.scanline;
+ if(!line)s->zero_reached=1;
+ if(s->zero_reached && (line==256 || line==s->raster.lines-1 ||
+ (!old_blank && s->raster.blank && line>=232))) {
+ if(!s->frame_pending)s->frame_request_cycle=s->raster.cycle;
+ s->frame_pending=1;
+ }
+}
+static inline int source_gpu_service_to(SourceGPUServiceClock *s,uint64_t now,
+ SourceGPUServiceEvent event,void *context) {
+ if(nowcycle) return 0;
+ uint64_t next;
+ while((next=source_gpu_service_next(s))<=now) {
+ uint64_t elapsed=next-s->cycle;
+ if(elapsed>UINT32_MAX) return 0;
+ source_gpu_service_raster(s,(uint32_t)elapsed); s->cycle=next;
+ if(next==s->gpu_deadline) {
+ event(context,next,SOURCE_GPU_EVENT_OWN);
+ uint32_t distance=source_gpu_service_until_phase(s);
+ s->gpu_deadline=next+(distance<128 ? distance : 128);
+ }
+ if(next==s->dma_deadline) {
+ event(context,next,SOURCE_GPU_EVENT_DMA); s->dma_deadline+=128;
+ }
+ }
+ uint64_t elapsed=now-s->cycle;
+ if(elapsed>UINT32_MAX) return 0;
+ source_gpu_service_raster(s,(uint32_t)elapsed); s->cycle=now;
+ return 1;
+}
+static inline int source_gpu_service_dma_write(SourceGPUServiceClock *s,uint64_t now,
+ SourceGPUServiceEvent event,void *context) {
+ if(!source_gpu_service_to(s,now,event,context)) return 0;
+ event(context,now,SOURCE_GPU_EVENT_WRITE); return 1;
+}
+/* Original frame-end ForceEventUpdates runs GPU then DMA at the returned CPU
+ * timestamp, and replaces the GPU event deadline even for a zero-time update.
+ * This absolute-clock representation needs no timestamp rebase. The caller
+ * must supply an independently qualified source-equivalent return boundary. */
+static inline int source_gpu_service_frame_end(SourceGPUServiceClock *s,uint64_t now,
+ SourceGPUServiceEvent event,void *context) {
+ if(!source_gpu_service_to(s,now,event,context)) return 0;
+ event(context,now,SOURCE_GPU_EVENT_FRAME_END);
+ uint32_t distance=source_gpu_service_until_phase(s);
+ s->gpu_deadline=now+(distance<128 ? distance : 128);
+ event(context,now,SOURCE_GPU_EVENT_DMA);
+ s->frame_pending=0;s->zero_reached=0;s->frame_returns++;
+ return 1;
+}
+static inline int source_gpu_service_cpu_boundary(SourceGPUServiceClock *s,uint64_t now,
+ SourceGPUServiceEvent event,void *context) {
+ if(!source_gpu_service_to(s,now,event,context)) return 0;
+ return !s->frame_pending || source_gpu_service_frame_end(s,now,event,context);
+}
+#endif
diff --git a/runtime/include/source_gpu_texture.h b/runtime/include/source_gpu_texture.h
new file mode 100644
index 000000000..9191ef3d9
--- /dev/null
+++ b/runtime/include/source_gpu_texture.h
@@ -0,0 +1,17 @@
+#ifndef PSX_SOURCE_GPU_TEXTURE_H
+#define PSX_SOURCE_GPU_TEXTURE_H
+#include
+/* Parameters for the opt-in, native software source-comparison renderer.
+ * These are command attributes, never imported source-emulator state. */
+typedef struct SourceGPUTexture {
+ uint32_t uv[3],window;
+ uint16_t page,clut;
+ int raw,load_clut;
+} SourceGPUTexture;
+typedef struct SourceGPUBlock {
+ const uint32_t *words;
+ uint32_t draw_mode,texture_window;
+ int x,y,interlace,clip_left,clip_top,clip_right,clip_bottom;
+ unsigned skip_field;
+} SourceGPUBlock;
+#endif
diff --git a/runtime/include/source_ram_page_probe.h b/runtime/include/source_ram_page_probe.h
new file mode 100644
index 000000000..7ac6569da
--- /dev/null
+++ b/runtime/include/source_ram_page_probe.h
@@ -0,0 +1,66 @@
+#ifndef PSX_SOURCE_RAM_PAGE_PROBE_H
+#define PSX_SOURCE_RAM_PAGE_PROBE_H
+#include
+#include
+#include
+#include
+
+/* Passive diagnostic index. A hash mismatch locates bytes to inspect; hashes
+ * are not a substitute for comparing the selected raw RAM snapshots. */
+static uint64_t source_ram_page_hash(const uint8_t *bytes, unsigned count) {
+ uint64_t hash = UINT64_C(14695981039346656037);
+ for (unsigned i = 0; i < count; ++i)
+ hash = (hash ^ bytes[i]) * UINT64_C(1099511628211);
+ return hash;
+}
+
+static void source_ram_page_probe(unsigned frame, uint64_t cycle) {
+ static int initialized, enabled;
+ static FILE *stream;
+ static unsigned snapshots[32], snapshot_count;
+ if (!initialized) {
+ initialized = 1;
+ const char *setting = getenv("PSX_SOURCE_RAM_PAGE_PROBE");
+ enabled = setting && strcmp(setting, "1") == 0;
+ const char *cursor = getenv("PSX_SOURCE_RAM_SNAPSHOT_FRAMES");
+ while (enabled && cursor && *cursor) {
+ if (*cursor < '0' || *cursor > '9') abort();
+ char *end;
+ unsigned long value = strtoul(cursor, &end, 10);
+ if (end == cursor || value < 1 || value > 20000 || snapshot_count == 32 ||
+ (*end && *end != ',')) abort();
+ for (unsigned i = 0; i < snapshot_count; ++i)
+ if (snapshots[i] == value) abort();
+ snapshots[snapshot_count++] = (unsigned)value;
+ cursor = *end ? end + 1 : end;
+ if (*end && !*cursor) abort();
+ }
+ }
+ if (!enabled) return;
+ if (frame < 1 || frame > 20000) abort();
+ extern uint8_t *memory_get_ram_ptr(void);
+ const uint8_t *ram = memory_get_ram_ptr();
+ char path[4096];
+ const char *directory = getenv("PSX_INPUT_ROUTE_CAPTURE_DIR");
+ if (!ram || !directory) abort();
+ if (!stream) {
+ if (snprintf(path, sizeof(path), "%s/ram-pages.tsv", directory) >= (int)sizeof(path)) abort();
+ stream = fopen(path, "wx");
+ if (!stream) abort();
+ fputs("# psx-ram-pages-v1 page_bytes=4096 ram_bytes=2097152 hash=fnv1a64\nframe\tcycle", stream);
+ for (unsigned page = 0; page < 512; ++page) fprintf(stream, "\t%06X", page * 4096);
+ fputc('\n', stream);
+ }
+ fprintf(stream, "%u\t%llu", frame, (unsigned long long)cycle);
+ for (unsigned page = 0; page < 512; ++page)
+ fprintf(stream, "\t%016llX", (unsigned long long)source_ram_page_hash(ram + page * 4096, 4096));
+ fputc('\n', stream);
+ if (fflush(stream) != 0 || ferror(stream)) abort();
+ for (unsigned i = 0; i < snapshot_count; ++i) if (snapshots[i] == frame) {
+ if (snprintf(path, sizeof(path), "%s/ram-frame-%06u.bin", directory, frame) >= (int)sizeof(path)) abort();
+ FILE *snapshot = fopen(path, "wbx");
+ if (!snapshot) abort();
+ if (fwrite(ram, 1, 2097152, snapshot) != 2097152 || fclose(snapshot) != 0) abort();
+ }
+}
+#endif
diff --git a/runtime/include/timer1_source_clock.h b/runtime/include/timer1_source_clock.h
new file mode 100644
index 000000000..14dabf1b9
--- /dev/null
+++ b/runtime/include/timer1_source_clock.h
@@ -0,0 +1,73 @@
+#ifndef PSX_TIMER1_SOURCE_CLOCK_H
+#define PSX_TIMER1_SOURCE_CLOCK_H
+#include
+#include
+/* Optional original-Octoshock timer 1 state machine. Independently expressed
+ * from documented transitions; only IRQ-disabled modes are admitted. The
+ * caller supplies ordered CPU, H-retrace and VBlank events. No guest memory. */
+typedef struct PsxTimer1Source {
+ uint32_t mode, counter, target;
+ int counting, blank;
+} PsxTimer1Source;
+static inline void timer1_source_reset(PsxTimer1Source *s) {
+ memset(s,0,sizeof(*s)); s->blank=1;
+}
+static inline void timer1_source_match(PsxTimer1Source *s) {
+ s->mode|=0x800;
+ if(s->mode&8) s->counter%=s->target ? s->target : 1;
+}
+static inline void timer1_source_count(PsxTimer1Source *s,uint32_t n) {
+ if((s->mode&8) && !s->target && !s->counter) {
+ timer1_source_match(s); return;
+ }
+ if(s->counting<=0 || !n) return;
+ uint32_t before=s->counter;
+ s->counter+=n;
+ if((beforetarget && s->counter>=s->target) || s->counter>=s->target+65536u)
+ timer1_source_match(s);
+ if(s->counter>=65536u) {s->mode|=0x1000; s->counter&=65535u;}
+}
+static inline void timer1_source_cpu(PsxTimer1Source *s,uint32_t n) {
+ if(!(s->mode&0x100)) timer1_source_count(s,n);
+}
+static inline void timer1_source_hblank(PsxTimer1Source *s,uint32_t n) {
+ if(s->mode&0x100) timer1_source_count(s,n);
+}
+static inline void timer1_source_blank(PsxTimer1Source *s,int blank) {
+ unsigned sync=s->mode&7;
+ if(sync==1) s->counting=!blank;
+ if(sync==5) s->counting=blank;
+ if((sync==3 || sync==5) && s->blank && !blank) {
+ s->counter=0;
+ if(!s->target) timer1_source_match(s);
+ }
+ if(sync==7) {
+ if(s->counting<0 && !s->blank && blank) s->counting=0;
+ else if(!s->counting && s->blank && !blank) s->counting=1;
+ }
+ s->blank=blank;
+}
+static inline int timer1_source_write(PsxTimer1Source *s,unsigned reg,uint16_t v) {
+ if(reg==4 && (v&0x30)) return 0; /* no IRQ phase claim */
+ if(reg==0) s->counter=v;
+ if(reg==4) {
+ s->mode=(s->mode&0x1c00)|(v&0x3ff);s->counter=0;s->counting=1;
+ if((v&7)==1) s->counting=!s->blank;
+ if((v&7)==5) s->counting=s->blank;
+ if((v&7)==7) s->counting=-1;
+ }
+ if(reg==8) s->target=v;
+ if(s->counter==s->target) timer1_source_match(s);
+ return 1;
+}
+static inline uint32_t timer1_source_read(PsxTimer1Source *s,unsigned reg) {
+ if(reg==0) return s->counter;
+ if(reg==8) return s->target;
+ if(reg==4) {
+ uint32_t value=s->mode;s->mode&=~0x1000u;
+ if(s->counter!=s->target) s->mode&=~0x800u;
+ return value;
+ }
+ return 0;
+}
+#endif
diff --git a/runtime/include/timer2_source_clock.h b/runtime/include/timer2_source_clock.h
new file mode 100644
index 000000000..f6b4b23e1
--- /dev/null
+++ b/runtime/include/timer2_source_clock.h
@@ -0,0 +1,78 @@
+#ifndef PSX_TIMER2_SOURCE_CLOCK_H
+#define PSX_TIMER2_SOURCE_CLOCK_H
+#include
+#include
+/* Explicit original-Octoshock compatibility state. Independent expression of
+ * the retained source transitions, qualified against an external scalar oracle.
+ * This is not a claim that every original-core quirk describes PS1 hardware.
+ * No guest memory, title addresses, input words or selected timing constants. */
+typedef struct PsxTimer2Source {
+ uint32_t counter,mode,target,divider;
+ int irq_done,counting;
+} PsxTimer2Source;
+static inline void timer2_source_reset(PsxTimer2Source *s){memset(s,0,sizeof(*s));}
+static inline int timer2_source_target(PsxTimer2Source *s,unsigned *pulses){
+ s->mode|=0x800;
+ if(s->mode&8) s->counter%=s->target?s->target:1;
+ if(!(s->mode&0x10) || s->irq_done) return 0;
+ s->irq_done=1;(*pulses)++;
+ return !s->counter || s->counter==s->target;
+}
+static inline unsigned timer2_source_cpu(PsxTimer2Source *s,uint32_t cycles){
+ unsigned pulses=0;
+ /* Original TIMER_Update skips this encoding even for timer 2. */
+ if(s->mode&0x100) return 0;
+ if(s->counting<=0) cycles=0;
+ uint32_t divided=(s->divider+cycles)>>3;
+ s->divider=(s->divider+cycles)&7;
+ if(s->mode&0x200) cycles=divided;
+ if(s->mode&1) cycles=0;
+ if((s->mode&8) && !s->target && !s->counter){
+ (void)timer2_source_target(s,&pulses);return pulses;
+ }
+ if(!cycles) return 0;
+ uint32_t before=s->counter;
+ s->counter+=cycles;
+ if(s->mode&0x40) s->irq_done=0;
+ int exact=0;
+ if((beforetarget && s->counter>=s->target) || s->counter>=s->target+65536u)
+ exact=timer2_source_target(s,&pulses);
+ if(s->counter>=65536u){
+ s->mode|=0x1000;s->counter&=65535u;
+ if((s->mode&0x20) && !s->irq_done){
+ exact|=!s->counter;s->irq_done=1;pulses++;
+ }
+ }
+ if((s->mode&0x40) && !exact) s->irq_done=0;
+ return pulses;
+}
+static inline unsigned timer2_source_write(PsxTimer2Source *s,unsigned reg,uint16_t v){
+ unsigned pulses=0;
+ if(reg==0){s->counter=v;s->irq_done=0;}
+ if(reg==4){
+ s->mode=(s->mode&0x1c00)|(v&0x3ff);s->counter=0;s->irq_done=0;
+ s->counting=1; /* Divider deliberately survives mode writes. */
+ }
+ if(reg==8) s->target=v;
+ if(s->counter==s->target)(void)timer2_source_target(s,&pulses);
+ return pulses;
+}
+static inline uint32_t timer2_source_read(PsxTimer2Source *s,unsigned reg){
+ if(reg==0)return s->counter;
+ if(reg==8)return s->target;
+ if(reg!=4)return 0;
+ uint32_t value=s->mode;s->mode&=~0x1000u;
+ if(s->counter!=s->target)s->mode&=~0x800u;
+ return value;
+}
+static inline uint32_t timer2_source_next(const PsxTimer2Source *s){
+ /* Source periodic update bound, independent of CPU interrupt masking. */
+ if(!(s->mode&0x30))return 1024;
+ if((s->mode&8) && !s->counter && !s->target && !s->irq_done)return 1;
+ if((s->mode&1) || s->counting<=0)return 1024;
+ uint32_t target=((s->mode&0x18) && s->countertarget)?s->target:65536u;
+ uint32_t clocks=target-s->counter;
+ if(s->mode&0x200)clocks=clocks*8-s->divider;
+ return clocks<1024?clocks:1024;
+}
+#endif
diff --git a/runtime/include/timers.h b/runtime/include/timers.h
index 3f67ff7c4..281536fe9 100644
--- a/runtime/include/timers.h
+++ b/runtime/include/timers.h
@@ -10,6 +10,12 @@ extern "C" {
#define TIMER_BASE 0x1F801100
void timers_init(void);
+int timers_source_raster_enabled(void);
+/* HBlank-driven counter reads see GPU events before the final bus wait.
+ * CPU-clock counter modes still sample after that wait. Physical address. */
+int timers_source_hblank_counter_read(uint32_t addr);
+void timers_source_raster_event(void *context, uint64_t cycle, unsigned event, int blank);
+void timers_source_raster_finish(uint64_t cycle);
void timers_get_snapshot(uint16_t counter[3], uint32_t mode[3],
uint16_t target[3], int32_t irq_line[3],
uint32_t frac[3]);
diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake
index 2526ded97..d1f7dbdb1 100644
--- a/runtime/runtime.cmake
+++ b/runtime/runtime.cmake
@@ -244,6 +244,7 @@ if(PSX_RECOMP_UI AND (NOT RECOMP_UI_ROOT OR RECOMP_UI_ROOT STREQUAL ""))
endif()
set(PSXRECOMP_RUNTIME_SOURCES
+ ${PSXRECOMP_ROOT}/runtime/src/input_route_observer.c
${PSXRECOMP_ROOT}/runtime/src/main.cpp
${PSXRECOMP_ROOT}/runtime/src/psx_window_icon.cpp
${PSXRECOMP_ROOT}/runtime/src/psx_sdl_audio.cpp
@@ -251,6 +252,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/memory.c
${PSXRECOMP_ROOT}/runtime/src/guest_tty.c
${PSXRECOMP_ROOT}/runtime/src/gpu.c
+ ${PSXRECOMP_ROOT}/runtime/src/source_gpu_runtime.c
${PSXRECOMP_ROOT}/runtime/src/ws_ui_group.c
${PSXRECOMP_ROOT}/runtime/src/ws_aspect_cone_math.c
${PSXRECOMP_ROOT}/runtime/src/gpu_sw_renderer.c
diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c
index 7d100e8da..7f6a0e828 100644
--- a/runtime/src/cdrom.c
+++ b/runtime/src/cdrom.c
@@ -651,6 +651,16 @@ static int sector_delay_cycles(void) {
}
static int initial_read_delay_cycles(void) {
+ /* Explicit source-core comparison profile, not a hardware timing claim.
+ * Octoshock 2.2.2 fills two pipeline slots before presenting the oldest
+ * sector on its third fetch (cdc.h SectorPipe_Count / HandlePlayRead).
+ * This reproduces that start deadline only; seek jitter, command latency,
+ * and rotating drive state are separate, still-unqualified differences. */
+ const char *profile = getenv("PSX_CD_READ_START_MODEL");
+ if (profile && strcmp(profile, "octoshock-2.2.2-pipeline") == 0)
+ return apply_read_speed(((mode_reg & 0x80)
+ ? CDROM_SINGLE_SPEED_SECTOR_CYCLES / 2
+ : CDROM_SINGLE_SPEED_SECTOR_CYCLES) * 3);
/* Beetle/PCSX model an additional read-start latency after ReadN/ReadS.
* In double-speed mode the first sector still waits one 1x sector period;
* subsequent sectors use the steady-state 2x cadence above. */
@@ -749,8 +759,11 @@ static CDROMTraceEntry cdrom_trace[CDROM_TRACE_CAP];
static uint64_t cdrom_trace_seq;
static void trace_cdrom(uint8_t kind, uint32_t addr, uint32_t val, uint8_t width) {
+ extern uint32_t debug_guest_ra(void);
CDROMTraceEntry *e = &cdrom_trace[cdrom_trace_seq % CDROM_TRACE_CAP];
e->seq = cdrom_trace_seq++;
+ e->cycle = psx_cycle_count;
+ e->guest_ra = debug_guest_ra();
e->kind = kind;
e->addr = addr;
e->val = val;
@@ -779,6 +792,15 @@ static void trace_cdrom(uint8_t kind, uint32_t addr, uint32_t val, uint8_t width
e->read_delay = read_delay;
}
+#include "cdrom_random_tape.h"
+static CdRandomTape s_source_clock_tape;
+static int s_source_clock;
+/* Independent drive operation, not a command second response. */
+static uint64_t s_source_reset_due;
+static uint64_t s_source_command_due, s_source_ready_due;
+static uint32_t s_source_clock_calls;
+static int s_source_command_phase, s_source_args_remaining;
+
static void record_command_history(uint8_t kind, uint8_t cmd,
const uint8_t* params, int count) {
CDROMCommandHistoryEntry *e =
@@ -815,6 +837,9 @@ static void record_command_history(uint8_t kind, uint8_t cmd,
e->pending_pending = (uint8_t)(pending.pending ? 1 : 0);
e->queued_cmd = queued_cmd.cmd;
e->queued_pending = (uint8_t)(queued_cmd.pending ? 1 : 0);
+ e->source_clock=(uint8_t)s_source_clock;
+ e->source_random_cursor=s_source_clock_tape.cursor;
+ e->source_random_calls=s_source_clock_calls;
}
static int xa_is_audio_realtime(const CDROMSectorDelivery *d) {
@@ -879,6 +904,41 @@ static int has_disc(void) {
#define CDSTAT_SEEKERR 0x04
#define CDSTAT_IDERROR 0x08
#define CDSTAT_SHELL 0x10
+
+/* Explicit cold-start source compatibility only. Hardware identity is not
+ * inferred from the main BIOS. Defaults retain the existing PU-7 response.
+ * This does not reproduce the source drive's seek/rotation or swap lifecycle. */
+static int s_source_firmware_model;
+static int s_source_cold_status_model;
+static int s_source_toc_seek_model;
+static int s_source_explicit_seek_model;
+/* Source timing profile only: PAUSED and STANDBY have the same public status
+ * bits but different restart costs. Kept separate from visible READ/SEEK. */
+static uint8_t s_source_seek_paused;
+/* Native MSF names the next delivered sector. The source drive has two
+ * additional sectors in its pipeline. Remember the stream origin so Pause
+ * can rewind up to four physical reads even before that pipeline is full. */
+static int s_source_read_start_lba;
+static uint32_t source_clock_random(uint32_t maximum) {
+ uint32_t value;
+ if (!cd_tape_bounded(&s_source_clock_tape,maximum,&value)) {
+ fprintf(stderr,"[CDROM] Source clock random tape exhausted at word %u\n",s_source_clock_tape.cursor);
+ exit(2);
+ }
+ s_source_clock_calls++;
+ return value;
+}
+static int source_clock_receive_ready(void) {
+ return irq_flag==0 && (!s_source_clock || psx_cycle_count>=s_source_ready_due);
+}
+static int source_boot_model(const char *name) {
+ const char *value=getenv(name);
+ if(!value || !*value || strcmp(value,"default")==0) return 0;
+ if(strcmp(value,"octoshock-2.2.2")==0) return 1;
+ fprintf(stderr,"[CDROM] Unsupported %s=%s\n",name,value);
+ exit(2);
+}
+
#define CDSTAT_READ 0x20
#define CDSTAT_SEEK 0x40
#define CDSTAT_PLAY 0x80
@@ -907,11 +967,18 @@ static void set_irq(int type) {
* so re-arm the latch (the delayed present below raises it once). */
cdrom_irq_generation++;
cdrom_intc_request_latched = 0;
- /* Arm the presentation latency: this response must NOT be presented to INTC
- * synchronously inside the guest store that triggered it (see the
- * CDROM_IRQ_PRESENT_DELAY note). Absolute due — not a slice-relative
- * countdown (see pending.due_cyc). */
- cdrom_irq_present_due = psx_cycle_count + (uint64_t)CDROM_IRQ_PRESENT_DELAY;
+ /* A command response must not appear inside the command/ack store that
+ * produced it. Asynchronous INT1 already comes from a sector deadline (or
+ * the separately scheduled pending-data-ready release). Its readable IRQ
+ * flag and INTC line must become visible together. Holding INTC for another
+ * 5000 cycles lets the next command's preparatory ACK erase a new sector
+ * notification before the CPU has ever received it. This preserves the
+ * single-outstanding latch and command queue; it changes no buffer owner.
+ * Octoshock 2.2.2 CheckAIP/WriteIRQ likewise publishes async flag+line together.
+ */
+ cdrom_irq_present_due = (type == CDIRQ_DATA_READY || s_source_clock) ? psx_cycle_count
+ : psx_cycle_count + (uint64_t)CDROM_IRQ_PRESENT_DELAY;
+ if (s_source_clock) s_source_ready_due=0; /* 2000 clocks begin after IRQ acknowledgement. */
trace_cdrom('I', 0, (uint32_t)type, 0);
/* DEQUEUE: CD response/data event fired (aux = CD irq type). */
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_CD_IRQ, (uint32_t)type);
@@ -1559,7 +1626,82 @@ static int read_continues_current_stream(void) {
return 1;
}
+/* A pending Setloc is also a seek when consumed by ReadN/ReadS. The source
+ * comparison for this model is Octoshock 2.2.2 CalcSeekTime/ReadBase:
+ * https://github.com/TASEmulators/BizHawk/blob/2.2.2/psx/octoshock/psx/cdc.cpp
+ * Its deterministic component models travel across a 72-minute disc in one
+ * second, a 300ms long-seek settle, and a simplified paused-drive restart.
+ * This independently expressed lower bound omits its 0..25000-cycle jitter;
+ * it is an emulator timing model, not a hardware-calibrated exact guarantee.
+ * Command response and sector pipeline delays remain separate below.
+ */
+static int source_seek_lower_bound(int origin,int target,int motor_on,int paused,uint8_t mode) {
+ int64_t cycles=0;
+ if(!motor_on) {origin=0;cycles=33868800;}
+ int64_t distance=llabs((int64_t)target-origin);
+ int64_t travel=distance*33868800/(72*60*75);
+ cycles+=travel>20000?travel:20000;
+ if(distance>=2250)cycles+=10160640;
+ else if(paused)cycles+=(mode&0x80)?1237952:2475904;
+ return cycles>INT32_MAX?INT32_MAX:(int)cycles;
+}
+static int implicit_read_seek_cycles(void) {
+ if (s_source_clock) {
+ int origin=msf_to_lba(read_min,read_sec,read_sect);
+ int target=setloc_pending?s_setloc_lba:origin;
+ if(origin<0)origin=0;
+ if(target<0)target=0;
+ int delay=source_seek_lower_bound(origin,target,!!(stat_reg&CDSTAT_MOTOR),s_source_seek_paused,mode_reg);
+ uint32_t jitter=source_clock_random(25000);
+ return delay>INT32_MAX-(int)jitter?INT32_MAX:delay+(int)jitter;
+ }
+ if (!setloc_pending) return 0;
+ int origin=last_sector_lba>=0?last_sector_lba:0;
+ return apply_speed(source_seek_lower_bound(origin,s_setloc_lba,(stat_reg&CDSTAT_MOTOR)!=0,
+ !reading&&!(stat_reg&CDSTAT_PLAY),mode_reg));
+}
+/* This optional comparison adds only the independently expressed source seek
+ * lower bound. The source's global PRNG jitter and physical drive-head position
+ * are not recreated. The first cold ReadTOC's paused/zero position is measured. */
+static int source_toc_seek_cycles(void) {
+ int origin=last_sector_lba>=0?last_sector_lba:0;
+ /* Pause rewinds the source drive head without changing the last sector
+ * delivered to the host. ReadTOC seeks from that stopped head, just as
+ * a subsequent explicit seek or resumed ReadN does. */
+ if(s_source_clock && s_source_seek_paused && !reading) {
+ origin=msf_to_lba(read_min,read_sec,read_sect);
+ if(origin<0)origin=0;
+ }
+ int motor=(stat_reg&CDSTAT_MOTOR)!=0;
+ int paused=motor&&!reading&&!(stat_reg&(CDSTAT_READ|CDSTAT_SEEK|CDSTAT_PLAY));
+ int delay=source_seek_lower_bound(origin,0,motor,paused,mode_reg);
+ uint32_t jitter=s_source_clock?source_clock_random(25000):0;
+ return delay>INT32_MAX-(int)jitter?INT32_MAX:delay+(int)jitter;
+}
+
+static int source_explicit_seek_cycles(uint8_t cmd) {
+ /* Independently expressed deterministic part of original Octoshock 2.2.2
+ * Command_SeekL/P. The native next-sector cursor approximates physical
+ * position; source jitter/rotation/pipeline head state remain unmodeled. */
+ int origin = msf_to_lba(read_min, read_sec, read_sect);
+ int target = msf_to_lba(seek_min, seek_sec, seek_sect);
+ if (origin < 0) origin = 0;
+ if (target < 0) target = 0;
+ int seek = source_seek_lower_bound(origin, target,
+ !!(stat_reg & CDSTAT_MOTOR), s_source_seek_paused, mode_reg);
+ if(s_source_clock) {
+ uint32_t jitter=source_clock_random(25000);
+ seek=seek>INT32_MAX-(int)jitter?INT32_MAX:seek+(int)jitter;
+ }
+ int header_period = cmd == 0x15 ?
+ CDROM_SINGLE_SPEED_SECTOR_CYCLES / ((mode_reg & 0x80) ? 2 : 1) : 0;
+ return seek > INT32_MAX - header_period ? INT32_MAX : seek + header_period;
+}
+
static void start_read_stream(uint8_t cmd) {
+ int source_target = setloc_pending ? s_setloc_lba :
+ msf_to_lba(read_min, read_sec, read_sect);
+ int seek_cycles = implicit_read_seek_cycles();
cdda_playing = 0;
cdda_track = 0;
cdda_delay = 0;
@@ -1577,14 +1719,20 @@ static void start_read_stream(uint8_t cmd) {
read_min = seek_min;
read_sec = seek_sec;
read_sect = seek_sect;
+ if (s_source_clock) {
+ lba_to_msf(source_target, 150, &read_min, &read_sec, &read_sect);
+ s_source_read_start_lba = source_target;
+ }
read_cmd = cmd;
- read_delay = initial_read_delay_cycles();
+ read_delay = seek_cycles + initial_read_delay_cycles();
s_cd_probe_read_start_count++;
s_cd_probe_read_start_cycles += (uint64_t)read_delay;
s_cd_timing_next_due = psx_cycle_count + (uint64_t)read_delay;
s_cd_timing_stream_starts++;
reading = 1;
- stat_reg |= CDSTAT_READ;
+ s_source_seek_paused = 0;
+ stat_reg &= (uint8_t)~(CDSTAT_SEEK | CDSTAT_READ | CDSTAT_PLAY);
+ stat_reg |= seek_cycles ? CDSTAT_SEEK : CDSTAT_READ;
/* ENQUEUE: sector-read stream scheduled (due in read_delay cycles). A
* content load that happens in OFF but not ON shows up as a missing
* SRC_CD_READ enqueue here. */
@@ -1625,6 +1773,7 @@ static void stop_cdda_playback(void) {
cdda_delay = 0;
cdda_data_end_pending = 0;
stat_reg &= (uint8_t)~CDSTAT_PLAY;
+ s_source_seek_paused = 1;
}
static void deliver_cdda_data_end(void) {
@@ -1798,7 +1947,7 @@ static int data_fifo_ready(void) {
static uint64_t s_dataready_fires; /* INT1 (data-ready) raised per streamed sector — FMV dispatch probe */
uint64_t cdrom_get_dataready_fires(void) { return s_dataready_fires; }
-static int deliver_read_sector(void) {
+static int deliver_read_sector(uint64_t timing_seq) {
int delivered = read_sector_at(read_min, read_sec, read_sect);
advance_msf(&read_min, &read_sec, &read_sect);
if (!delivered) return 0;
@@ -1807,6 +1956,7 @@ static int deliver_read_sector(void) {
/* Delivered immediately: this INT1 announces the slot just filled. */
s_ring_read = s_ring_write;
set_irq(CDIRQ_DATA_READY);
+ cd_timing_arm_irq(timing_seq);
fire_cdrom_irq();
s_dataready_fires++;
return 1;
@@ -1921,6 +2071,16 @@ static void cd_bisect_cmd_log(const char *kind, uint8_t cmd,
static void try_execute_queued_command(void) {
if (!queued_cmd.pending || irq_flag != 0) return;
+ if(s_source_clock && (psx_cycle_count0) {
+ s_source_args_remaining--;s_source_command_phase=0;
+ s_source_command_due=psx_cycle_count+1815u;
+ } else {
+ s_source_command_phase=1;s_source_command_due=psx_cycle_count+8500u;
+ }
+ return;
+ }
uint8_t cmd = queued_cmd.cmd;
int count = queued_cmd.param_count;
@@ -1939,6 +2099,26 @@ static void try_execute_queued_command(void) {
}
static void queue_or_exec_command(uint8_t cmd) {
+ if(s_source_clock) {
+ /* Source reception: 12315+jitter, then 1815 per argument and 8500.
+ * Capture the already-written argument packet. Post-command argument
+ * writes are outside this first clock profile's qualification. */
+ if(param_count<0 || param_count>PARAM_FIFO_SIZE) {
+ fprintf(stderr,"[CDROM] Invalid source clock argument count\n");exit(2);
+ }
+ if(cmd==0x03) {
+ fprintf(stderr,"[CDROM] Source clock CDDA Play seek is not qualified\n");exit(2);
+ }
+ s_source_command_due=psx_cycle_count+12315u+source_clock_random(3000);
+ s_source_command_phase=-1;s_source_args_remaining=param_count;
+ queued_cmd.cmd=cmd;queued_cmd.param_count=param_count;queued_cmd.pending=1;
+ memcpy(queued_cmd.params,param_fifo,(size_t)param_count);
+ pending.pending=0; /* Source reception replaces an outstanding second response. */
+ param_count=0;
+ trace_cdrom('Q',0,cmd,0);
+ record_command_history('Q',cmd,queued_cmd.params,queued_cmd.param_count);
+ return;
+ }
if (irq_flag == 0) {
exec_command(cmd);
return;
@@ -1977,6 +2157,15 @@ static int pause_complete_delay_cycles(void) {
return 5000;
int lba = reading ? msf_to_lba(read_min, read_sec, read_sect)
: last_sector_lba;
+ if (s_source_clock && reading) {
+ /* Source Command_Pause rewinds min(4, physical reads). With its
+ * two-sector pipe this is max(stream origin, next delivery - 2).
+ * Before any delivery, all physical reads are rewound to the origin.
+ * Preserve that stopped head for a subsequent seek or resumed read. */
+ lba -= 2;
+ if (lba < s_source_read_start_lba) lba = s_source_read_start_lba;
+ lba_to_msf(lba, 150, &read_min, &read_sec, &read_sect);
+ }
if (lba < 0) lba = 0;
int64_t cycles = 1124584 + (int64_t)lba * 42596 / (75 * 60);
if (!(mode_reg & 0x80))
@@ -2035,6 +2224,9 @@ static void exec_command(uint8_t cmd) {
memcpy(cmd_params, param_fifo, (size_t)cmd_param_count);
response_clear();
+ if(s_source_clock && s_source_reset_due && cmd!=0x01 && cmd!=0x0A) {
+ fprintf(stderr,"[CDROM] Source command %02X during drive reset is not qualified\n",cmd);exit(2);
+ }
switch (cmd) {
case 0x01: /* GetStat */
if (has_disc()) {
@@ -2043,6 +2235,7 @@ static void exec_command(uint8_t cmd) {
stat_reg |= CDSTAT_SHELL;
}
response_push(stat_reg);
+ if(s_source_cold_status_model && has_disc()) stat_reg &= (uint8_t)~CDSTAT_SHELL;
set_irq(CDIRQ_ACK);
break;
@@ -2070,8 +2263,8 @@ static void exec_command(uint8_t cmd) {
break;
}
if (read_continues_current_stream()) break; /* ACKed inside */
- start_read_stream(cmd);
response_push(stat_reg);
+ start_read_stream(cmd);
set_irq(CDIRQ_ACK);
break;
@@ -2114,6 +2307,7 @@ static void exec_command(uint8_t cmd) {
* timeout) and never advances past its first content load. */
stop_read_stream();
stop_cdda_playback();
+ s_source_seek_paused = 0;
xa_reset_decode();
spu_cd_audio_reset();
stat_reg &= ~(CDSTAT_READ | CDSTAT_PLAY | CDSTAT_SEEK);
@@ -2128,13 +2322,17 @@ static void exec_command(uint8_t cmd) {
break;
case 0x09: { /* Pause */
+ /* Source Command_Pause queues MakeStatus before changing the drive
+ * state. Its ACK describes the active stream; completion describes
+ * the paused drive. Preserve the default response image. */
+ uint8_t source_ack_status = stat_reg;
int complete_delay = pause_complete_delay_cycles();
stop_read_stream();
stop_cdda_playback();
xa_reset_decode();
spu_cd_audio_reset();
- stat_reg &= ~(CDSTAT_READ | CDSTAT_PLAY);
- response_push(stat_reg);
+ stat_reg &= ~(CDSTAT_READ | CDSTAT_PLAY | CDSTAT_SEEK);
+ response_push(s_source_clock ? source_ack_status : stat_reg);
set_irq(CDIRQ_ACK);
pending_arm(0x09, complete_delay, 1);
s_cd_probe_pause_count++;
@@ -2142,26 +2340,33 @@ static void exec_command(uint8_t cmd) {
break;
}
- case 0x0A: /* Init */
+ case 0x0A: /* Init (named Command_Reset in original Octoshock 2.2.2) */
+ if(s_source_clock) {
+ if(reading || cdda_playing || pending_dataready) {
+ fprintf(stderr,"[CDROM] Source reset from an active stream is not qualified\n");exit(2);
+ }
+ /* ACK reports the old state. Repeated reset does not restart the
+ * drive timer. Empty/paused stream is the qualified source scope. */
+ response_push(stat_reg);
+ set_irq(CDIRQ_ACK);
+ if(!s_source_reset_due) {
+ s_source_reset_due=psx_cycle_count+1136000u;
+ stat_reg=has_disc()?CDSTAT_MOTOR:CDSTAT_SHELL;
+ }
+ break;
+ }
stop_read_stream();
stop_cdda_playback();
spu_cd_audio_reset();
xa_reset_decode();
stat_reg = has_disc() ? CDSTAT_MOTOR : CDSTAT_SHELL;
+ s_source_seek_paused = has_disc() ? 1 : 0;
response_push(stat_reg);
set_irq(CDIRQ_ACK);
- /* The old 1136000-cycle (34 ms) value was borrowed from Beetle's
- * Command_RESET — a different command. Init(0x0A) on an already-
- * spinning drive completes far faster on real hardware, and OpenBIOS
- * depends on it: its cdromInnerInit spin-waits only ~30000 loop
- * iterations (single-digit ms) for the INT2 completion, then
- * re-issues Init — which re-armed our 34 ms clock every retry, a
- * permanent livelock (Init spam at 2/frame, stalling CD boot).
- * Sony's driver never noticed: it waits on the completion event with
- * no short timeout. 131072 cycles (~3.9 ms; an arbitrary power of
- * two, not a measured constant) sits inside OpenBIOS's window and
- * within real-hardware quick-init behavior; cross-check against
- * Beetle's exact PS_CDC::Command_Init figure when refining. */
+ /* Retained default OpenBIOS timeout accommodation. This arbitrary
+ * 131072-cycle delay is not a measured hardware constant. Original
+ * Octoshock's Command_Reset is this same opcode, despite its name;
+ * the explicit source clock above uses that core's drive deadline. */
pending_arm(0x0A, 131072, 1);
break;
@@ -2337,6 +2542,7 @@ static void exec_command(uint8_t cmd) {
}
}
}
+ s_source_seek_paused = 0;
response_push(stat_reg);
set_irq(CDIRQ_ACK);
break;
@@ -2349,14 +2555,24 @@ static void exec_command(uint8_t cmd) {
set_irq(CDIRQ_ERROR);
break;
}
+ {
+ int lat = s_source_explicit_seek_model ? source_explicit_seek_cycles(cmd) :
+ seek_complete_delay_cycles();
+ /* Plain seeks ACK the old state, cancel the stream, then own SEEK.
+ * PSX-SPX SeekL/P; upstream cd55e8a stops/retargets the read producer.
+ * The already admitted guest data FIFO is independent and survives. */
+ response_push(stat_reg);
+ set_irq(CDIRQ_ACK);
+ stop_read_stream();
xa_reset_decode();
spu_cd_audio_reset();
stop_cdda_playback();
- stat_reg |= CDSTAT_SEEK;
- response_push(stat_reg);
- set_irq(CDIRQ_ACK);
- {
- int lat = seek_complete_delay_cycles();
+ s_source_seek_paused = 0; /* source STANDBY after plain seek */
+ read_min = seek_min;
+ read_sec = seek_sec;
+ read_sect = seek_sect;
+ stat_reg = (stat_reg & ~(CDSTAT_READ | CDSTAT_PLAY)) |
+ CDSTAT_MOTOR | CDSTAT_SEEK;
pending_arm(cmd, lat, 1); /* 0x15/0x16 — completed in process_pending */
s_cd_probe_seek_count++;
s_cd_probe_seek_cycles += (uint64_t)lat;
@@ -2381,8 +2597,8 @@ static void exec_command(uint8_t cmd) {
break;
}
if (read_continues_current_stream()) break; /* ACKed inside */
- start_read_stream(cmd);
response_push(stat_reg);
+ start_read_stream(cmd);
set_irq(CDIRQ_ACK);
break;
@@ -2392,24 +2608,23 @@ static void exec_command(uint8_t cmd) {
/* Beetle PS_CDC::Command_ReadTOC: ~30M cycles (a near-second TOC
* re-scan; Beetle adds a seek term on top — we keep the dominant
* constant). Unscaled — authentic-latency class. */
- pending_arm(0x1E, 30000000, 1);
+ {
+ int seek=s_source_toc_seek_model?source_toc_seek_cycles():0;
+ int delay=seek>INT32_MAX-30000000?INT32_MAX:30000000+seek;
+ pending_arm(0x1E, delay, 1);
+ s_source_seek_paused = 1;
+ }
break;
case 0x19: /* Test */
if (param_count >= 1 && param_fifo[0] == 0x20) {
- /* CD controller firmware version (BCD date + region). This BIOS is
- * SCPH-1001, whose sub-CPU reports the 1994 controller: 94/09/19 C0.
- * The value must be < 0x95 in the high byte — the shell's CD-init
- * (func at ROM 0x1DF50) sets kernel flag [0xA000DFFC]=1 when the
- * version byte >= 0x95, which later makes the boot CD-open
- * (0xBFC0D570) issue a spurious ReadTOC that wedges the game's
- * streaming reads. Beetle hardcodes the PSone-era 0x97 regardless of
- * BIOS, which is wrong for SCPH-1001; matching the real 1994
- * controller keeps the flag clear (Kula World demo-load wedge). */
- response_push(0x94);
- response_push(0x09);
- response_push(0x19);
- response_push(0xC0);
+ /* Two documented HC05 identities: PU-7 default, PU-18 source.
+ * The old default also suppresses BIOS ReadTOC; retain it without
+ * claiming that firmware selection proves main-BIOS compatibility. */
+ static const uint8_t version[2][4] = {
+ {0x94,0x09,0x19,0xC0}, {0x97,0x01,0x10,0xC2}
+ };
+ for(int i=0;i<4;i++) response_push(version[s_source_firmware_model][i]);
set_irq(CDIRQ_ACK);
} else {
response_push(stat_reg);
@@ -2432,6 +2647,22 @@ static void exec_command(uint8_t cmd) {
cd_bisect_cmd_log("ISSUE", cmd, cmd_params, cmd_param_count);
}
+static void process_source_reset(void) {
+ if(!s_source_clock || !s_source_reset_due || psx_cycle_counts_source_ready_due?s_source_command_due:s_source_ready_due;
+ best=(uint32_t)cycles_until_due(due);
+ }
/* Armed response awaiting presentation (raises bit2 at present_due). */
if (cdrom_irq_mask_matches_reason(irq_enable, irq_flag)) {
int rem = irq_present_rem_cycles();
@@ -2986,6 +3252,10 @@ uint32_t cdrom_cycles_to_irq(uint32_t i_mask) {
/* Pending second response: slice to due_cyc while the drive clock runs.
* Once due, presentation waits on irq_flag clear (CPU ack) — do not
* advertise a deliverable CD IRQ until the FIFO is free. */
+ if(s_source_clock && s_source_reset_due) {
+ uint32_t d=(uint32_t)cycles_until_due(s_source_reset_due);
+ if(d= pending_present_due) {
pending_present_due = 0;
- if (pending_dataready && irq_flag == 0)
+ if (pending_dataready && source_clock_receive_ready())
present_pending_dataready();
+ else if(s_source_clock && pending_dataready && irq_flag==0)
+ pending_present_due=s_source_ready_due;
}
process_read_stream(cycles);
process_cdda_stream(cycles);
+ /* A command admitted at this interval's end must not age its new stream
+ * by the interval that elapsed before admission. */
+ if(s_source_clock)try_execute_queued_command();
refresh_cdrom_irq_line();
}
@@ -3030,13 +3306,15 @@ void cdrom_tick(void) {
cdrom_advance(33868u);
}
-uint32_t cdrom_dma_read(void) {
+static uint32_t cdrom_dma_read_internal(int padded) {
uint32_t val = 0;
int got = 0;
if ((request_reg & CDROM_REQUEST_BFRD) && rb_available() &&
- RB_.pos + 4 <= RB_.size) {
- memcpy(&val, RB_.data + RB_.pos, 4);
- RB_.pos += 4;
+ (padded || RB_.pos + 4 <= RB_.size)) {
+ int count=RB_.size-RB_.pos;
+ if(count>4)count=4;
+ memcpy(&val, RB_.data + RB_.pos, (size_t)count);
+ RB_.pos += count;
got = 1;
}
/* Per-word DMA data reads flood the CD trace ring (hundreds per sector) and
@@ -3054,6 +3332,9 @@ uint32_t cdrom_dma_read(void) {
return val;
}
+uint32_t cdrom_dma_read(void) { return cdrom_dma_read_internal(0); }
+uint32_t cdrom_dma_read_padded(void) { return cdrom_dma_read_internal(1); }
+
int cdrom_dma_ready(void) {
if (request_reg & CDROM_REQUEST_BFRD) ring_note_starved();
return (request_reg & CDROM_REQUEST_BFRD) && rb_available() &&
@@ -3324,6 +3605,18 @@ static int cdrom_snap_emit(PstW *w) {
W8(pending.cmd); WI(pending.pending); WI(pending_rem_cycles()); WI(pending.phase);
W8(queued_cmd.cmd); WB(queued_cmd.params); WI(queued_cmd.param_count); WI(queued_cmd.pending);
W8(pending_dataready); W8(pending_dataready_stat);
+ /* This explicit private profile adds its timing state to the CD section.
+ * Default bytes stay unchanged. Full-machine/cross-profile restore remains
+ * unqualified; matching-profile controller state is not reconstructed. */
+ if (s_source_explicit_seek_model) W8(s_source_seek_paused);
+ if(s_source_clock) {
+ WI(s_source_read_start_lba);
+ WU(0x33434c43u);WB(s_source_clock_tape.sha256);
+ WU(s_source_clock_tape.count);WU(s_source_clock_tape.cursor);WU(s_source_clock_calls);
+ WI(s_source_command_phase);WI(s_source_args_remaining);
+ WI(cycles_until_due(s_source_command_due));WI(cycles_until_due(s_source_ready_due));
+ WI(setloc_pending);WI(s_source_reset_due!=0);WI(cycles_until_due(s_source_reset_due));
+ }
#undef W8
#undef WI
#undef WU
@@ -3388,6 +3681,19 @@ static int cdrom_snap_parse(PstR *r) {
R8(pending.cmd); RI(pending.pending); RI(pending_rem); RI(pending.phase);
R8(queued_cmd.cmd); RB(queued_cmd.params); RI(queued_cmd.param_count); RI(queued_cmd.pending);
R8(pending_dataready); R8(pending_dataready_stat);
+ if (s_source_explicit_seek_model) R8(s_source_seek_paused);
+ if(s_source_clock) {
+ uint8_t identity[32];uint32_t signature,count;
+ int command_rem,ready_rem,reset_active,reset_rem;
+ RI(s_source_read_start_lba);
+ RU(signature);RB(identity);RU(count);RU(s_source_clock_tape.cursor);RU(s_source_clock_calls);
+ RI(s_source_command_phase);RI(s_source_args_remaining);
+ RI(command_rem);RI(ready_rem);RI(setloc_pending);RI(reset_active);RI(reset_rem);
+ (void)signature;(void)count; /* Identity and range checks precede any parse mutation. */
+ s_source_command_due=psx_cycle_count+(uint32_t)command_rem;
+ s_source_ready_due=psx_cycle_count+(uint32_t)ready_rem;
+ s_source_reset_due=reset_active?psx_cycle_count+(uint32_t)reset_rem:0;
+ }
#undef R8
#undef RI
#undef RU
@@ -3416,6 +3722,21 @@ void cdrom_snapshot_write(uint8_t *p) {
int cdrom_snapshot_read(const uint8_t *p, uint32_t len) {
PstR r;
if (len != cdrom_snapshot_bytes()) return 0;
+ uint32_t clock_bytes=s_source_clock?80u:0u;
+ if (s_source_explicit_seek_model && p[len-clock_bytes-1] > 1) return 0;
+ if(s_source_clock) {
+ const uint8_t *clock=p+len-clock_bytes+4;
+ int32_t phase=(int32_t)cd_tape_le32(clock+48);
+ if(cd_tape_le32(clock)!=0x33434c43u || memcmp(clock+4,s_source_clock_tape.sha256,32) ||
+ cd_tape_le32(clock+36)!=s_source_clock_tape.count ||
+ cd_tape_le32(clock+40)>s_source_clock_tape.count ||
+ cd_tape_le32(clock+44)>cd_tape_le32(clock+40) ||
+ phase < -1 || phase > 1 || cd_tape_le32(clock+52)>PARAM_FIFO_SIZE ||
+ cd_tape_le32(clock+56)>INT32_MAX || cd_tape_le32(clock+60)>2000u ||
+ cd_tape_le32(clock+64)>1u || cd_tape_le32(clock+68)>1u ||
+ cd_tape_le32(clock+72)>1136000u ||
+ (!cd_tape_le32(clock+68) && cd_tape_le32(clock+72))) return 0;
+ }
pst_r_init(&r, p, len);
if (!cdrom_snap_parse(&r))
return 0;
diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c
index 659cc725b..8758f68bb 100644
--- a/runtime/src/debug_server.c
+++ b/runtime/src/debug_server.c
@@ -282,7 +282,8 @@ static inline void rec_event(uint8_t kind, uint32_t addr, uint32_t val,
void debug_server_trace_ram_read_watch(uint32_t phys, uint32_t val)
{
if (phys >= s_rwatch_lo && phys < s_rwatch_hi)
- rec_event(REC_KIND_RAM_R, phys, val, 0, 0);
+ rec_event(REC_KIND_RAM_R, phys, val, 0,
+ debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0);
}
/* ---- CPU state pointer (set at init) ---- */
@@ -322,16 +323,54 @@ static uint8_t s_axis_st[4] = { 0x80, 0x80, 0x80, 0x80 };
* queue in debug_server_get_input_override() avoids host/TCP timing gaps
* between short presses and remains deterministic while turbo loads are active.
*/
-#define INPUT_ROUTE_MAX_STEPS 4096
-typedef struct {
- uint32_t frames;
- uint16_t buttons;
-} InputRouteStep;
+#include "input_route_file.h"
+#include "input_route_observer.h"
static PSX_BSS InputRouteStep s_input_route[INPUT_ROUTE_MAX_STEPS];
static uint32_t s_input_route_count = 0;
static uint32_t s_input_route_index = 0;
static uint32_t s_input_route_remaining = 0;
static int s_input_route_active = 0;
+static int s_input_route_file_mode = 0;
+static uint32_t s_input_route_consumed = 0;
+#include "input_update_replay.h"
+#include "input_instruction_histogram_impl.h"
+
+/* Called on the emulator thread before guest execution. Fail closed; a bad
+ * file never exposes a partial route. Existing TCP routes remain unchanged. */
+int debug_server_preload_input_route(const char *path)
+{
+ InputRouteStep *staged;
+ uint32_t count = 0, frames = 0;
+ FILE *f;
+ const char *error;
+ if (s_frame_count || s_input_route_active || s_input_route_count) return 0;
+ f = fopen(path, "rb");
+ if (!f) return 0;
+ staged = (InputRouteStep *)calloc(INPUT_ROUTE_MAX_STEPS, sizeof(*staged));
+ if (!staged) { fclose(f); return 0; }
+ error = input_route_read(f, staged, &count, &frames);
+ if (fclose(f) != 0 && !error) error = "close error";
+ if (error) {
+ fprintf(stderr, "input route rejected: %s\n", error);
+ free(staged);
+ return 0;
+ }
+ memcpy(s_input_route, staged, count * sizeof(*staged));
+ free(staged);
+ if (!update_configure(s_input_route,count,frames)) return 0;
+ if (!input_route_observer_init(s_update_enabled ? s_update_config[7] : frames)) return 0;
+ if (!input_instruction_histogram_configure()) return 0;
+ if (s_update_enabled) s_update_log=input_route_observer_output("update-clock.jsonl");
+ s_input_override = -1; s_input_frames = 0; s_axis_override = 0;
+ s_input_route_count = count; s_input_route_index = 0;
+ s_input_route_remaining = s_input_route[0].frames;
+ s_input_route_active = 1;
+ s_input_route_file_mode = 1;
+ s_input_route_consumed = 0;
+ fprintf(stdout, "input_route_preloaded: start_frame=0 frames=%u steps=%u\n",
+ (unsigned)frames, (unsigned)count);
+ return 1;
+}
/* ---- Frontend turbo override ---- */
static volatile int s_turbo_enabled = 0;
@@ -9486,6 +9525,7 @@ static void handle_card_trace_dump(int id, const char *json)
void debug_server_trace_write_check(uint32_t phys, uint32_t old_val,
uint32_t new_val, uint8_t width)
{
+ update_accept_write(phys,width,g_debug_last_store_pc,debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0,old_val,new_val);
#ifdef PSX_NO_DEBUG_TOOLS
(void)phys; (void)old_val; (void)new_val; (void)width;
return;
@@ -12864,6 +12904,52 @@ static void handle_overlay_dump(int id, const char *json)
* (very rare at dump time) cannot make us walk off the end.
* ==================================================================== */
+void debug_server_dump_watched_writes(FILE *f, const uint32_t *addresses, uint32_t count)
+{
+ if (!f || !addresses || count > 32) return;
+ uint64_t total = s_wtrace_all_seq;
+ uint32_t avail = total < WRITE_TRACE_ALL_CAP ? (uint32_t)total : WRITE_TRACE_ALL_CAP;
+ uint32_t start = total < WRITE_TRACE_ALL_CAP ? 0 : s_wtrace_all_head;
+ fprintf(f, "{\"kind\":\"coverage\",\"total_writes\":%llu,\"retained_writes\":%u}\n",
+ (unsigned long long)total, s_wtrace_all ? avail : 0);
+ if (s_rec_frame >= 0) {
+ fprintf(f, "{\"kind\":\"recorded_frame_coverage\",\"frame\":%lld,\"count\":%u,\"overflow\":%u}\n",
+ (long long)s_rec_frame,s_rec_count,s_rec_overflow);
+ for (uint32_t i = 0; i < s_rec_count; ++i) {
+ const RecEntry *e = &s_rec_buf[i];
+ if (e->kind == REC_KIND_MMIO_W && e->addr == 0x1F801040u) {
+ fprintf(f, "{\"kind\":\"recorded_sio_write\",\"i\":%u,\"frame\":%lld,\"cycle\":%llu,\"new\":%u,\"pc\":%u,\"ra\":%u}\n",
+ i,(long long)s_rec_frame,(unsigned long long)e->cyc,e->val,e->pc,e->ra);
+ }
+ if (e->kind == REC_KIND_MMIO_W || e->kind == REC_KIND_MMIO_R) {
+ fprintf(f, "{\"kind\":\"%s\",\"i\":%u,\"frame\":%lld,\"cycle\":%llu,\"addr\":%u,\"new\":%u,\"pc\":%u,\"ra\":%u}\n",
+ e->kind == REC_KIND_MMIO_R ? "recorded_mmio_read" : "recorded_mmio_write",
+ i,(long long)s_rec_frame,(unsigned long long)e->cyc,e->addr,e->val,e->pc,e->ra);
+ }
+ if (e->kind != REC_KIND_RAM_W && e->kind != REC_KIND_RAM_R) continue;
+ for (uint32_t j = 0; j < count; ++j) {
+ if (e->addr == addresses[j]) {
+ fprintf(f, "{\"kind\":\"%s\",\"i\":%u,\"frame\":%lld,\"cycle\":%llu,\"addr\":%u,\"new\":%u,\"pc\":%u,\"ra\":%u}\n",
+ e->kind == REC_KIND_RAM_R ? "recorded_read" : "recorded_write",
+ i,(long long)s_rec_frame,(unsigned long long)e->cyc,e->addr,e->val,e->pc,e->ra);
+ break;
+ }
+ }
+ }
+ }
+ if (!s_wtrace_all) return;
+ for (uint32_t i = 0; i < avail; ++i) {
+ const WriteTraceAllEntry *e = &s_wtrace_all[(start + i) % WRITE_TRACE_ALL_CAP];
+ for (uint32_t j = 0; j < count; ++j) {
+ if (e->addr < addresses[j] + 2 && addresses[j] < e->addr + e->w) {
+ fprintf(f, "{\"seq\":%llu,\"addr\":%u,\"new\":%u,\"pc\":%u,\"ra\":%u,\"frame\":%u,\"width\":%u}\n",
+ (unsigned long long)e->seq,e->addr,e->new_val,e->pc,e->ra,e->frame,e->w);
+ break;
+ }
+ }
+ }
+}
+
void debug_server_freeze_dump_wtrace_all_json(FILE *f, uint32_t max_count)
{
if (!f) return;
@@ -13963,6 +14049,13 @@ static void process_command(const char *line)
int id = json_get_int(line, "id", 0);
+ /* A preloaded evidence run cannot be edited or rescheduled over TCP. */
+ if (s_input_route_file_mode && strcmp(cmd, "input_route_status") != 0 &&
+ strcmp(cmd, "pad_status") != 0) {
+ send_err(id, "file replay permits only input_route_status and pad_status");
+ return;
+ }
+
for (const CmdEntry *e = s_commands; e->name; e++) {
if (strcmp(cmd, e->name) == 0) {
/* Suppress lockstep memory recording for the WHOLE handler.
@@ -14689,10 +14782,28 @@ int debug_server_is_connected(void)
return s_client != SOCK_INVALID;
}
+void debug_server_note_input_applied(void)
+{
+ if (s_input_route_file_mode)
+ input_route_observer_applied(sio_get_pad_buttons_slot(0),
+ sio_get_pad_connected(0), sio_get_pad_analog(0));
+}
+
int debug_server_get_input_override(void)
{
+ update_boundary();
+ input_instruction_histogram_boundary(s_frame_count);
+ if (s_input_route_file_mode)
+ input_route_observer_boundary(s_input_route_consumed, s_frame_count);
+ if (s_update_enabled) {
+ input_route_observer_input(0xFFFF); ++s_input_route_consumed; return 0xFFFF;
+ }
if (s_input_route_active && s_input_route_index < s_input_route_count) {
int current = (int)s_input_route[s_input_route_index].buttons;
+ if (s_input_route_file_mode) {
+ input_route_observer_input((uint16_t)current);
+ ++s_input_route_consumed;
+ }
if (s_input_route_remaining > 0 && --s_input_route_remaining == 0) {
s_input_route_index++;
if (s_input_route_index < s_input_route_count) {
@@ -14704,6 +14815,12 @@ int debug_server_get_input_override(void)
}
return current;
}
+ /* A file route never releases P1 to physical input at EOF. */
+ if (s_input_route_file_mode) {
+ input_route_observer_input(0xFFFF);
+ ++s_input_route_consumed;
+ return 0xFFFF;
+ }
int current = s_input_override;
if (s_input_override >= 0 && s_input_frames > 0) {
if (--s_input_frames == 0)
diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c
index 1fd738b2d..121094354 100644
--- a/runtime/src/dirty_ram_interp.c
+++ b/runtime/src/dirty_ram_interp.c
@@ -39,6 +39,8 @@
#include
#include
#include
+#include "source_gpu_runtime.h"
+#include "source_cpu_block_bound.h"
uint64_t g_dirty_ram_blocks_run = 0;
uint64_t g_dirty_ram_insns_run = 0;
@@ -92,7 +94,7 @@ extern int g_ls_replay_active; /* defined in the lockstep section; used by e
extern uint64_t g_psx_cycle_fast_limit;
extern int g_event_step_conservative;
-static inline void interp_cyc_step(CPUState *cpu, uint32_t reg_mask) {
+static inline void interp_cyc_step(CPUState *cpu, uint32_t reg_mask, uint32_t load_rt) {
uint8_t w = cpu->read_absorb_which;
if (cpu->read_absorb[w]) {
cpu->read_absorb[w]--;
@@ -106,11 +108,15 @@ static inline void interp_cyc_step(CPUState *cpu, uint32_t reg_mask) {
}
}
psx_cyc_deps(cpu, reg_mask);
+ if (load_rt < 32u && cpu->ld_which_t == load_rt) cpu->ld_which_t = 0u;
psx_cyc_lds(cpu);
}
#else
-static inline void interp_cyc_step(CPUState *cpu, uint32_t reg_mask) {
- psx_cyc_step(cpu, reg_mask);
+static inline void interp_cyc_step(CPUState *cpu, uint32_t reg_mask, uint32_t load_rt) {
+ psx_cyc_base(cpu);
+ psx_cyc_deps(cpu, reg_mask);
+ if (load_rt < 32u && cpu->ld_which_t == load_rt) cpu->ld_which_t = 0u;
+ psx_cyc_lds(cpu);
}
#endif
@@ -134,6 +140,24 @@ static int interp_exception(CPUState *cpu, uint32_t exc_code,
return 1;
}
+/* Synchronous arithmetic faults use the executing opcode and, when present,
+ * its branch owner. Unlike a native dispatcher return, this is an actual
+ * guest exception boundary and retires the older load before the handler. */
+static struct { uint32_t pc,target;int taken,active; } arithmetic_slot;
+static uint64_t arithmetic_exceptions;
+static int interp_arithmetic_overflow(CPUState *cpu,uint32_t pc,uint32_t insn) {
+ uint32_t pending=cpu->cop0[13]&0x0000ff00u;
+ dirty_ram_ld_delay_flush(cpu);
+ interp_exception(cpu,12u,cpu->cop0[8],pc);
+ cpu->cop0[13]=pending|(12u<<2)|((insn<<2)&0x30000000u);
+ if(arithmetic_slot.active && arithmetic_slot.pc==pc) {
+ cpu->cop0[14]=pc-4u;cpu->cop0[6]=arithmetic_slot.target;
+ cpu->cop0[13]|=0x80000000u|(arithmetic_slot.taken?0x40000000u:0u);
+ }
+ arithmetic_exceptions++;
+ return 1;
+}
+
#ifdef PSX_COSIM
static int g_cosim_exec_one_hooked = 0;
static void cosim_exec_one_begin(void) { g_cosim_exec_one_hooked = 0; }
@@ -149,6 +173,9 @@ static int cosim_exec_one_did_hook(void) { return 0; }
static void cosim_exec_one_transfer_hook(uint32_t pc) { (void)pc; }
#endif
uint64_t g_slice_fired = 0; /* diagnostic: slices actually run */
+/* Passive snapshots of an already-computed block gate; no extra device query. */
+uint32_t g_slice_probe_pc, g_slice_probe_deadline, g_slice_probe_bound;
+uint64_t g_slice_probe_cycle;
uint64_t g_slice_irq_taken = 0; /* diagnostic: IRQs taken inside precise-mode */
/* First-divergence trace for the precise slice (PRECISE_IRQ_SLICE.md Task #4). */
uint32_t g_slice_last_block = 0; /* block_addr the guard fired on */
@@ -322,9 +349,11 @@ int psx_exec_phase(void) { return g_exec_phase; }
* 128-byte memory-card frame was stored to one address (cards read as
* permanently unformatted; the send side transmitted one byte 128x).
*
- * Contract: on a same-register conflict the LOAD wins (it retires later),
- * matching the compiled emitter. Pending state is flushed on interpreter exit
- * and before exception delivery, where the pipeline would have drained. */
+ * The original source commits the older load before an ordinary successor's
+ * write, so that successor wins a same-register write conflict. A second load
+ * to the same destination cancels the older load instead. Kernel transfers
+ * retain this owner through the actual guest load-delay instruction. Mixed
+ * compiled/interpreted handoffs outside that region remain separately scoped. */
static uint32_t s_ld_pend_rt = 0;
static uint32_t s_ld_pend_val = 0;
static uint32_t s_ld_pend_age = 0; /* 0 = armed; 1 = delay slot has run */
@@ -510,6 +539,12 @@ static inline uint32_t target26 (uint32_t i) { return i & 0x03FFFFFFu
/* Read a 32-bit instruction word from kernel RAM at the given physical addr.
* Caller has already verified the address is in dirty kernel RAM. */
static inline uint32_t fetch_word(uint32_t phys) {
+ if (phys >= 0x1FC00000u && phys < 0x1FC80000u) {
+ /* Source-profile precision slices may enter the selected BIOS ROM.
+ * This is a byte fetch; instruction timing remains in exec_one. */
+ extern uint32_t psx_read_word(uint32_t);
+ return psx_read_word(phys);
+ }
/* Main RAM is a process-lifetime static allocation. Cache its address so
* instruction fetch does not cross translation units for every guest op. */
static const uint8_t *ram;
@@ -1392,16 +1427,49 @@ static int interp_enter_compiled(CPUState *cpu, uint32_t target) {
* Branches encode their delay slot themselves before returning 1. */
static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn,
uint32_t *next_pc_out);
+static int exec_one_fetched_context(CPUState *cpu, uint32_t pc, uint32_t insn,
+ uint32_t *next_pc_out, int in_slot,
+ uint32_t target, int taken);
+static int precise_irq_deliverable(CPUState *cpu);
+static int precise_irq_before(CPUState *cpu,uint32_t pc) {
+ return precise_irq_deliverable(cpu) && psx_irq_opcode_eligible(pc);
+}
+static int source_dirty_irq_before(CPUState *cpu,uint32_t pc) {
+ /* Ordinary dirty/kernel interpretation owns real instruction boundaries
+ * even when the compiled-block precision slicer is inactive. A pending
+ * source-model IRQ must preempt this opcode, not a later dispatch target. */
+ if(!g_precise_mode && source_gpu_runtime_active() && precise_irq_before(cpu,pc)) {
+ extern uint64_t g_irq_deliver_count;
+ uint64_t before=g_irq_deliver_count;
+ uint32_t previous=g_dirty_safe_resume_pc;
+ cpu->pc=pc;g_dirty_safe_resume_pc=pc;
+ dirty_ram_ld_delay_flush(cpu);
+ psx_check_interrupts(cpu);
+ g_dirty_safe_resume_pc=previous;
+ if(g_irq_deliver_count!=before) {
+ if(!cpu->pc)cpu->pc=pc;
+ return 1;
+ }
+ }
+ return 0;
+}
static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
+ if(source_dirty_irq_before(cpu,pc))return 1;
return exec_one_fetched(cpu, pc, fetch_word(pc & 0x1FFFFFFFu), next_pc_out);
}
/* Forward: helper for delay-slot execution on jumps/branches. */
-static void exec_delay_slot(CPUState *cpu, uint32_t pc) {
+static int exec_delay_slot(CPUState *cpu,uint32_t pc,uint32_t target,int taken) {
/* Delay-slot instruction at pc must NOT be a control transfer.
* Recursively interpret as a single non-branching instruction. */
uint32_t ds_phys = pc & 0x1FFFFFFFu;
uint32_t insn = fetch_word(ds_phys);
+ if(source_gpu_runtime_active() && precise_irq_before(cpu,pc)) {
+ dirty_ram_ld_delay_flush(cpu);
+ if(psx_check_interrupts_delay_slot(cpu,pc,target,taken,insn)) {
+ g_slice_irq_taken++;return 1;
+ }
+ }
uint32_t opc = op_field(insn);
uint32_t fnt = funct_field(insn);
/* Reject branches/jumps in delay slots — undefined on R3000A and our
@@ -1414,15 +1482,28 @@ static void exec_delay_slot(CPUState *cpu, uint32_t pc) {
opc == 0x01 /*regimm*/ ||
(opc == 0x00 && (fnt == 0x08 /*jr*/ || fnt == 0x09 /*jalr*/))) {
(void)abort_unsupported(pc, insn, "control-transfer in delay slot");
- return;
+ return 0;
}
uint32_t dummy_next = 0;
- (void)exec_one_fetched(cpu, pc, insn, &dummy_next);
+ const int arithmetic=opc==8u || (opc==0u && (fnt==0x20u || fnt==0x22u));
+ uint64_t faults=arithmetic_exceptions;
+ /* Only these non-branching arithmetic opcodes can own this context;
+ * RFE/host escape paths never leave an active context behind. */
+ if(arithmetic) {
+ arithmetic_slot.pc=pc;arithmetic_slot.target=target;arithmetic_slot.taken=taken;
+ arithmetic_slot.active=1;
+ }
+ uint64_t slot_takes=g_slice_irq_taken;
+ (void)exec_one_fetched_context(cpu,pc,insn,&dummy_next,1,target,taken);
+ if(arithmetic)arithmetic_slot.active=0;
g_dirty_ram_insns_run++;
+ if(g_slice_irq_taken!=slot_takes)return 1;
+ if(arithmetic_exceptions!=faults)return 1;
/* CYCLE MODEL: the delay-slot instruction is a real retired R3000A instruction
* and is charged its own per-instruction interlock INSIDE exec_one (top-of-fn
* §1+deps+DO_LDS, or psx_cyc_load_* for a load delay slot) — so a branch+slot
* pair costs both, matching hardware. No separate charge here. */
+ return 0;
}
/* Load-delay shim around the decoder (see dirty_ram_ld_delay_flush above).
@@ -1432,8 +1513,76 @@ static void exec_delay_slot(CPUState *cpu, uint32_t pc) {
static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
uint32_t *next_pc_out);
+static uint32_t delayed_value_writer(CPUState *cpu,uint32_t insn) {
+ uint32_t op=op_field(insn),fn=funct_field(insn),rs=rs_field(insn);
+ /* Faulting arithmetic has no destination write; exception entry still
+ * owns retirement of the older pending load. Evaluate with slot operands. */
+ if(op==8u || (!op && (fn==0x20u || fn==0x22u))) {
+ int64_t a=(int32_t)cpu->gpr[rs];
+ int64_t b=op==8u?(int16_t)insn:(int32_t)cpu->gpr[rt_field(insn)];
+ int64_t result=!op && fn==0x22u?a-b:a+b;
+ if(resultINT32_MAX)return 0u;
+ }
+ if(!op) {
+ if(fn==0u || (fn>=2u && fn<=4u) || fn==6u || fn==7u || fn==9u || fn==0x10u || fn==0x12u ||
+ (fn>=0x20u && fn<=0x27u) || fn==0x2au || fn==0x2bu)
+ return rd_field(insn);
+ } else if(op==3u || (op==1u && (rt_field(insn)==0x10u || rt_field(insn)==0x11u)))return 31u;
+ else if((op>=8u && op<=0xfu) || (op>=0x20u && op<=0x26u))return rt_field(insn);
+ else if(op==0x10u && rs==0u && rd_field(insn)<16u && ((1u<cop0[12]&0x40000000u) && (rs==0u || rs==2u))return rt_field(insn);
+ return 0u;
+}
+
static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn,
uint32_t *next_pc_out) {
+ return exec_one_fetched_context(cpu,pc,insn,next_pc_out,0,0,0);
+}
+
+static int exec_one_fetched_context(CPUState *cpu, uint32_t pc, uint32_t insn,
+ uint32_t *next_pc_out, int in_slot,
+ uint32_t target, int taken) {
+ if(source_gpu_runtime_active()) {
+ /* The boundary may wait through a DMA halt. An IRQ raised in that
+ * interval preempts this opcode, before its load cancellation or
+ * register effects. The IRQ path owns its fetch and ordinary step.
+ * Source COP2 bypasses the halt/interrupt opcode table. */
+ psx_cpu_step_boundary(cpu,pc);
+ if(op_field(insn)!=0x12u && precise_irq_deliverable(cpu)) {
+ extern uint64_t g_irq_deliver_count;
+ uint64_t before=g_irq_deliver_count;
+ dirty_ram_ld_delay_flush(cpu);
+ if(in_slot) {
+ arithmetic_slot.active=0;
+ (void)psx_check_interrupts_delay_slot(cpu,pc,target,taken,insn);
+ } else {
+ uint32_t previous=g_dirty_safe_resume_pc;
+ cpu->pc=pc;g_dirty_safe_resume_pc=pc;
+ psx_check_interrupts(cpu);
+ g_dirty_safe_resume_pc=previous;
+ if(!cpu->pc)cpu->pc=pc;
+ }
+ if(g_irq_deliver_count!=before) {
+ g_slice_irq_taken++;
+ return 1;
+ }
+ }
+ }
+ const uint32_t ld_op = op_field(insn);
+ const uint32_t ld_rt = rt_field(insn);
+ const uint32_t pc_phys = pc & 0x1FFFFFFFu;
+ const int in_bios_rom = pc_phys >= 0x1FC00000u && pc_phys < 0x1FC80000u;
+ const int in_bios_kernel_ram = pc_phys < 0x00010000u;
+ /* Source COP2-to-GPR moves use the CPU's delayed value slot as well.
+ * Precise execution retains ownership while that slot is armed, including
+ * a move in a branch delay slot followed by a compiled block leader.
+ * Ordinary high-RAM mixed dispatch still has the historical eager-value
+ * contract; it cannot yet carry a pending value across every host return. */
+ const int source_gte_value = source_gpu_runtime_active() &&
+ (g_precise_mode || in_bios_rom || in_bios_kernel_ram) &&
+ ld_op==0x12u && (rs_field(insn)==0u || rs_field(insn)==2u) &&
+ (cpu->cop0[12]&0x40000000u);
+
/* A load's writeback becomes visible to the instruction AFTER its delay
* slot: load at N, hidden from N+1, visible from N+2. s_ld_pend_age tracks
* that: 0 = armed by the instruction just executed, 1 = the delay slot has
@@ -1464,39 +1613,43 @@ static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn,
cpu->gpr[0] = 0;
} else {
s_ld_pend_age = 1u; /* this instruction IS the delay slot: stay hidden */
+ /* Its operands still see the old GPR. An ordinary write by this
+ * successor then supersedes the pending write. Loads retain the
+ * separate same-destination cancellation below. */
+ const int successor_load=(nx_op>=0x20u && nx_op<=0x26u) ||
+ (nx_op==0x10u && rs_field(insn)==0u) || source_gte_value;
+ if(!successor_load && delayed_value_writer(cpu,insn)==s_ld_pend_rt)
+ s_ld_pend_armed=0;
}
}
/* op 0x20..0x26 = LB/LH/LWL/LW/LBU/LHU/LWR. LWC2 (GTE, 0x32) targets a COP2
* register, not a GPR, so it needs no deferral here. */
- const uint32_t ld_op = op_field(insn);
- const uint32_t ld_rt = rt_field(insn);
- const uint32_t pc_phys = pc & 0x1FFFFFFFu;
/* OpenBIOS executes cardfasttrack both from ROM and from its low-RAM
* kernel copy (for example 0x3554..0x36D4). Its hand-written dependent
* load requires the real R3000A value delay. Game-owned dirty RAM crosses
* mixed compiled/interpreted boundaries that do not carry this pending
* writeback yet, so preserve the historical eager-value contract there. */
- const int in_bios_rom =
- pc_phys >= 0x1FC00000u && pc_phys < 0x1FC80000u;
- const int in_bios_kernel_ram = pc_phys < 0x00010000u;
- const int is_ld = (in_bios_rom || in_bios_kernel_ram) &&
- (ld_op >= 0x20u && ld_op <= 0x26u) &&
+ const int mfc0_value = ld_op==0x10u && rs_field(insn)==0u &&
+ rd_field(insn)<16u && ((1u<= 0x20u && ld_op <= 0x26u) || mfc0_value)) ||
+ source_gte_value) &&
(ld_rt != 0u);
const uint32_t ld_before = is_ld ? cpu->gpr[ld_rt] : 0u;
const int rv = exec_one_fetched_inner(cpu, pc, insn, next_pc_out);
- if (is_ld) {
+ if (is_ld && rv==0) {
const uint32_t loaded = cpu->gpr[ld_rt];
- if (loaded != ld_before) {
+ {
/* Back-to-back loads: retire the older writeback before reusing the
* slot, otherwise its register write would be dropped entirely. */
if (s_ld_pend_armed && s_ld_pend_rt != ld_rt && s_ld_pend_rt != 0u)
cpu->gpr[s_ld_pend_rt] = s_ld_pend_val;
/* Undo the eager write; it becomes visible one instruction later.
- * On a same-register conflict this naturally makes the LOAD win,
- * matching what the compiled backend emits for a dependent pair. */
+ * A same-register second load cancels the older pending write,
+ * including when the new loaded bytes equal the original GPR. */
cpu->gpr[ld_rt] = ld_before;
s_ld_pend_rt = ld_rt;
s_ld_pend_val = loaded;
@@ -1526,7 +1679,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
/* Instruction FETCH cost (I-cache) — charged FIRST, before the §1 base, exactly
* like Beetle ReadInstruction precedes the per-instruction base (cpu.cpp). HIT=+0,
* KSEG1=+4, cached miss=+3+refill; a miss also clears the load give-back. */
- psx_icache_fetch_interp(cpu, pc);
+ if(source_gpu_runtime_active())psx_icache_fetch_interp_after_boundary(cpu,pc);
+ else psx_icache_fetch_interp(cpu, pc);
/* Per-instruction R3000A load-delay interlock (single-source: psx_cyc.h, shared
* with both static emitters). §1 base + GPR_DEPRES + DO_LDS run HERE, before the
@@ -1534,8 +1688,16 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
* (Beetle order). CPU loads (op 0x20-0x26) are skipped here — psx_cyc_load_* runs
* their full interlock inside the body (and arms LDWhich=rt). This replaces the
* old flat per-instruction psx_advance_cycles(psx_instr_base_cycles). */
- if (!(opc >= 0x20u && opc <= 0x26u))
- interp_cyc_step(cpu, psx_cyc_dep_res_mask(insn));
+ if (source_gpu_runtime_active() && opc == 0x12u &&
+ (rs == 0u || rs == 2u) && (cpu->cop0[12] & 0x40000000u)) {
+ /* MFC2/CFC2 cancel an older load to the same GPR before DO_LDS.
+ * The canceled load's timing credit belongs to the zero slot, just
+ * as for an ordinary CPU load. Moving it to the destination instead
+ * loses that credit when a later cache miss clears the active slot.
+ * COP2 reads have no GPR dependency/result clear in the source CPU. */
+ interp_cyc_step(cpu, 0u, rt);
+ } else if (!(opc >= 0x20u && opc <= 0x26u))
+ interp_cyc_step(cpu, psx_cyc_dep_res_mask(insn), 32u);
#endif
/* Widescreen far-backdrop column PRELOAD (auto_backdrop). At a detected
@@ -1649,7 +1811,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
case 0x08: { /* JR rs */
uint32_t target = cpu->gpr[rs];
if (target & 3) return interp_exception(cpu, 4, target, pc); /* LoadAddressError */
- exec_delay_slot(cpu, pc + 4);
+ if(exec_delay_slot(cpu,pc+4,target,1))return 1;
cosim_exec_one_transfer_hook(pc + 4);
/* crossing (if target is compiled) is counted at the block-loop
* tail-transfer site (interp_enter_compiled, §18) — not here, to
@@ -1672,7 +1834,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
* leaks psx_dispatch_call frames. */
if (rd != 0) cpu->gpr[rd] = return_pc;
cpu->gpr[0] = 0;
- exec_delay_slot(cpu, pc + 4);
+ if(exec_delay_slot(cpu,pc+4,target,1))return 1;
cosim_exec_one_transfer_hook(pc + 4);
uint32_t site_sp = cpu->gpr[29]; /* call contract: sp at the call */
#ifndef PSX_NO_DEBUG_TOOLS
@@ -1812,17 +1974,23 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
psx_muldiv_set(cpu, 37u); /* DIVU completion deadline (fixed) */
#endif
return 0;
- case 0x20: /* ADD - overflow traps are delegated if they occur. */
+ case 0x20: /* ADD */
case 0x21: { /* ADDU rd, rs, rt */
uint32_t a = cpu->gpr[rs], b = cpu->gpr[rt];
+ int64_t signed_result=(int64_t)(int32_t)a+(int64_t)(int32_t)b;
+ if(fnt==0x20u && (signed_resultINT32_MAX))
+ return interp_arithmetic_overflow(cpu,pc,insn);
cpu->gpr[rd] = a + b;
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b);
cpu->gpr[0] = 0;
return 0;
}
- case 0x22: /* SUB - overflow traps are delegated if they occur. */
+ case 0x22: /* SUB */
case 0x23: { /* SUBU */
uint32_t a = cpu->gpr[rs], b = cpu->gpr[rt];
+ int64_t signed_result=(int64_t)(int32_t)a-(int64_t)(int32_t)b;
+ if(fnt==0x22u && (signed_resultINT32_MAX))
+ return interp_arithmetic_overflow(cpu,pc,insn);
if (rs == 0 && psx_ws_is_cull_negsub_site(pc))
cpu->gpr[rd] = 0u - b - (uint32_t)psx_ws_x_margin();
else
@@ -1877,7 +2045,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
case 0x02: { /* J target */
uint32_t target = ((pc + 4) & 0xF0000000u) | (target26(insn) << 2);
- exec_delay_slot(cpu, pc + 4);
+ if(exec_delay_slot(cpu,pc+4,target,1))return 1;
cosim_exec_one_transfer_hook(pc + 4);
/* crossing counted at the block-loop tail-transfer site (§18). */
cpu->pc = target;
@@ -1887,7 +2055,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
uint32_t target = ((pc + 4) & 0xF0000000u) | (target26(insn) << 2);
uint32_t return_pc = pc + 8;
cpu->gpr[31] = return_pc;
- exec_delay_slot(cpu, pc + 4);
+ if(exec_delay_slot(cpu,pc+4,target,1))return 1;
cosim_exec_one_transfer_hook(pc + 4);
uint32_t site_sp = cpu->gpr[29]; /* call contract: sp at the call */
#ifndef PSX_NO_DEBUG_TOOLS
@@ -1942,28 +2110,28 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
}
case 0x04: { /* BEQ rs, rt, simm */
int taken = (cpu->gpr[rs] == cpu->gpr[rt]);
- exec_delay_slot(cpu, pc + 4);
+ if(exec_delay_slot(cpu,pc+4,taken?(pc+4+(simm<<2)):(pc+8),taken))return 1;
cosim_exec_one_transfer_hook(pc + 4);
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
return 1;
}
case 0x05: { /* BNE */
int taken = (cpu->gpr[rs] != cpu->gpr[rt]);
- exec_delay_slot(cpu, pc + 4);
+ if(exec_delay_slot(cpu,pc+4,taken?(pc+4+(simm<<2)):(pc+8),taken))return 1;
cosim_exec_one_transfer_hook(pc + 4);
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
return 1;
}
case 0x06: { /* BLEZ */
int taken = ((int32_t)cpu->gpr[rs] <= 0);
- exec_delay_slot(cpu, pc + 4);
+ if(exec_delay_slot(cpu,pc+4,taken?(pc+4+(simm<<2)):(pc+8),taken))return 1;
cosim_exec_one_transfer_hook(pc + 4);
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
return 1;
}
case 0x07: { /* BGTZ */
int taken = ((int32_t)cpu->gpr[rs] > 0);
- exec_delay_slot(cpu, pc + 4);
+ if(exec_delay_slot(cpu,pc+4,taken?(pc+4+(simm<<2)):(pc+8),taken))return 1;
cosim_exec_one_transfer_hook(pc + 4);
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
return 1;
@@ -1987,15 +2155,18 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
cpu->gpr[31] = pc + 8; break;
default: return abort_unsupported(pc, insn, "REGIMM rt");
}
- exec_delay_slot(cpu, pc + 4);
+ if(exec_delay_slot(cpu,pc+4,taken?(pc+4+(simm<<2)):(pc+8),taken))return 1;
cosim_exec_one_transfer_hook(pc + 4);
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
return 1;
}
- case 0x08: /* ADDI rt, rs, simm — same as ADDIU, sans overflow trap (we don't model traps here) */
+ case 0x08: /* ADDI rt, rs, simm */
{
uint32_t widened = 0;
uint32_t a = cpu->gpr[rs];
+ int64_t signed_result=(int64_t)(int32_t)a+(int64_t)simm;
+ if(signed_resultINT32_MAX)
+ return interp_arithmetic_overflow(cpu,pc,insn);
if (psx_ws_angle_site(pc, insn, &widened))
cpu->gpr[rt] = widened;
else
@@ -2505,6 +2676,9 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {
* boundary? Mirrors the gate in psx_check_interrupts: a pending+unmasked I_STAT
* bit, COP0 IEc + IM2 set, and not already inside the exception handler. */
static int precise_irq_deliverable(CPUState *cpu) {
+ /* A caller may own a generated block/local cycle batch. Publish the
+ * completed instruction before inspecting the device IRQ line. */
+ psx_cyc_batch_flush();
extern uint32_t i_stat;
uint32_t sr = cpu->cop0[12];
/* COP0 software interrupts (CAUSE.IP0/IP1 & SR.IM0/IM1): guest-raised via
@@ -2512,6 +2686,10 @@ static int precise_irq_deliverable(CPUState *cpu) {
uint32_t sw_pending = cpu->cop0[13] & sr & 0x0300u;
if ((i_stat & i_mask) == 0 && sw_pending == 0) return 0;
if (psx_get_in_exception()) return 0;
+ /* The actual delivery routine can decline during its inherited cooldown.
+ * Predicting a take here would return at the same PC with no guest cycles,
+ * so that cycle-based cooldown could never expire. */
+ if (psx_interrupt_cooldown_active()) return 0;
if (!(sr & 0x1u)) return 0; /* IEc: interrupts globally enabled */
/* INTC needs IM2; a pending software interrupt is deliverable without it. */
if (!(sr & (1u << 10)) && sw_pending == 0) return 0;
@@ -2531,6 +2709,10 @@ static int precise_irq_deliverable(CPUState *cpu) {
* clean-text PC: it keeps interpreting (exec_one handles arbitrary mid-function
* flow) until cpu->pc satisfies this predicate. */
static int precise_pc_dispatchable(uint32_t pc) {
+ /* Generated entries do not carry the interpreter's deferred GPR value.
+ * Keep its owner through the actual load-delay instruction; an eager
+ * boundary flush would change an immediate consumer's visible operand. */
+ if (s_ld_pend_armed) return 0;
#ifdef PSX_HAS_GAME_DISPATCH
uint32_t phys = pc & 0x1FFFFFFFu;
if (psx_game_address_in_text(pc) && !dirty_ram_is_dirty(phys))
@@ -2573,10 +2755,18 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) {
g_slice_exit_in_text = 0;
#endif
g_slice_exit_want = 0;
- int irq_taken = 0; /* one take per slice (avoid re-taking an unacked IRQ) */
+ int irq_taken = 0; /* default profile limits takes; also requests safe exit */
enum { MAX_PRECISE_INSNS = 200000 };
- for (int i = 0; i < MAX_PRECISE_INSNS; i++) {
- if (!irq_taken && precise_irq_deliverable(cpu)) {
+ /* A host instruction budget cannot retire a pending guest load or create a
+ * generated entry. In the source profile retain ownership until the safe
+ * exit below, even when a branch-slot load spans every lap of a long loop.
+ * Each instruction still advances devices and the normal frontend hooks.
+ * Keep the legacy guard outside this explicit profile. Saturate the
+ * diagnostic iteration count rather than overflowing on a guest spin. */
+ const int source_owned_slice = source_gpu_runtime_active();
+ for (uint32_t i = 0; source_owned_slice || i < MAX_PRECISE_INSNS;
+ i += i != UINT32_MAX) {
+ if ((source_owned_slice || !irq_taken) && precise_irq_before(cpu,pc)) {
uint32_t committed = pc;
extern uint32_t i_stat;
g_slice_last_committed = committed;
@@ -2588,6 +2778,7 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) {
g_cosim_dirty_pump_site = 7;
g_dirty_safe_resume_pc = committed;
s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run;
+ dirty_ram_ld_delay_flush(cpu); /* accepted IRQ retires the prior load */
psx_check_interrupts(cpu);
if (cpu->pc == 0u && committed != 0u) {
cpu->pc = committed;
@@ -2599,7 +2790,9 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) {
irq_taken = 1;
pc = cpu->pc ? cpu->pc : committed;
cpu->pc = pc;
- if (precise_pc_dispatchable(cpu->pc)) {
+ uint32_t resume_phys = cpu->pc & 0x1FFFFFFFu;
+ if (precise_pc_dispatchable(cpu->pc) &&
+ !(resume_phys >= 0x1FC00000u && resume_phys < 0x1FC80000u)) {
g_slice_exit_reason = 1;
g_slice_exit_iter = (uint32_t)i;
g_slice_exit_want = 1;
@@ -2608,10 +2801,24 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) {
continue;
}
+ /* Only RAM and the selected BIOS ROM are valid instruction regions.
+ * ROM admission belongs to the explicit original-source profile. */
+ uint32_t instruction_phys = pc & 0x1FFFFFFFu;
+ if (instruction_phys >= 0x00200000u &&
+ !(source_gpu_runtime_active() && instruction_phys >= 0x1FC00000u &&
+ instruction_phys < 0x1FC80000u)) {
+ extern void psx_fatal_halt(const char *);
+ cpu->pc = pc;
+ psx_fatal_halt("precise slice requires unsupported non-RAM continuation");
+ break;
+ }
uint32_t next_pc = 0;
g_unsupported_seen = 0;
cosim_exec_one_begin();
+ uint64_t slot_takes=g_slice_irq_taken;
int transferred = exec_one(cpu, pc, &next_pc); /* charges its own interlock */
+ int slot_irq_taken = g_slice_irq_taken!=slot_takes;
+ if(slot_irq_taken)irq_taken=1;
g_dirty_ram_insns_run++;
#ifdef PSX_COSIM
if (!cosim_exec_one_did_hook()) { extern void cosim_instr(uint32_t); cosim_instr(pc); }
@@ -2637,11 +2844,11 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) {
* clean game text. */
int want_exit = 0;
- /* Exact take-point: an interrupt deliverable on THIS instruction's cycle is
- * taken before the next instruction retires — once per slice. Re-checking
- * after a take re-fires an IRQ the handler has not acked yet (an
- * 8-takes-in-11-insns storm), so gate on !irq_taken. */
- if (!irq_taken && precise_irq_deliverable(cpu)) {
+ /* Source RFE can re-enable an unacknowledged IRQ while this slice
+ * still owns a mid-block return target. SR/in_exception determine
+ * eligibility at every boundary; a previous take cannot permit one
+ * extra opcode before the next IRQ. Retain the default take limit. */
+ if ((source_owned_slice || !irq_taken) && precise_irq_before(cpu,committed)) {
extern uint32_t i_stat;
g_slice_last_committed = committed;
g_slice_last_istat = i_stat;
@@ -2652,6 +2859,7 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) {
g_cosim_dirty_pump_site = 7;
g_dirty_safe_resume_pc = committed; /* real EPC for exception entry */
s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run;
+ dirty_ram_ld_delay_flush(cpu); /* before the handler's saved-GPR snapshot */
psx_check_interrupts(cpu); /* takes it; runs handler; restores GPRs */
g_dirty_safe_resume_pc = 0;
g_cosim_dirty_pump_site = prev_site;
@@ -2680,7 +2888,13 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) {
/* Hand back ONLY at a dispatchable PC. Otherwise (mid-function clean text)
* keep interpreting until one is reached. */
- if (want_exit && precise_pc_dispatchable(cpu->pc)) {
+ /* A ROM exception may return to a branch opcode inside a generated
+ * block. Continue through that real branch before handing back; its
+ * interrupted delay slot is not a completed control transfer. */
+ uint32_t resume_phys = cpu->pc & 0x1FFFFFFFu;
+ int rom_resume = resume_phys >= 0x1FC00000u && resume_phys < 0x1FC80000u;
+ if (want_exit && precise_pc_dispatchable(cpu->pc) &&
+ (!rom_resume || (transferred && !slot_irq_taken))) {
g_slice_exit_reason = 1;
g_slice_exit_iter = (uint32_t)i;
g_slice_exit_want = 1;
@@ -2728,6 +2942,10 @@ int psx_slice_block_impl(CPUState *cpu, uint32_t block_addr, uint32_t bcyc, int
* drift / faithful per-instruction cycle model — see CLAUDE.md Rule -1). */
if (!g_psx_precise_slice) return 0;
+ uint32_t block_phys = block_addr & 0x1FFFFFFFu;
+ if (block_phys >= 0x1FC00000u && block_phys < 0x1FC80000u &&
+ !source_gpu_runtime_active()) return 0;
+
/* No nested slicing: a handler dispatched from inside precise-mode, and any
* block executed while in_exception, run compiled (interrupts are gated during
* exception handling anyway). Keeps re-entrancy structurally impossible. */
@@ -2745,12 +2963,37 @@ int psx_slice_block_impl(CPUState *cpu, uint32_t block_addr, uint32_t bcyc, int
if (s_slice_margin < 0) s_slice_margin = 0;
}
+ /* Relative device countdowns describe their last serviced clock. The
+ * source CPU may have advanced since then, including deferred block
+ * charges. Bring devices to the entry clock before deciding that an
+ * entire compiled block can safely run past the next IRQ boundary. */
+ if(source_gpu_runtime_active()) {
+ psx_cyc_batch_flush();
+ psx_devices_service_to_now();
+ }
uint32_t deadline = cycles_to_next_event();
- uint32_t budget = bcyc + (uint32_t)s_slice_margin;
- if (budget < bcyc) budget = 0xFFFFFFFFu;
+ uint32_t block_bound=bcyc;
+ if(!side_effects && source_gpu_runtime_active() && (block_addr&0x1fffffffu)<0x200000u)
+ block_bound=source_cpu_block_bound(cpu,block_addr,bcyc,deadline);
+ uint32_t budget = block_bound + (uint32_t)s_slice_margin;
+ if (budget < block_bound) budget = 0xFFFFFFFFu;
int entry_deliverable = precise_irq_deliverable(cpu);
- int has_deadline = s_slice_always || entry_deliverable || (deadline <= budget);
- if (!has_deadline && !side_effects) return 0; /* fast path: no event in this block */
+ /* A DMA halt can consume the entire deadline before the block's first
+ * opcode. Retain instruction ownership through that wait, even when the
+ * block itself is shorter than the currently pending event countdown. */
+ extern int dma_cpu_source_halted(void);
+ int source_halted=source_gpu_runtime_active() && dma_cpu_source_halted();
+ int has_deadline = s_slice_always || source_halted || entry_deliverable || (deadline <= budget);
+ g_slice_probe_pc=block_addr;g_slice_probe_deadline=deadline;
+ g_slice_probe_bound=budget;g_slice_probe_cycle=psx_get_cycle_count();
+ /* A distant device event does not prove value-pipeline safety. Existing
+ * generated COP2 reads write eagerly, including across a guest return.
+ * Use the same precise owner for those RAM blocks; it retains the delayed
+ * value through the consumer before returning to compiled execution. */
+ int gte_value_delay = !has_deadline && !side_effects &&
+ source_gpu_runtime_active() && (cpu->cop0[12]&0x40000000u) &&
+ block_phys<0x200000u && source_cpu_block_gte_value_delay(block_addr,bcyc);
+ if (!has_deadline && !side_effects && !gte_value_delay) return 0;
g_slice_entry_deliverable = (uint32_t)entry_deliverable;
g_slice_fired++;
@@ -3062,7 +3305,8 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
uint32_t before_s3 = cpu->gpr[19];
#endif
cosim_exec_one_begin();
- int transferred = exec_one_fetched(cpu, pc, insn, &next_pc);
+ int transferred = source_dirty_irq_before(cpu,pc) ? 1 :
+ exec_one_fetched(cpu, pc, insn, &next_pc);
#ifndef PSX_NO_DEBUG_TOOLS
/* Armed iff the window is NON-EMPTY, same as the callret ring: a `lo`
* test made address 0 a silent off switch, so lo=0 with a real hi
@@ -3239,6 +3483,14 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
}
#endif
uint32_t target_phys = target & 0x1FFFFFFFu;
+ if(s_ld_pend_armed && target!=0u && target!=stop_addr &&
+ target_phys<0x10000u && dirty_ram_is_dirty(target_phys)) {
+ /* A host dispatcher return is not a guest pipeline boundary.
+ * Preserve a load in a branch delay slot through the next
+ * instruction when its low-RAM kernel owner is available. */
+ pc=target;current_page=target_phys>>12;current_page_dirty=1;
+ continue;
+ }
if (allow_local_dirty_flow && target != 0 &&
target != stop_addr &&
phys_is_overlay_flow_region(target_phys) &&
diff --git a/runtime/src/dma.c b/runtime/src/dma.c
index 2bb30e50f..51fb392dc 100644
--- a/runtime/src/dma.c
+++ b/runtime/src/dma.c
@@ -13,6 +13,7 @@
*/
#include "dma.h"
+#include "source_gpu_runtime.h"
#include "cdrom.h"
#include "crash_trace.h"
#include "dirty_ram_interp.h"
@@ -23,6 +24,7 @@
#include "spu.h"
#include "audio_trace.h"
#include "event_ring.h"
+#include "psx_cycles.h"
#include
#include
#include
@@ -62,6 +64,47 @@ typedef struct {
static DMAAsyncChannel mdec_async[2];
static DMAAsyncChannel cdrom_async;
+/* Optional source-core experiment. This deliberately models Octoshock 2.2.2
+ * OTC service granularity and whole-CPU waiting, not PS1 bus arbitration.
+ * Facts consulted: original dma.cpp RunChannelI/DMA_Update/DMA_Write.
+ * No reference implementation text is incorporated. */
+static int otc_source_model;
+static struct {
+ uint32_t remaining, address;
+ uint64_t last_cycle, next_cycle;
+} otc_source;
+/* Optional manual CD DMA timing only. Register conventions remain native.
+ * Source facts: 64 initial clocks, 9 clocks/word, 128-clock service and CPU
+ * halt. These are original-core compatibility rules, not bus measurements. */
+static int cd_source_model;
+static struct {
+ int32_t budget;
+ uint64_t last_cycle, next_cycle;
+} cd_source;
+/* Optional source request timing for an already active VRAM upload only. */
+static int gpu_upload_source_model;
+static int gpu_ll_source_model;
+static struct {
+ uint32_t active, address, remaining, nodes;
+ int32_t budget;
+ uint64_t last_cycle, next_cycle;
+} gpu_ll_source;
+uint32_t g_dma_cpu_read_wait;
+static uint32_t gpu_upload_live_read_wait;
+static int cpu_read_wait_latched;
+static struct {
+ uint32_t remaining, block_size, in_block, address;
+ int32_t budget;
+ uint64_t last_cycle, next_cycle;
+} gpu_upload_source;
+/* Source SPU request DMA:64 initial clocks,48 per word, global128-clock
+ * updates. Payload and register progress belong to those service calls. */
+static struct {
+ uint32_t remaining, block_size, in_block, address, total_words, start_addr;
+ int32_t budget;
+ uint64_t last_cycle, next_cycle;
+} spu_source;
+
/* ---- CD DMA transfer log ---- */
/* Every forward CH3 DMA that lands below 0x1C0000 (game data region) records
* (setloc_lba, dest_addr, size). Transfers to 0x1C0000+ are FMV/streaming
@@ -321,6 +364,7 @@ static uint32_t transfer_word_count(int ch) {
uint32_t block_count = (channels[ch].bcr >> 16) & 0xFFFFu;
if (ch == 3 && block_size == 0) {
+ if (cd_source_model) return 0x10000u;
return cdrom_dma_sector_word_count();
}
@@ -362,7 +406,10 @@ static void complete_transfer(int ch) {
uint32_t dicr_before = dicr;
uint32_t i_stat_before = i_stat;
channels[ch].chcr &= ~((1u << 24) | (1u << 28));
- if (channel_irq_flag_armed(ch)) {
+ /* Source channel4 latches its enabled completion flag even when the
+ * master IRQ output is disabled; the master gate belongs to IRQ output. */
+ if ((ch==4 && source_gpu_runtime_active()) ?
+ ((dicr>>(16+ch))&1u) : channel_irq_flag_armed(ch)) {
dicr |= (1u << (24 + ch));
raise_dma_irq_on_master_edge(dicr_before);
}
@@ -410,6 +457,11 @@ static void schedule_delayed_complete(int ch, uint32_t total_words,
delayed_complete[ch].total_words = total_words;
delayed_complete[ch].cycles_remaining = (uint32_t)cycles;
event_ring_record_aux(EV_DMA_SCHED, (uint8_t)ch, channels[ch].chcr);
+ /* The MMIO pre-barrier cached deadlines before this channel was armed.
+ * Refresh through the scheduler owner so a later cached event cannot hide
+ * this completion. This publishes no new CPU cycles; nested service is
+ * guarded and its outer pass recomputes the deadline. */
+ psx_devices_service_to_now();
}
static void advance_delayed_complete(int ch, uint32_t cycles) {
@@ -653,6 +705,185 @@ static void execute_ch1_mdec_out(void) {
complete_transfer(1);
}
+static void source_gpu_upload_words(void) {
+ int queued=source_gpu_runtime_active();
+ unsigned to_gpu=channels[2].chcr&1u;
+ g_dma_cur_ch=2;g_dma_initiator_pc=s_dma_ch_initiator_pc[2];
+ g_dma_cur_madr=channels[2].madr;g_dma_cur_bcr=channels[2].bcr;
+ while(gpu_upload_source.remaining && gpu_upload_source.budget>0) {
+ if(!queued && gpu_dma_vram_upload_words()0)gpu_upload_source.budget=0;
+ uint32_t n=gpu_upload_source.block_size?gpu_upload_source.block_size-1u:0u;
+ int can= !queued || !to_gpu || gpu_dma_source_ll_ready()==1;
+ gpu_upload_live_read_wait=gpu_upload_source.remaining && can?(n>200u?200u:n):0u;
+ if(!cpu_read_wait_latched)g_dma_cpu_read_wait=gpu_upload_live_read_wait;
+}
+static void start_source_gpu_upload(void) {
+ uint32_t bs=channels[2].bcr&0xffffu,bc=channels[2].bcr>>16;
+ uint64_t total=(uint64_t)bs*bc;
+ unsigned mode=channels[2].chcr&0x00ffffffu;
+ int queued=source_gpu_runtime_active();
+ if((queued?(mode&~3u)!=0x200u:mode!=0x201u) || !bs || !bc ||
+ (channels[2].madr&0x800000u) || (!queued && total>gpu_dma_vram_upload_words())) {
+ fprintf(stderr,"[dma-model] unsupported source GPU request: CHCR=%08X BCR=%08X MADR=%08X upload=%u\n",channels[2].chcr,channels[2].bcr,channels[2].madr,gpu_dma_vram_upload_words());exit(2);
+ }
+ gpu_upload_source.remaining=(uint32_t)total;
+ gpu_upload_source.block_size=bs;gpu_upload_source.in_block=0;
+ gpu_upload_source.budget=64;
+ gpu_upload_source.last_cycle=psx_cycle_count;
+ gpu_upload_source.next_cycle=psx_cycle_count+128u-(psx_cycle_count&127u);
+ source_gpu_upload_words();
+}
+static void advance_source_gpu(void) {
+ while(gpu_upload_source.remaining && psx_cycle_count>=gpu_upload_source.next_cycle) {
+ gpu_upload_source.budget+=(int32_t)(gpu_upload_source.next_cycle-gpu_upload_source.last_cycle);
+ gpu_upload_source.last_cycle=gpu_upload_source.next_cycle;
+ gpu_upload_source.next_cycle+=128;
+ source_gpu_upload_words();
+ }
+}
+/* Bounded source compatibility experiment. Independently implemented from
+ * observed original-core facts: 64 startup clocks, 15/10 per header, one per
+ * payload, ready checked at a node boundary, 128-clock subsequent service.
+ * Only NOP/environment payloads and the qualified parser-ready states are
+ * admitted. General draw/FIFO timing, forced stop and restore are excluded. */
+static void source_gpu_ll_words(void) {
+ g_dma_cur_ch=2;g_dma_initiator_pc=s_dma_ch_initiator_pc[2];
+ while(gpu_ll_source.active && gpu_ll_source.budget>0) {
+ if(!gpu_ll_source.remaining) {
+ int ready=gpu_dma_source_ll_ready();
+ if(ready<0) {
+ fprintf(stderr,"[dma-model] unsupported source linked-list GPU parser state\n");exit(2);
+ }
+ if(!ready)break;
+ if(channels[2].madr&0x800000u) {
+ fprintf(stderr,"[dma-model] unsupported source linked-list address %08X\n",channels[2].madr);exit(2);
+ }
+ uint32_t at=channels[2].madr&0x1ffffcu;
+ uint32_t header=psx_read_word(at);
+ gpu_ll_source.remaining=header>>24;
+ gpu_ll_source.address=(channels[2].madr+4u)&0xffffffu;
+ channels[2].madr=header&0xffffffu;
+ gpu_set_gp0_linked_list_node(at,gpu_ll_source.nodes++);
+ gpu_ll_source.budget-=gpu_ll_source.remaining?15:10;
+ } else {
+ if(gpu_ll_source.address&0x800000u) {
+ fprintf(stderr,"[dma-model] unsupported source linked-list payload address\n");exit(2);
+ }
+ uint32_t at=gpu_ll_source.address&0x1ffffcu;
+ uint32_t value=psx_read_word(at),opcode=value>>24;
+ if(!source_gpu_runtime_active() && (gpu_dma_source_ll_ready()!=1 ||
+ !(opcode==0 || opcode==1 || (opcode>=0xe1 && opcode<=0xe6)))) {
+ fprintf(stderr,"[dma-model] unsupported source linked-list payload %08X at %08X\n",value,at);exit(2);
+ }
+ gpu_set_gp0_source(at);gpu_write_gp0(value);
+ gpu_ll_source.address=(gpu_ll_source.address+4u)&0xffffffu;
+ gpu_ll_source.remaining--;gpu_ll_source.budget--;
+ }
+ if(!gpu_ll_source.remaining && channels[2].madr==0xffffffu) {
+ gpu_ll_source.active=0;complete_transfer(2);
+ }
+ }
+ /* Source discards unused positive clocks at a ready stall/completion. */
+ if(gpu_ll_source.budget>0)gpu_ll_source.budget=0;
+}
+static void start_source_gpu_ll(void) {
+ if((channels[2].chcr&0x00ffffffu)!=0x401u) {
+ fprintf(stderr,"[dma-model] unsupported source linked-list CHCR=%08X\n",channels[2].chcr);exit(2);
+ }
+ memset(&gpu_ll_source,0,sizeof(gpu_ll_source));
+ gpu_ll_source.active=1;gpu_ll_source.budget=64;
+ gpu_ll_source.last_cycle=psx_cycle_count;
+ gpu_ll_source.next_cycle=psx_cycle_count+128u-(psx_cycle_count&127u);
+ source_gpu_ll_words();
+ psx_devices_service_to_now();
+}
+static void advance_source_gpu_ll(void) {
+ while(gpu_ll_source.active && psx_cycle_count>=gpu_ll_source.next_cycle) {
+ gpu_ll_source.budget+=(int32_t)(gpu_ll_source.next_cycle-gpu_ll_source.last_cycle);
+ gpu_ll_source.last_cycle=gpu_ll_source.next_cycle;
+ gpu_ll_source.next_cycle+=128u;source_gpu_ll_words();
+ }
+}
+static void advance_source_cdrom(void);
+static void source_cdrom_words(void);
+static void advance_source_otc(void);
+static void advance_source_otc_words(uint32_t budget);
+static void advance_source_spu(void);
+static void source_spu_words(void);
+void dma_source_gpu_service_at(uint64_t cycle) {
+ if(cycle!=psx_cycle_count) {
+ fprintf(stderr,"[dma-model] source GPU service outside scheduler cycle\n");exit(2);
+ }
+ g_dma_exec_depth++;
+ advance_source_gpu();
+ if(gpu_upload_source.remaining && cycle>gpu_upload_source.last_cycle) {
+ gpu_upload_source.budget+=(int32_t)(cycle-gpu_upload_source.last_cycle);
+ gpu_upload_source.last_cycle=cycle;source_gpu_upload_words();
+ }
+ advance_source_gpu_ll();
+ if(gpu_ll_source.active && cycle>gpu_ll_source.last_cycle) {
+ gpu_ll_source.budget+=(int32_t)(cycle-gpu_ll_source.last_cycle);
+ gpu_ll_source.last_cycle=cycle;source_gpu_ll_words();
+ }
+ /* Original DMA_Update services every channel on register writes and
+ * frontend returns, including the fraction since the last 128-clock
+ * event. Preserve that global event phase and charge each interval once. */
+ advance_source_cdrom();
+ if(cd_source_model && cdrom_async.active && cycle>cd_source.last_cycle) {
+ cd_source.budget+=(int32_t)(cycle-cd_source.last_cycle);
+ cd_source.last_cycle=cycle;source_cdrom_words();
+ }
+ advance_source_spu();
+ if(spu_source.remaining && cycle>spu_source.last_cycle) {
+ spu_source.budget+=(int32_t)(cycle-spu_source.last_cycle);
+ spu_source.last_cycle=cycle;source_spu_words();
+ }
+ advance_source_otc();
+ if(otc_source_model && otc_source.remaining && cycle>otc_source.last_cycle) {
+ uint32_t elapsed=(uint32_t)(cycle-otc_source.last_cycle);
+ otc_source.last_cycle=cycle;advance_source_otc_words(elapsed);
+ }
+ g_dma_exec_depth--;
+}
+uint32_t dma_cpu_read_penalty(void) {
+ return g_dma_cpu_read_wait;
+}
+void dma_cpu_read_wait_boundary(void) {
+ /* Source RunReal does not service an event crossed by fetch/base cycles
+ * until the instruction's memory access (MMIO) or retirement (RAM/ROM).
+ * Its ReadMemory therefore sees the DMA steal from instruction entry.
+ * Our scheduler may already have completed DMA during the fetch refill;
+ * preserve that entry wait, including for compiled RAM fast-path routing. */
+ cpu_read_wait_latched=1;
+ g_dma_cpu_read_wait=gpu_upload_live_read_wait;
+}
+
static uint32_t execute_ch2_gpu(void) {
uint32_t chcr = channels[2].chcr;
uint32_t direction = chcr & 1; /* 0=to RAM, 1=from RAM (to device) */
@@ -822,16 +1053,74 @@ static uint32_t execute_ch2_gpu(void) {
return actual_words;
}
+static void source_cdrom_words(void) {
+ DMAAsyncChannel *a=&cdrom_async;
+ uint32_t addr=channels[3].madr&0x1FFFFCu;
+ int32_t step=(channels[3].chcr&2u)?-4:4;
+ if(a->remaining_words==a->total_words) {
+ uint32_t bytes=a->total_words*4u;
+ if(bytes>0x200000u-addr)bytes=0x200000u-addr;
+ if(addr<0x1C0000u)overlay_capture_before_dma(addr,bytes);
+ }
+ while(a->active && a->remaining_words && cd_source.budget>0) {
+ uint32_t word=cdrom_dma_read_padded();
+ g_dma_cur_ch=3;g_dma_cur_madr=addr;g_dma_cur_bcr=channels[3].bcr;
+ g_dma_initiator_pc=s_dma_ch_initiator_pc[3];
+ psx_write_word(addr,word);record_cdrom_dma_word(word);
+ dirty_ram_mark_executable_range(addr,4);
+ addr=(addr+step)&0x1FFFFCu;
+ a->remaining_words--;cd_source.budget-=9;
+ channels[3].madr=addr;
+ if(!a->remaining_words)finish_async_cdrom_transfer(addr);
+ }
+}
+
+static void start_source_cdrom(void) {
+ cd_source.budget=64;cd_source.last_cycle=psx_cycle_count;
+ cd_source.next_cycle=psx_cycle_count+128u-(psx_cycle_count&127u);
+ source_cdrom_words();
+}
+
+static void advance_source_cdrom(void) {
+ while(cd_source_model && cdrom_async.active && psx_cycle_count>=cd_source.next_cycle) {
+ cd_source.budget+=(int32_t)(cd_source.next_cycle-cd_source.last_cycle);
+ cd_source.last_cycle=cd_source.next_cycle;cd_source.next_cycle+=128u;
+ source_cdrom_words();
+ }
+}
+
static void execute_ch3_cdrom(void) {
uint32_t chcr = channels[3].chcr;
uint32_t direction = chcr & 1; /* 0=to RAM, 1=from RAM */
+ if(cd_source_model && ((chcr&0x601u)!=0 || (channels[3].madr&0x800000u))) {
+ fprintf(stderr,"[dma-model] source CD timing requires valid manual to-RAM transfer: frame=%llu cycle=%llu CHCR=%08X MADR=%08X BCR=%08X\n",
+ (unsigned long long)s_frame_count,(unsigned long long)psx_cycle_count,
+ chcr,channels[3].madr,channels[3].bcr);
+ exit(2);
+ }
if (direction != 0) {
channels[3].chcr &= ~((1u << 24) | (1u << 28));
return;
}
start_async_cdrom_transfer();
+ if(cd_source_model) {
+ start_source_cdrom();
+ /* Source's CPU owns fetch attempts during a manual DMA halt, just as
+ * for OTC. Let the CHCR store retire before that boundary runs.
+ * Standalone controller callers retain their synchronous contract. */
+ if(source_gpu_runtime_active())return;
+ while(cdrom_async.active && !(chcr&0x100u)) {
+ uint32_t wait=cd_source.next_cycle>psx_cycle_count?
+ (uint32_t)(cd_source.next_cycle-psx_cycle_count):1u;
+ psx_advance_cycles(wait);
+ /* The MMIO pre-barrier can cache a later device deadline before
+ * this transfer is armed. Service each new DMA boundary even when
+ * that cached deadline has not expired, as the OTC halt does. */
+ psx_devices_service_to_now();
+ }
+ }
}
/* Returns the number of words moved so the caller can schedule a faithful
@@ -875,7 +1164,112 @@ static uint32_t execute_ch4_spu(void) {
return total_words;
}
+static void source_spu_words(void) {
+ g_dma_cur_ch=4;g_dma_initiator_pc=s_dma_ch_initiator_pc[4];
+ while(spu_source.remaining && spu_source.budget>0) {
+ if(!spu_source.in_block) {
+ spu_source.in_block=spu_source.block_size;
+ spu_source.address=channels[4].madr&0xffffffu;
+ channels[4].bcr-=1u<<16;
+ }
+ if(spu_source.address&0x800000u) {
+ fprintf(stderr,"[dma-model] source SPU request payload address unsupported\n");exit(2);
+ }
+ uint32_t at=spu_source.address&0x1ffffcu;
+ g_dma_cur_madr=spu_source.address;g_dma_cur_bcr=channels[4].bcr;
+ if(channels[4].chcr&1u)spu_dma_write(psx_read_word(at));
+ else psx_write_word(at,spu_dma_read());
+ spu_source.address=(spu_source.address+((channels[4].chcr&2u)?-4u:4u))&0xffffffu;
+ spu_source.budget-=48;spu_source.in_block--;spu_source.remaining--;
+ if(!spu_source.in_block)channels[4].madr=spu_source.address;
+ if(!spu_source.remaining) {
+ audio_trace_event((channels[4].chcr&1u)?AUDIO_EV_DMA_WRITE:AUDIO_EV_DMA_READ,
+ spu_source.total_words,spu_source.start_addr);
+ complete_transfer(4);
+ }
+ }
+ if(spu_source.budget>0)spu_source.budget=0;
+}
+static void start_source_spu(void) {
+ uint32_t bs=channels[4].bcr&0xffffu,bc=channels[4].bcr>>16;
+ if((channels[4].chcr&0x00fffffcu)!=0x200u || !bs || !bc ||
+ (channels[4].madr&0x800000u)) {
+ fprintf(stderr,"[dma-model] source SPU request unsupported CHCR=%08X BCR=%08X MADR=%08X\n",
+ channels[4].chcr,channels[4].bcr,channels[4].madr);exit(2);
+ }
+ spu_source.remaining=spu_source.total_words=bs*bc;
+ spu_source.block_size=bs;spu_source.in_block=0;spu_source.budget=64;
+ spu_source.start_addr=channels[4].madr&0x1ffffcu;
+ spu_source.last_cycle=psx_cycle_count;
+ spu_source.next_cycle=psx_cycle_count+128u-(psx_cycle_count&127u);
+ source_spu_words();
+}
+static void advance_source_spu(void) {
+ while(spu_source.remaining && psx_cycle_count>=spu_source.next_cycle) {
+ spu_source.budget+=(int32_t)(spu_source.next_cycle-spu_source.last_cycle);
+ spu_source.last_cycle=spu_source.next_cycle;spu_source.next_cycle+=128;
+ source_spu_words();
+ }
+}
+
+static void advance_source_otc_words(uint32_t budget) {
+ while (budget-- && otc_source.remaining) {
+ if (otc_source.address & 0x800000u) {
+ otc_source.remaining = 0;
+ channels[6].chcr &= ~((1u << 24) | (1u << 28));
+ uint32_t before = dicr;
+ dicr |= 0x8000u;
+ raise_dma_irq_on_master_edge(before);
+ return;
+ }
+ uint32_t value = otc_source.remaining == 1u ? 0xFFFFFFu :
+ (otc_source.address - 4u) & 0xFFFFFFu;
+ g_dma_cur_ch = 6; g_dma_cur_madr = otc_source.address;
+ g_dma_cur_bcr = channels[6].bcr;
+ g_dma_initiator_pc = s_dma_ch_initiator_pc[6];
+ psx_write_word(otc_source.address & 0x1FFFFCu, value);
+ otc_source.address = (otc_source.address - 4u) & 0xFFFFFFu;
+ if (--otc_source.remaining == 0) complete_transfer(6);
+ }
+}
+
+static void start_source_otc(void) {
+ otc_source.remaining = channels[6].bcr & 0xFFFFu;
+ if (!otc_source.remaining) otc_source.remaining = 65536u;
+ otc_source.address = channels[6].madr & 0xFFFFFCu;
+ otc_source.last_cycle = psx_cycle_count;
+ otc_source.next_cycle = psx_cycle_count + 128u - (psx_cycle_count & 127u);
+ advance_source_otc_words(64u);
+}
+
+static void advance_source_otc(void) {
+ while (otc_source.remaining && psx_cycle_count >= otc_source.next_cycle) {
+ uint32_t elapsed = (uint32_t)(otc_source.next_cycle - otc_source.last_cycle);
+ otc_source.last_cycle = otc_source.next_cycle;
+ otc_source.next_cycle += 128u;
+ advance_source_otc_words(elapsed);
+ }
+}
+
static void execute_ch6_otc(void) {
+ if (otc_source_model) {
+ start_source_otc();
+ /* The source CPU fetches while halted. Its functional instruction
+ * boundary owns that overlap when the source CPU/GPU profile is on.
+ * Standalone controller callers retain the synchronous contract. */
+ if(source_gpu_runtime_active())return;
+ /* The ordinary CHCR store does not return to guest execution until
+ * the source-style halt ends. All devices keep advancing. Source's
+ * next instruction fetch may overlap this halt; this adapter does
+ * not claim fetch/cache phase or physical write-queue equivalence. */
+ while (otc_source.remaining) {
+ uint32_t wait = otc_source.next_cycle > psx_cycle_count ?
+ (uint32_t)(otc_source.next_cycle - psx_cycle_count) : 1u;
+ psx_advance_cycles(wait);
+ psx_devices_service_to_now();
+ }
+ return;
+ }
/* OTC (Ordering Table Clear): writes a backward-linked list to RAM.
* Node N = address of node N-1, node 0 = 0xFFFFFF (end marker).
* Direction is always to-RAM, step is always backward.
@@ -900,6 +1294,16 @@ static void execute_ch6_otc(void) {
complete_transfer(6);
}
+int dma_cpu_otc_halted(void) {
+ return otc_source_model && otc_source.remaining!=0u;
+}
+
+int dma_cpu_source_halted(void) {
+ return dma_cpu_otc_halted() ||
+ (cd_source_model && cdrom_async.active &&
+ !(channels[3].chcr&0x100u));
+}
+
static void execute_ch5_pio(void) {
/* PIO (Parallel I/O) — used for expansion port / parallel port transfers.
* Very simple: just move words directly to/from RAM with no device interaction.
@@ -937,7 +1341,11 @@ static void try_execute(int ch) {
if (!((chcr >> 24) & 1)) return;
if (!channel_enabled(ch)) return;
- channels[ch].chcr &= ~(1u << 28);
+ /* Source CD and SPU retain the trigger in their live registers until
+ * completion, observable while the CPU is allowed to keep running.
+ * Default and standalone controller callers retain their old image. */
+ if(!((ch==4 || (ch==3 && cd_source_model)) && source_gpu_runtime_active()))
+ channels[ch].chcr &= ~(1u << 28);
trace_dma('S', ch, transfer_word_count(ch), dicr, i_stat);
event_ring_record_aux(EV_DMA_KICK, (uint8_t)ch, channels[ch].chcr);
event_ring_record_aux(EV_ENQ, (uint8_t)(SRC_DMA0 + ch), transfer_word_count(ch));
@@ -959,15 +1367,20 @@ static void try_execute(int ch) {
start_async_mdec_transfer(1);
break;
case 2:
- schedule_delayed_complete(2, execute_ch2_gpu(),
- DMA_GPU_CYCLES_PER_WORD);
+ if(gpu_upload_source_model && ((channels[2].chcr>>9)&3u)==1u)
+ start_source_gpu_upload();
+ else if(gpu_ll_source_model && ((channels[2].chcr>>9)&3u)==2u)
+ start_source_gpu_ll();
+ else schedule_delayed_complete(2, execute_ch2_gpu(),
+ DMA_GPU_CYCLES_PER_WORD);
break;
case 3:
execute_ch3_cdrom();
break;
case 4:
- schedule_delayed_complete(4, execute_ch4_spu(),
- DMA_SPU_CYCLES_PER_WORD);
+ if(source_gpu_runtime_active())start_source_spu();
+ else schedule_delayed_complete(4, execute_ch4_spu(),
+ DMA_SPU_CYCLES_PER_WORD);
break;
case 5:
execute_ch5_pio();
@@ -1005,9 +1418,31 @@ int dma_cdrom_transfer_active(void) {
* already accumulated (always <= the true remaining, since per-word cost >= 1),
* and the exact countdown for delayed-complete channels. Over-slicing on a
* channel whose DICR completion is masked is safe. See PRECISE_IRQ_SLICE.md. */
+static uint32_t source_gpu_dma_irq_bound(void) {
+ uint32_t best=0xFFFFFFFFu;
+ if(gpu_ll_source.active)
+ best=gpu_ll_source.next_cycle>psx_cycle_count?
+ (uint32_t)(gpu_ll_source.next_cycle-psx_cycle_count):1u;
+ if(gpu_upload_source.remaining) {
+ uint32_t d=gpu_upload_source.next_cycle>psx_cycle_count?
+ (uint32_t)(gpu_upload_source.next_cycle-psx_cycle_count):1u;
+ if(dpsx_cycle_count?
+ (uint32_t)(spu_source.next_cycle-psx_cycle_count):1u;
+ if(dpsx_cycle_count?
+ (uint32_t)(gpu_ll_source.next_cycle-psx_cycle_count):1u;
+ }
+ if(gpu_upload_source.remaining) {
+ best=gpu_upload_source.next_cycle>psx_cycle_count?
+ (uint32_t)(gpu_upload_source.next_cycle-psx_cycle_count):1u;
+ }
+ if (otc_source.remaining) {
+ uint32_t d = otc_source.next_cycle > psx_cycle_count ?
+ (uint32_t)(otc_source.next_cycle - psx_cycle_count) : 1u;
+ if(dpsx_cycle_count?
+ (uint32_t)(cd_source.next_cycle-psx_cycle_count):1u;
+ if(dpsx_cycle_count?
+ (uint32_t)(spu_source.next_cycle-psx_cycle_count):1u;
+ if(d> 24) & 1u) && channel_enabled(3)) {
if ((channels[3].chcr & 1u) != 0) {
return 1u; /* unsupported RAM->CD direction cancels next tick */
@@ -1073,7 +1531,12 @@ uint32_t dma_cycles_to_internal_event(void) {
uint32_t dma_cycles_to_deliverable_irq(uint32_t i_mask) {
if (!(i_mask & (1u << 3))) return 0xFFFFFFFFu;
- uint32_t best = 0xFFFFFFFFu;
+ uint32_t best = channel_irq_flag_armed(2)?source_gpu_dma_irq_bound():0xFFFFFFFFu;
+ if(spu_source.remaining && channel_irq_flag_armed(4)) {
+ uint32_t d=spu_source.next_cycle>psx_cycle_count?
+ (uint32_t)(spu_source.next_cycle-psx_cycle_count):1u;
+ if(d> 1) & 1u;
@@ -1161,11 +1629,38 @@ void dma_advance(uint32_t cycles) {
}
void dma_init(void) {
+ g_dma_cpu_read_wait=0;
+ gpu_upload_live_read_wait=0;cpu_read_wait_latched=0;
+ memset(&gpu_ll_source,0,sizeof(gpu_ll_source));
+ memset(&gpu_upload_source,0,sizeof(gpu_upload_source));
+ memset(&spu_source,0,sizeof(spu_source));
+ const char *gpu_model=getenv("PSX_GPU_DMA_MODEL");
+ gpu_upload_source_model=gpu_model && *gpu_model;
+ gpu_ll_source_model=gpu_model && (!strcmp(gpu_model,"octoshock-2.2.2-bounded-linked-list") || !strcmp(gpu_model,"octoshock-2.2.2-bounded-quad"));
+ if(gpu_upload_source_model && ((!gpu_ll_source_model && strcmp(gpu_model,"octoshock-2.2.2-vram-upload")) || !getenv("PSX_INPUT_ROUTE_FILE"))) {
+ fprintf(stderr,"[dma-model] invalid source GPU upload model or missing route\n");exit(2);
+ }
+ memset(&otc_source, 0, sizeof(otc_source));
+ memset(&cd_source,0,sizeof(cd_source));
+ const char *cd_model=getenv("PSX_CD_DMA_MODEL");
+ cd_source_model=cd_model && *cd_model;
+ if(cd_source_model && (strcmp(cd_model,"octoshock-2.2.2") || !getenv("PSX_INPUT_ROUTE_FILE"))) {
+ fprintf(stderr,"[dma-model] invalid source CD model or missing route\n");exit(2);
+ }
+ const char *model = getenv("PSX_INPUT_ROUTE_DMA_MODEL");
+ otc_source_model = model && *model;
+ if (otc_source_model &&
+ (strcmp(model, "octoshock-2.2.2-otc") || !getenv("PSX_INPUT_ROUTE_FILE"))) {
+ fprintf(stderr, "[dma-model] invalid source OTC model or missing input route\n");
+ exit(2);
+ }
memset(channels, 0, sizeof(channels));
memset(mdec_async, 0, sizeof(mdec_async));
memset(&cdrom_async, 0, sizeof(cdrom_async));
memset(delayed_complete, 0, sizeof(delayed_complete));
- dpcr = 0x07654321u; /* default: priorities set, no channels enabled */
+ /* Original2.2.2 Power clears DMAControl. Keep that source-profile cold
+ * value distinct from the default hardware-style priority reset image. */
+ dpcr = gpu_ll_source_model ? 0u : 0x07654321u;
dicr = 0;
dma_debug_clear_trace();
dma_debug_clear_cdrom_history();
@@ -1208,6 +1703,55 @@ uint32_t dma_read(uint32_t addr) {
}
void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask) {
+ source_gpu_runtime_dma_write();
+ if(spu_source.remaining &&
+ ((addr>=0x1f8010c0u && addr<=0x1f8010cbu) ||
+ (addr==0x1f8010f0u && ((dpcr^val)&mask&0xf0000u)))) {
+ fprintf(stderr,"[dma-model] active source SPU request register replacement unsupported\n");exit(2);
+ }
+ if(gpu_ll_source.active) {
+ advance_source_gpu_ll();
+ if(gpu_ll_source.active && psx_cycle_count>gpu_ll_source.last_cycle) {
+ gpu_ll_source.budget+=(int32_t)(psx_cycle_count-gpu_ll_source.last_cycle);
+ gpu_ll_source.last_cycle=psx_cycle_count;source_gpu_ll_words();
+ }
+ /* Source DMA_Write services elapsed work first. With no payload left,
+ * clearing start stops before the next header: no GPU FIFO flush, no
+ * completion IRQ, and no retained positive DMA credit. Other active
+ * register changes and partial-payload cancellation remain guarded. */
+ if(gpu_ll_source.active && !gpu_ll_source.remaining &&
+ addr==0x1f8010a8u && mask==0xffffffffu &&
+ channels[2].chcr==0x01000401u && val==0x00000401u) {
+ gpu_ll_source.active=0;
+ gpu_ll_source.budget=0;
+ }
+ if(gpu_ll_source.active &&
+ ((addr>=0x1f8010a0u && addr<=0x1f8010abu) ||
+ (addr==0x1f8010f0u && ((dpcr^val)&mask&0xf00u)))) {
+ SourceGPUCommandProjection state;source_gpu_runtime_copy(0,&state);
+ fprintf(stderr,"[dma-model] active source linked-list register replacement unsupported at %llu addr=%08X value=%08X mask=%08X pc=%08X MADR=%08X BCR=%08X CHCR=%08X payload=%08X remaining=%u nodes=%u budget=%d last=%llu next=%llu GPU=%d,%u,%u,%u,%08X\n",
+ (unsigned long long)psx_cycle_count,addr,val,mask,g_debug_last_store_pc,
+ channels[2].madr,channels[2].bcr,channels[2].chcr,gpu_ll_source.address,
+ gpu_ll_source.remaining,gpu_ll_source.nodes,gpu_ll_source.budget,
+ (unsigned long long)gpu_ll_source.last_cycle,(unsigned long long)gpu_ll_source.next_cycle,
+ state.budget,state.phase,state.count,state.command,state.count?state.queue[0]:0);
+ exit(2);
+ }
+ }
+ if(gpu_upload_source.remaining) {
+ /* The source synchronizes elapsed DMA work before changing any DMA
+ * register. In particular, completion at this timestamp must see the
+ * previous DICR enables. Reads do not perform this partial service. */
+ advance_source_gpu();
+ if(gpu_upload_source.remaining && psx_cycle_count>gpu_upload_source.last_cycle) {
+ gpu_upload_source.budget+=(int32_t)(psx_cycle_count-gpu_upload_source.last_cycle);
+ gpu_upload_source.last_cycle=psx_cycle_count;
+ source_gpu_upload_words();
+ }
+ }
+ if(gpu_upload_source.remaining && addr==0x1f8010f0u && ((dpcr^val)&mask&0xf00u)) {
+ fprintf(stderr,"[dma-model] active source GPU channel control change unsupported\n");exit(2);
+ }
/* DPCR */
if (addr == 0x1F8010F0u) {
dpcr = (dpcr & ~mask) | (val & mask);
@@ -1233,6 +1777,9 @@ void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask) {
return;
}
+ if(gpu_upload_source.remaining && addr>=0x1f8010a0u && addr<=0x1f8010abu) {
+ fprintf(stderr,"[dma-model] source GPU upload register replacement unsupported\n");exit(2);
+ }
/* Per-channel registers */
if (addr >= 0x1F801080u && addr <= 0x1F8010EFu) {
uint32_t offset = addr - 0x1F801080u;
@@ -1352,6 +1899,22 @@ static int dma_r_delay(PstR *r, DMADelayedComplete *d) {
uint32_t dma_snapshot_bytes(void) { return DMA_SNAP_WIRE_BYTES; }
void dma_snapshot_write(uint8_t *p) {
+ if(spu_source.remaining) {
+ fprintf(stderr,"[dma-model] active source SPU request capture unsupported\n");exit(2);
+ }
+ if(gpu_ll_source.active) {
+ fprintf(stderr,"[dma-model] active source linked-list capture unsupported\n");exit(2);
+ }
+ if(gpu_upload_source.remaining) {
+ fprintf(stderr,"[dma-model] active source GPU upload capture unsupported\n");exit(2);
+ }
+ if(cd_source_model && cdrom_async.active) {
+ fprintf(stderr,"[dma-model] state capture during source CD transfer is unsupported\n");exit(2);
+ }
+ if (otc_source.remaining) {
+ fprintf(stderr, "[dma-model] state capture during experimental OTC transfer is unsupported\n");
+ exit(2);
+ }
PstW w;
pst_w_init(&w, p, DMA_SNAP_WIRE_BYTES);
for (int i = 0; i < 7; i++) {
@@ -1369,6 +1932,16 @@ void dma_snapshot_write(uint8_t *p) {
}
int dma_snapshot_read(const uint8_t *p, uint32_t len) {
+ if(gpu_upload_source_model) {
+ fprintf(stderr,"[dma-model] source GPU upload timing requires cold boot\n");return 0;
+ }
+ if(cd_source_model) {
+ fprintf(stderr,"[dma-model] source CD timing requires cold boot\n");return 0;
+ }
+ if (otc_source_model) {
+ fprintf(stderr, "[dma-model] experimental source OTC model requires cold boot\n");
+ return 0;
+ }
PstR r;
if (len != DMA_SNAP_WIRE_BYTES) return 0;
pst_r_init(&r, p, len);
diff --git a/runtime/src/event_ring.c b/runtime/src/event_ring.c
index 2fb1c3cec..86b868ed0 100644
--- a/runtime/src/event_ring.c
+++ b/runtime/src/event_ring.c
@@ -94,8 +94,7 @@ static int format_entry(char *out, int cap, const EventEntry *e) {
(unsigned)e->detail, e->aux);
}
-int event_ring_dump_file(const char *path) {
- FILE *f = fopen(path ? path : "event_ring.json", "w");
+int event_ring_dump_stream(FILE *f) {
if (!f) return -1;
uint64_t total = s_seq;
uint64_t start = (total > EVENT_RING_CAP) ? (total - EVENT_RING_CAP) : 0;
@@ -109,7 +108,14 @@ int event_ring_dump_file(const char *path) {
first = 0; count++;
}
fputs("]\n", f);
- fclose(f);
+ return ferror(f) ? -1 : count;
+}
+
+int event_ring_dump_file(const char *path) {
+ FILE *f = fopen(path ? path : "event_ring.json", "w");
+ if (!f) return -1;
+ int count = event_ring_dump_stream(f);
+ if (fclose(f)) return -1;
return count;
}
diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c
index 28026b81e..333a060e4 100644
--- a/runtime/src/gpu.c
+++ b/runtime/src/gpu.c
@@ -11,6 +11,8 @@
*/
#include "gpu.h"
+#include "source_gpu_runtime.h"
+#include "interrupts.h"
#include "pgxp.h"
#include "mod_memory.h"
#include "gpu_primitive_reject.h"
@@ -33,6 +35,8 @@
#include
#include
+static int gpu_source_dispatch(const SourceGPUCommandDispatch *);
+
extern uint16_t psx_read_half(uint32_t addr);
extern uint8_t psx_read_byte(uint32_t addr);
extern uint32_t psx_read_word(uint32_t addr);
@@ -2278,12 +2282,17 @@ static int32_t polyline_prev_x, polyline_prev_y; /* previous vertex */
static uint16_t polyline_prev_c; /* shaded polyline: previous color */
static int polyline_semi_trans; /* semi-transparency flag from command word */
static int polyline_has_prev; /* have we seen at least one vertex? */
+/* Read-only admission metadata for the bounded source DMA experiment. It
+ * never changes parser behavior. Source restore is excluded by dma.c. */
+static unsigned source_ll_polyline_vertices;
+static int source_ll_incomplete_terminator;
/* VRAM write transfer state (CPU→VRAM, command 0xA0) */
static uint16_t vram_write_x, vram_write_y; /* start coords */
static uint16_t vram_write_w, vram_write_h; /* dimensions */
static uint16_t vram_write_col, vram_write_row; /* current offset */
static uint32_t vram_write_remaining; /* words remaining */
+uint32_t gpu_dma_vram_upload_words(void) { return vram_write_remaining; }
/* Stage one complete GP0(A0) transfer so renderer backends receive one bulk
* rectangle instead of hundreds of thousands of single-pixel callbacks. The
* CPU-visible transfer remains ordered because GP0 accepts no next command
@@ -2314,6 +2323,14 @@ static void gp0_commit_cpu_to_vram(void) {
/* VRAM read transfer state (VRAM→CPU, command 0xC0) */
static int vram_read_active;
+int gpu_dma_source_ll_ready(void) {
+ int projected=source_gpu_runtime_ready();
+ if(projected!=-2)return projected;
+ if(source_ll_incomplete_terminator || vram_read_active)return -1;
+ if(gp0_state==GP0_IDLE)return 1;
+ if(gp0_state==GP0_POLYLINE_MONO && source_ll_polyline_vertices>=2)return 0;
+ return -1;
+}
static uint16_t vram_read_x, vram_read_y;
static uint16_t vram_read_w, vram_read_h;
static uint16_t vram_read_col, vram_read_row;
@@ -2629,6 +2646,7 @@ static void gpu_reset_state(int clear_vram) {
polyline_prev_c = 0;
polyline_semi_trans = 0;
polyline_has_prev = 0;
+ source_ll_polyline_vertices=0;source_ll_incomplete_terminator=0;
vram_write_x = vram_write_y = 0;
vram_write_w = vram_write_h = 0;
vram_write_col = vram_write_row = 0;
@@ -2695,6 +2713,8 @@ static void gpu_reset_state(int clear_vram) {
void gpu_init(void) {
gpu_reset_state(1);
+ source_gpu_runtime_set_dispatch_sink(gpu_source_dispatch);
+ gr_source_texture_control(0,0);
}
/* ---- GPUSTAT read (0x1F801814) ---- */
@@ -2790,6 +2810,15 @@ uint32_t gpu_read_gpustat(void) {
/* Bit 31: LCF — drawing even/odd lines in interlace mode */
stat |= (lcf & 1) << 31;
+ uint32_t raster_bits;
+ if (interrupts_raster_gpu_status(&raster_bits))
+ stat=(stat&~0x80002000u)|raster_bits;
+
+ /* Source-profile busy and feedback bits describe the same queue that
+ * owns command effects. A status read observes it without servicing it. */
+ if(source_gpu_runtime_active())
+ stat=(stat&~0x1e000000u)|source_gpu_runtime_status_bits();
+
return stat;
}
@@ -2810,12 +2839,17 @@ uint32_t gpu_read_gpuread(void) {
value |= (uint32_t)gr_vram_read((int)rx, (int)ry) << (i * 16);
if (++vram_read_col == vram_read_w) {
- vram_read_col = 0;
- if (++vram_read_row == vram_read_h) {
+ if(source_gpu_runtime_active()) {
+ /* Original source reads both halves of the final word. For
+ * odd extents its extra half uses the next X on the final row. */
+ if(vram_read_row+1==vram_read_h)vram_read_active=0;
+ else {vram_read_row++;vram_read_col=0;}
+ } else if (++vram_read_row == vram_read_h) {
/* Transfer complete */
+ vram_read_col = 0;
vram_read_active = 0;
break;
- }
+ } else vram_read_col=0;
}
}
@@ -2828,7 +2862,8 @@ uint32_t gpu_read_gpuread(void) {
c0_capture_slot_fwd = -1; /* transfer complete */
}
- gpuread_latch = value;
+ if(source_gpu_runtime_active())source_gpu_runtime_read();
+ else gpuread_latch = value;
return value;
}
@@ -3197,6 +3232,18 @@ int gpu_video_standard_is_pal(void) {
return (int)(video_mode & 1u);
}
+int gpu_display_is_interlaced(void) {
+ return (int)(vertical_interlace & 1u);
+}
+
+/* Passive private observer: no GPUSTAT read, polling side effect or time pump. */
+void gpu_observer_video_state(uint32_t *out) {
+ out[0]=(hres1&3u)|((vres&1u)<<2)|((video_mode&1u)<<3)|
+ ((display_depth&1u)<<4)|((vertical_interlace&1u)<<5)|((hres2&1u)<<6);
+ out[1]=v_display_y1; out[2]=v_display_y2;
+ out[3]=(uint32_t)interlace_field; out[4]=(uint32_t)lcf;
+}
+
void gpu_get_display_info(GpuDisplayInfo* out) {
out->display_x = display_area_x;
out->display_y = display_area_y;
@@ -4494,6 +4541,10 @@ static void gp0_exec_vram_to_cpu(void) {
uint32_t h = (gp0_cmd_buf[2] >> 16) & 0x1FFu;
vram_read_w = (w == 0) ? 0x400 : (uint16_t)w;
vram_read_h = (h == 0) ? 0x200 : (uint16_t)h;
+ if(source_gpu_runtime_active()) {
+ uint32_t raw_height=(gp0_cmd_buf[2]>>16)&1023u;
+ vram_read_h=(uint16_t)(raw_height>512 ? raw_height&511u : raw_height);
+ }
/* Record for debug */
if (c0_history_count < C0_HISTORY_CAP) {
@@ -4512,7 +4563,7 @@ static void gp0_exec_vram_to_cpu(void) {
vram_read_col = 0;
vram_read_row = 0;
- vram_read_active = 1;
+ vram_read_active = vram_read_h != 0;
}
/* Determine how many words a GP0 command requires (header only, not counting
@@ -5292,7 +5343,6 @@ uint16_t gpu_vram_peek(int x, int y) {
}
static void gpu_write_gp0_body(uint32_t val) {
- gp0_write_count++;
/* State: consuming pixel data for CPU→VRAM transfer */
if (gp0_state == GP0_VRAM_WRITE) {
@@ -5345,6 +5395,7 @@ static void gpu_write_gp0_body(uint32_t val) {
/* State: mono polyline — each word is a vertex (or terminator) */
if (gp0_state == GP0_POLYLINE_MONO) {
if ((val & 0xF000F000u) == 0x50005000u) {
+ if(source_ll_polyline_vertices<2)source_ll_incomplete_terminator=1;
/* Terminator: hardware ends a polyline ONLY when the masked word
* matches 0x50005000 (the 0x55555555 terminator) — Beetle
* gpu.cpp:1030, psx-spx. The old `(val & 0xF000F000) != 0` test
@@ -5359,6 +5410,7 @@ static void gpu_write_gp0_body(uint32_t val) {
return;
}
int32_t x, y;
+ if(source_ll_polyline_vertices<2)source_ll_polyline_vertices++;
parse_vertex(val, &x, &y);
x += draw_offset_x; y += draw_offset_y;
if (polyline_has_prev &&
@@ -5448,6 +5500,7 @@ static void gpu_write_gp0_body(uint32_t val) {
polyline_has_prev = 0;
gr_set_semi_transparency(polyline_semi_trans, (int)semi_transparency);
gp0_state = shaded ? GP0_POLYLINE_SHADED : GP0_POLYLINE_MONO;
+ source_ll_polyline_vertices=0;
gp0_draw_count++;
/* Record polyline header (variable-length body not captured;
* just enough so per-frame stream shows the polyline existed). */
@@ -5475,7 +5528,95 @@ static void gpu_write_gp0_body(uint32_t val) {
* rasterization / batching / VRAM transfer work on the emu thread — as its
* own phase so it is separable from the guest code that issued the write.
* Covers both the MMIO store chokepoint and DMA channel-2 feeds. */
+static int gpu_source_draw_triangle(const uint32_t *words,int second) {
+ unsigned opcode=words[0]>>24,stride=source_gpu_polygon_stride(opcode);
+ int x[3],y[3],extra_work=0;uint32_t colors[3];SourceGPUTexture texture={0};
+ SourceGPUCommandProjection state;source_gpu_runtime_copy(0,&state);
+ if((opcode&4) && !second) {
+ uint32_t page=words[4+!!(opcode&0x10)]>>16;
+ gr_source_texture_control(3,page);
+ set_tpage_from_poly((uint16_t)page);
+ }
+ texture.page=current_texpage();texture.window=texture_window_value;
+ texture.clut=words[2]>>16;texture.raw=!!(opcode&1);texture.load_clut=!second;
+ for(unsigned i=0;i<3;i++) {
+ unsigned v=i+(second?1:0);
+ x[i]=source_gpu_command_coord(words[1+stride*v],0)+draw_offset_x;
+ y[i]=source_gpu_command_coord(words[1+stride*v],16)+draw_offset_y;
+ colors[i]=words[(opcode&0x10)?stride*v:0]&0xffffffu;
+ if(opcode&4)texture.uv[i]=words[2+stride*v]&0xffff;
+ }
+ gr_set_semi_transparency(!!(opcode&2),(int)semi_transparency);
+ if(!gr_draw_source_triangle(x,y,colors,!!(opcode&0x10),(int)dither_enabled,
+ (state.display_mode&0x24)==0x24 && !(state.draw_mode&0x400),state.skip_field,
+ (opcode&4)?&texture:0,&extra_work))
+ psx_fatal_halt("Source GPU triangle requires native software rendering without geometry enhancement");
+ return extra_work;
+}
+
+static int gpu_source_draw_block(const uint32_t *words) {
+ unsigned opcode=words[0]>>24;int extra=0;
+ SourceGPUCommandProjection state;source_gpu_runtime_copy(0,&state);
+ SourceGPUBlock block={0};block.words=words;block.draw_mode=state.draw_mode;
+ block.texture_window=state.texture_window;
+ block.clip_left=state.clip_x0;block.clip_top=state.clip_y0;
+ block.clip_right=state.clip_x1;block.clip_bottom=state.clip_y1;
+ block.x=source_gpu_sprite_origin(words[1],0,state.offset_x);
+ block.y=source_gpu_sprite_origin(words[1],16,state.offset_y);
+ block.interlace=(state.display_mode&0x24)==0x24 && !(state.draw_mode&0x400);
+ block.skip_field=state.skip_field;
+ if(opcode==0x80)gr_source_texture_control(2,0);
+ gr_set_semi_transparency(!!(opcode&2),(int)semi_transparency);
+ if(!gr_draw_source_block(&block,&extra))psx_fatal_halt("Source GPU block requires native software rendering without enhancement");
+ return extra;
+}
+
+static int gpu_source_dispatch(const SourceGPUCommandDispatch *event) {
+ extern int g_exec_phase;
+ int previous_phase=g_exec_phase,extra_work=0;
+ g_exec_phase=4;
+ memcpy(gp0_cmd_buf,event->words,event->count*sizeof(uint32_t));
+ gp0_cmd_source_addr=gp0_next_source_addr;
+ gp0_words_collected=gp0_words_needed=(int)event->count;
+ if(event->kind==SOURCE_GPU_DISPATCH_UPLOAD_WORD) {
+ gpu_write_gp0_body(event->words[0]);
+ } else if(event->kind==SOURCE_GPU_DISPATCH_COMMAND) {
+ unsigned opcode=event->words[0]>>24;
+ if(source_gpu_block_supported(opcode)) {
+ gp0_opcode_count[opcode]++;
+ if(opcode==2)gp0_fill_count++;else if(opcode==0x80)gp0_copy_count++;else gp0_draw_count++;
+ gp0_ring_record(gp0_cmd_buf,event->count);
+ extra_work=gpu_source_draw_block(event->words);
+ } else if(source_gpu_polygon_supported(opcode)) {
+ gp0_opcode_count[opcode]++;gp0_draw_count++;
+ gp0_ring_record(gp0_cmd_buf,event->count);
+ extra_work=gpu_source_draw_triangle(event->words,0);
+ } else {
+ if(opcode==1)gr_source_texture_control(1,0);
+ if(opcode==0xa0 || opcode==0x80)gr_source_texture_control(2,0);
+ if(opcode==0xe1)gr_source_texture_control(3,event->words[0]);
+ gp0_execute_command();
+ }
+ } else if(event->kind==SOURCE_GPU_DISPATCH_QUAD_FIRST) {
+ /* Dispatch only the first three vertex groups. */
+ unsigned opcode=event->words[0]>>24;
+ gp0_opcode_count[opcode]++;gp0_draw_count++;
+ extra_work=gpu_source_draw_triangle(event->words,0);
+ } else if(event->kind==SOURCE_GPU_DISPATCH_QUAD_SECOND) {
+ /* The completed packet supplies source vertex groups (1,2,3). */
+ gp0_ring_record(gp0_cmd_buf,event->count);
+ extra_work=gpu_source_draw_triangle(event->words,1);
+ }
+ g_exec_phase=previous_phase;
+ return extra_work;
+}
+
void gpu_write_gp0(uint32_t val) {
+ gp0_write_count++;
+ if(source_gpu_runtime_active()) {
+ source_gpu_runtime_gp0(val);
+ return;
+ }
extern int g_exec_phase;
int prev_phase = g_exec_phase;
g_exec_phase = 4;
@@ -5495,6 +5636,7 @@ static void gp1_reset(void) {
static void gp1_reset_command_buffer(void) {
/* GP1(01h): Reset command buffer — clears FIFO, aborts current command */
gp0_state = GP0_IDLE;
+ source_ll_polyline_vertices=0;source_ll_incomplete_terminator=0;
gp0_words_collected = 0;
gp0_words_needed = 0;
vram_write_remaining = 0;
@@ -5620,10 +5762,14 @@ static void gp1_get_info(uint32_t val) {
}
void gpu_write_gp1(uint32_t val) {
+ source_gpu_runtime_gp1(val);
+ interrupts_raster_gp1(val);
uint32_t cmd = (val >> 24) & 0x3F;
switch (cmd) {
- case 0x00: gp1_reset(); break;
+ case 0x00: gp1_reset();
+ if(source_gpu_runtime_active())gr_source_texture_control(4,0);
+ break;
case 0x01: gp1_reset_command_buffer(); break;
case 0x02: gp1_ack_irq1(); break;
case 0x03: gp1_display_enable(val); break;
diff --git a/runtime/src/gpu_render.c b/runtime/src/gpu_render.c
index c833f491f..c37122bc7 100644
--- a/runtime/src/gpu_render.c
+++ b/runtime/src/gpu_render.c
@@ -94,6 +94,17 @@ void gr_set_backend(GrBackend backend) {
GrBackend gr_backend(void) { return g_effective; }
/* ---- Dispatch wrappers (one line each; forward to the active backend) ---- */
+int gr_draw_source_block(const SourceGPUBlock *block,int *extra_work) {
+ return g_b==&SW_BACKEND && sw_draw_source_block(block,extra_work);
+}
+void gr_source_texture_control(unsigned action,uint32_t page) {
+ if(g_b==&SW_BACKEND)sw_source_texture_control(action,page);
+}
+int gr_draw_source_triangle(const int *x,const int *y,const uint32_t *colors,
+ int shaded,int dither,int interlace,unsigned skip_field,
+ const SourceGPUTexture *texture,int *extra_work) {
+ return g_b==&SW_BACKEND && sw_draw_source_triangle(x,y,colors,shaded,dither,interlace,skip_field,texture,extra_work);
+}
void gr_init(uint16_t *vram) { g_b->init(vram); }
void gr_set_scale(int scale) { g_b->set_scale(scale); }
int gr_scale(void) { return g_b->scale(); }
diff --git a/runtime/src/gpu_sw_renderer.c b/runtime/src/gpu_sw_renderer.c
index ff40aceeb..55cf2de13 100644
--- a/runtime/src/gpu_sw_renderer.c
+++ b/runtime/src/gpu_sw_renderer.c
@@ -30,6 +30,7 @@
#include "gpu_sw_renderer.h"
#include "gpu_vram_dirty.h"
#include "gpu_sw_edges.h"
+#include "source_gpu_polygon_projection.h"
#include
#include
#include
@@ -280,7 +281,187 @@ static inline void put_opaque(const RTarget *t, int x, int y, uint16_t color) {
gpu_vram_dirty_mark_row((uint32_t)y);
}
-/* Write a textured pixel — semi-trans only if texel bit 15 is set */
+/* Native cache state for the opt-in source-comparison profile. Cache reads
+ * sample only g_vram. Uploads invalidate texture tags, but deliberately leave
+ * the palette cached until a palette-key change or GP0 cache clear. */
+static struct {
+ uint32_t tag[256],palette_key,page;
+ uint16_t line[256][4],palette[256];
+} source_texture_cache;
+void sw_source_texture_control(unsigned action,uint32_t page) {
+ int clear=action==0 || action==1 || action==2 || action==4;
+ if(action==3) {
+ unsigned old=source_texture_cache.page;
+ clear=((old^page)&31u)!=0 || (!!(old&0x180u)!=!!(page&0x180u));
+ }
+ if(action==0)memset(&source_texture_cache,0,sizeof(source_texture_cache));
+ if(action==0 || action==1 || action==4)source_texture_cache.palette_key=UINT32_MAX;
+ if(clear)for(unsigned i=0;i<256;i++)source_texture_cache.tag[i]=UINT32_MAX;
+ if(action==3 || action==4)source_texture_cache.page=page&511u;
+}
+typedef struct SourceTriangleColors {
+ RTarget target;
+ int core_x,core_y,dither,extra_work,mode;
+ uint32_t base[5],dx[5],dy[5];
+ const SourceGPUTexture *texture;
+} SourceTriangleColors;
+static uint16_t source_texture_fetch(SourceTriangleColors *c,unsigned u,unsigned v) {
+ unsigned window=c->texture->window,page=c->texture->page;
+ unsigned mx=window&31u,my=(window>>5)&31u;
+ unsigned ux=(u&~(mx<<3))+(((window>>10)&mx)<<3)+((page&15u)<<(8-c->mode));
+ unsigned vy=(v&~(my<<3))+(((window>>15)&my)<<3)+((page&16u)<<4);
+ unsigned address=vy*1024u+((ux>>(2-c->mode))&1023u);
+ unsigned entry=c->mode==0?((address>>2)&3u)|((address>>8)&252u):
+ ((address>>2)&7u)|((address>>7)&248u);
+ unsigned tag=address&~3u;
+ if(source_texture_cache.tag[entry]!=tag) {
+ c->extra_work+=4;
+ for(unsigned i=0;i<4;i++)source_texture_cache.line[entry][i]=g_vram[tag+i];
+ source_texture_cache.tag[entry]=tag;
+ }
+ uint16_t value=source_texture_cache.line[entry][address&3u];
+ if(c->mode==0)return source_texture_cache.palette[(value>>((ux&3u)*4))&15u];
+ if(c->mode==1)return source_texture_cache.palette[(value>>((ux&1u)*8))&255u];
+ return value;
+}
+static void source_texture_palette(SourceTriangleColors *c) {
+ unsigned clut=c->texture->clut;
+ uint32_t key=(clut&32767u)|((unsigned)c->mode<<16);
+ if(c->mode>=2 || !c->texture->load_clut || source_texture_cache.palette_key==key)return;
+ unsigned count=c->mode?256:16;
+ c->extra_work+=(int)count;
+ for(unsigned i=0;i>6)&511u)*1024u+(((clut&63u)*16u+i)&1023u)];
+ source_texture_cache.palette_key=key;
+}
+static unsigned source_triangle_component(const SourceTriangleColors *c,unsigned channel,int x,int y) {
+ uint32_t fraction=c->base[channel]+c->dx[channel]*(uint32_t)(x-c->core_x)+c->dy[channel]*(uint32_t)(y-c->core_y);
+ return (fraction>>12)&255u;
+}
+static void source_triangle_span(void *context,int y,int x,int width) {
+ SourceTriangleColors *c=context;
+ static const int matrix[4][4]={{-4,0,-3,1},{2,-2,3,-1},{-3,1,-4,0},{3,-1,2,-2}};
+ for(int end=x+width;xtexture) {
+ texel=source_texture_fetch(c,source_triangle_component(c,3,x,y),source_triangle_component(c,4,x,y));
+ if(!texel)continue;
+ pixel=texel&0x8000u;
+ }
+ if(c->texture && c->texture->raw)pixel=texel;
+ else for(unsigned channel=0;channel<3;channel++) {
+ int component=(int)source_triangle_component(c,channel,x,y);
+ if(c->texture)component=(((texel>>(channel*5))&31)*component)>>4;
+ if(c->dither)component+=matrix[y&3][x&3];
+ if(component<0)component=0;
+ if(component>255)component=255;
+ pixel|=(uint16_t)((component>>3)<<(5*channel));
+ }
+ /* Coverage was clipped in logical draw-area coordinates. Installed
+ * VRAM has nine Y bits; wrapping happens only at the pixel store. */
+ unsigned physical_y=(unsigned)y&511u,address=physical_y*1024u+(unsigned)x;
+ if(g_mask_check_bit && (g_vram[address]&0x8000u))continue;
+ if(g_semi_trans_enabled && (!c->texture || (texel&0x8000u))) {
+ pixel=blend_pixels(g_vram[address],pixel,g_semi_trans_mode);
+ if(c->texture)pixel|=0x8000u;
+ }
+ if(g_mask_set_bit)pixel|=0x8000u;
+ g_vram[address]=pixel;gpu_vram_dirty_mark_row(physical_y);
+ }
+}
+int sw_draw_source_triangle(const int *x,const int *y,const uint32_t *colors,
+ int shaded,int dither,int interlace,unsigned skip_field,
+ const SourceGPUTexture *texture,int *extra_work) {
+ *extra_work=0;
+ if(g_hr || g_wide_cur || g_precise_valid || g_perspective_valid)return 0;
+ int area=(x[1]-x[0])*(y[2]-y[0])-(x[2]-x[0])*(y[1]-y[0]);
+ int core=x[1]<=x[0]?(x[2]<=x[1]?2:1):x[2]page>>7)&3;if(c.mode>2)c.mode=2;
+ source_texture_palette(&c); /* CLUT fill precedes even degenerate draws. */
+ }
+ if(area)for(unsigned channel=0;channel<(texture?5u:3u);channel++) {
+ int values[3];
+ for(unsigned i=0;i<3;i++)values[i]=channel<3?(colors[i]>>(channel*8))&255u:
+ (texture->uv[i]>>((channel-3)*8))&255u;
+ c.base[channel]=((uint32_t)values[core]<<12)+2048u;
+ if(shaded || channel>=3) {
+ int64_t ax=(int64_t)(values[1]-values[0])*(y[2]-y[0])-(int64_t)(values[2]-values[0])*(y[1]-y[0]);
+ int64_t ay=(int64_t)(x[1]-x[0])*(values[2]-values[0])-(int64_t)(x[2]-x[0])*(values[1]-values[0]);
+ c.dx[channel]=(uint32_t)(ax*4096/area);c.dy[channel]=(uint32_t)(ay*4096/area);
+ }
+ }
+ int result=source_poly_walk(x,y,g_clip_x1,g_clip_y1,g_clip_x2,g_clip_y2,shaded||texture,
+ g_mask_check_bit||g_semi_trans_enabled,interlace,skip_field,source_triangle_span,&c);
+ *extra_work=c.extra_work;
+ return result>=0;
+}
+
+int sw_draw_source_block(const SourceGPUBlock *block,int *extra_work) {
+ *extra_work=0;
+ if(g_hr || g_wide_cur || g_precise_valid || g_perspective_valid)return 0;
+ const uint32_t *words=block->words;unsigned opcode=words[0]>>24;
+ if(opcode==2) {
+ unsigned x0=words[1]&1008u,y0=(words[1]>>16)&511u;
+ unsigned width=((words[2]&1023u)+15u)&~15u,height=(words[2]>>16)&511u;
+ uint16_t color=((words[0]>>3)&31u)|((words[0]>>6)&992u)|((words[0]>>9)&31744u);
+ for(unsigned row=0;rowinterlace && (y&1u)==block->skip_field)continue;
+ for(unsigned col=0;col>16)&511u;
+ unsigned dx=words[2]&1023u,dy=(words[2]>>16)&511u;
+ unsigned width=words[3]&1023u,height=(words[3]>>16)&511u;
+ if(!width)width=1024;if(!height)height=512;
+ uint16_t row_chunk[128];
+ for(unsigned row=0;row128)count=128;
+ for(unsigned i=0;ix,top=block->y,right=left+(int)(dimensions&1023u),bottom=top+(int)((dimensions>>16)&511u);
+ SourceGPUTexture texture={0};SourceTriangleColors c={0};
+ c.target=rt_native();c.core_x=left;c.core_y=top;
+ for(unsigned channel=0;channel<3;channel++)c.base[channel]=((words[0]>>(channel*8))&255u)*4096u;
+ if(textured) {
+ texture.page=block->draw_mode&511u;texture.window=block->texture_window;
+ texture.clut=words[2]>>16;texture.raw=opcode&1;texture.load_clut=1;
+ c.texture=&texture;c.mode=(texture.page>>7)&3;if(c.mode>2)c.mode=2;
+ source_texture_palette(&c);
+ unsigned u=words[2]&255u,v=(words[2]>>8)&255u;
+ if(block->draw_mode&0x1000u)u|=1;
+ c.base[3]=u*4096u;c.base[4]=v*4096u;
+ c.dx[3]=(block->draw_mode&0x1000u)?(uint32_t)-4096:4096u;
+ c.dy[4]=(block->draw_mode&0x2000u)?(uint32_t)-4096:4096u;
+ }
+ if(leftclip_left)left=block->clip_left;if(topclip_top)top=block->clip_top;
+ if(right>block->clip_right+1)right=block->clip_right+1;if(bottom>block->clip_bottom+1)bottom=block->clip_bottom+1;
+ if(right>left)for(int row=top;rowinterlace && ((unsigned)row&1u)==block->skip_field)continue;
+ source_triangle_span(&c,row,left,right-left);
+ }
+ *extra_work=c.extra_work;return 1;
+}
+
static inline void put_textured(const RTarget *t, int x, int y, uint16_t texel,
int mod_r, int mod_g, int mod_b,
int raw_texture) {
diff --git a/runtime/src/input_route_observer.c b/runtime/src/input_route_observer.c
new file mode 100644
index 000000000..fd09b22ef
--- /dev/null
+++ b/runtime/src/input_route_observer.c
@@ -0,0 +1,376 @@
+/* Optional source-neutral observation of a preloaded digital route.
+ * Boundary N is before input N+1, after N records were supplied. This does
+ * not claim equivalence with another engine's frame counter. */
+#include "input_route_observer.h"
+#include "psx_sha256.h"
+#include "psx_memory.h"
+#include "gpu.h"
+#include "sio.h"
+#include "cdrom.h"
+#include "dma.h"
+#include "timers.h"
+#include "event_ring.h"
+#include "debug_server.h"
+#include "png_write.h"
+#include
+#include
+#include
+#include
+#include
+#ifdef _WIN32
+#include
+#else
+#include
+#endif
+
+extern uint8_t *g_psx_ram;
+extern void gl_renderer_sync_cpu(void);
+extern void vk_renderer_sync_cpu(void);
+static FILE *log_file;
+static FILE *sio_trace_file;
+static FILE *watch_file;
+static FILE *cpu_file;
+static FILE *video_file;
+extern void gpu_observer_video_state(uint32_t *out);
+extern void interrupts_observer_field_state(uint32_t *out);
+extern uint64_t psx_cycle_count;
+extern uint32_t i_stat, i_mask;
+static uint32_t watch_u16[32];
+static unsigned watch_count;
+static uint32_t trace_seen;
+static const char *output_dir;
+static uint32_t total_inputs;
+static uint32_t tail_ticks;
+static uint32_t capture_every = 300;
+static psx_sha256_ctx input_hash;
+static psx_sha256_ctx delivered_hash;
+static uint32_t delivered_inputs;
+static uint16_t pending_word;
+static int pending;
+static int first_non_neutral_seen;
+
+static void fail(const char *message) {
+ fprintf(stderr, "input route observation failed: %s\n", message);
+ exit(3);
+}
+static FILE *open_output(const char *name) {
+ char path[4096];
+ int n = snprintf(path, sizeof(path), "%s/%s", output_dir, name);
+ if (n < 0 || (size_t)n >= sizeof(path)) fail("output path too long");
+ /* New output directories belong to the launcher; never overwrite evidence. */
+ /* MSVCRT does not implement C11 fopen's x modifier. O_EXCL retains
+ * atomic no-overwrite semantics on both Windows and POSIX. */
+#ifdef _WIN32
+ int fd = _open(path, _O_WRONLY | _O_CREAT | _O_EXCL | _O_BINARY,
+ _S_IREAD | _S_IWRITE);
+ FILE *f = fd < 0 ? NULL : _fdopen(fd, "wb");
+ if (!f && fd >= 0) _close(fd);
+#else
+ int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600);
+ FILE *f = fd < 0 ? NULL : fdopen(fd, "wb");
+ if (!f && fd >= 0) close(fd);
+#endif
+ if (!f) { fprintf(stderr, "output %s: %s\n", path, strerror(errno));
+ fail("cannot create new output"); }
+ return f;
+}
+static void hex_hash(const uint8_t bytes[32], char text[65]) {
+ static const char h[] = "0123456789abcdef";
+ for (unsigned i=0; i<32; ++i) {
+ text[i*2] = h[bytes[i] >> 4]; text[i*2+1] = h[bytes[i] & 15];
+ }
+ text[64] = 0;
+}
+FILE *input_route_observer_output(const char *name) { return open_output(name); }
+static void capture_cd_metadata(void) {
+ const CDROMCommandHistoryEntry *commands;
+ const CDROMSectorHistoryEntry *sectors;
+ uint64_t total = cdrom_debug_get_command_history(&commands);
+ FILE *f = open_output("cd-commands.jsonl");
+ uint64_t start = total > CDROM_COMMAND_HISTORY_CAP ? total - CDROM_COMMAND_HISTORY_CAP : 0;
+ for (uint64_t i = start; i < total; ++i) {
+ const CDROMCommandHistoryEntry *e = &commands[i % CDROM_COMMAND_HISTORY_CAP];
+ if (e->seq != i) fail("CD command history sequence mismatch");
+ fprintf(f, "{\"seq\":%llu,\"frame\":%u,\"cycle\":%llu,\"kind\":%u,\"cmd\":%u,\"mode\":%u,\"pc\":%u,\"func\":%u,\"stat\":%u,\"reading\":%u,\"read_msf\":[%u,%u,%u],\"params\":[",
+ (unsigned long long)i, e->frame, (unsigned long long)e->cycle, e->kind, e->cmd, e->mode,
+ e->pc, e->func, e->stat, e->reading, e->read_min, e->read_sec, e->read_sect);
+ for (unsigned j = 0; j < e->param_count; ++j) fprintf(f, "%s%u", j ? "," : "", e->params[j]);
+ fputs("]",f);
+ if(e->source_clock) fprintf(f,",\"source_clock\":true,\"random_cursor\":%u,\"random_calls\":%u",
+ e->source_random_cursor,e->source_random_calls);
+ fputs("}\n", f);
+ }
+ if (ferror(f) || fclose(f)) fail("CD command metadata write");
+ total = cdrom_debug_get_sector_history(§ors);
+ f = open_output("cd-sectors.jsonl");
+ start = total > CDROM_SECTOR_HISTORY_CAP ? total - CDROM_SECTOR_HISTORY_CAP : 0;
+ for (uint64_t i = start; i < total; ++i) {
+ const CDROMSectorHistoryEntry *e = §ors[i % CDROM_SECTOR_HISTORY_CAP];
+ if (e->seq != i) fail("CD sector history sequence mismatch");
+ fprintf(f, "{\"seq\":%llu,\"frame\":%u,\"lba\":%d,\"size\":%d,\"mode\":%u,\"data_delivered\":%u}\n",
+ (unsigned long long)i, e->frame, e->lba, e->size, e->mode, e->data_delivered);
+ }
+ if (ferror(f) || fclose(f)) fail("CD sector metadata write");
+ CdBurstRecord bursts[128];
+ int n = cdrom_get_bursts(bursts, 128);
+ f = open_output("cd-bursts.jsonl");
+ for (int i = n - 1; i >= 0; --i) fprintf(f,
+ "{\"start_frame\":%u,\"end_frame\":%u,\"sectors\":%u,\"divisor\":%u}\n",
+ bursts[i].start_frame, bursts[i].end_frame, bursts[i].sectors, bursts[i].divisor);
+ if (ferror(f) || fclose(f)) fail("CD burst metadata write");
+ const DMACDROMHistoryEntry *dma;
+ total = dma_debug_get_cdrom_history(&dma);
+ start = total > DMA_CDROM_HISTORY_CAP ? total - DMA_CDROM_HISTORY_CAP : 0;
+ f = open_output("cd-dma.jsonl");
+ for (uint64_t i = start; i < total; ++i) {
+ const DMACDROMHistoryEntry *e = &dma[i % DMA_CDROM_HISTORY_CAP];
+ if (e->seq != i) fail("CD DMA history sequence mismatch");
+ /* Retain transfer metadata only, never the ring's retail payload words. */
+ fprintf(f,"{\"seq\":%llu,\"frame_start\":%u,\"frame_end\":%u,\"address\":%u,\"requested_words\":%u,\"moved_words\":%u,\"lba\":%d,\"sector_offset\":%d,\"completed\":%u,\"pc\":%u}\n",
+ (unsigned long long)i, e->frame_start, e->frame_end, e->start_addr,
+ e->requested_words,e->moved_words,e->lba,e->sector_read_pos_start,e->completed,e->pc);
+ }
+ if (ferror(f) || fclose(f)) fail("CD DMA metadata write");
+ total = cdrom_timing_total();
+ start = total > 4096 ? total - 4096 : 0;
+ f = open_output("cd-timing.jsonl");
+ for (uint64_t i = start; i < total; ++i) {
+ CdTimingPub e;
+ if (!cdrom_timing_record(i, &e) || e.seq != i) fail("CD timing history sequence mismatch");
+ fprintf(f,"{\"seq\":%llu,\"frame\":%u,\"lba\":%d,\"due_cycle\":%llu,\"buffer_cycle\":%llu,\"irq_arm_cycle\":%llu,\"intc_cycle\":%llu,\"flags\":%u}\n",
+ (unsigned long long)i,e.frame,e.lba,(unsigned long long)e.due_cycle,
+ (unsigned long long)e.buffer_cycle,(unsigned long long)e.irq_arm_cycle,
+ (unsigned long long)e.intc_cycle,e.flags);
+ }
+ if (ferror(f) || fclose(f)) fail("CD timing metadata write");
+ f = open_output("device-events.json");
+ if (event_ring_dump_stream(f) < 0 || fclose(f)) fail("device event metadata write");
+ const CDROMTraceEntry *registers;
+ total = cdrom_debug_get_trace(®isters);
+ start = total > CDROM_TRACE_CAP ? total - CDROM_TRACE_CAP : 0;
+ f = open_output("cd-registers.jsonl");
+ for (uint64_t i = start; i < total; ++i) {
+ const CDROMTraceEntry *e = ®isters[i % CDROM_TRACE_CAP];
+ if (e->seq != i) fail("CD register history sequence mismatch");
+ /* Exclude data-port payloads; retain status, command and IRQ metadata. */
+ if (e->kind == 'D' || (e->kind == 'R' && (e->addr & 3u) == 2u)) continue;
+ fprintf(f,"{\"seq\":%llu,\"cycle\":%llu,\"frame\":%u,\"kind\":%u,\"addr\":%u,\"val\":%u,\"pc\":%u,\"guest_ra\":%u,\"index\":%u,\"istat\":%u,\"irq_enable\":%u,\"irq_flag\":%u,\"response_read\":%u,\"response_count\":%u,\"sector_available\":%u,\"sector_pos\":%d}\n",
+ (unsigned long long)i,(unsigned long long)e->cycle,e->frame,e->kind,e->addr,e->val,
+ e->pc,e->guest_ra,e->index_reg,e->i_stat,e->irq_enable,e->irq_flag,e->response_read,
+ e->response_count,e->sector_available,e->sector_read_pos);
+ }
+ if (ferror(f) || fclose(f)) fail("CD register metadata write");
+}
+int input_route_observer_init(uint32_t total) {
+ if (log_file || total == 0) return 0;
+ output_dir = getenv("PSX_INPUT_ROUTE_CAPTURE_DIR");
+ if (!output_dir) return 1;
+ if (!output_dir[0]) return 0;
+ total_inputs = total;
+ const char *watch = getenv("PSX_INPUT_ROUTE_WATCH_U16");
+ if (watch) {
+ const char *p = watch;
+ do {
+ char *end;
+ if (*p < '0' || *p > '9' || watch_count == 32) return 0;
+ errno = 0;
+ unsigned long address = strtoul(p, &end, 0);
+ if (errno || end == p || (*end && *end != ',') ||
+ address > PSX_MAIN_RAM_BYTES - 2 || (address & 1)) return 0;
+ for (unsigned i = 0; i < watch_count; ++i)
+ if (watch_u16[i] == address) return 0;
+ watch_u16[watch_count++] = (uint32_t)address;
+ p = *end ? end + 1 : end;
+ if (*end && !*p) return 0;
+ } while (*p);
+ }
+ const char *every = getenv("PSX_INPUT_ROUTE_CAPTURE_EVERY");
+ if (every) {
+ char *end;
+ unsigned long n = strtoul(every, &end, 10);
+ if (!every[0] || *end || !n || n > 10000) return 0;
+ capture_every = (uint32_t)n;
+ }
+ const char *tail = getenv("PSX_INPUT_ROUTE_NEUTRAL_TAIL");
+ if (tail) {
+ char *end;
+ unsigned long n = strtoul(tail, &end, 10);
+ if (!tail[0] || *end || n > 60000) return 0;
+ tail_ticks = (uint32_t)n;
+ }
+ psx_sha256_init(&input_hash);
+ psx_sha256_init(&delivered_hash);
+ delivered_inputs = 0;
+ pending = 0;
+ first_non_neutral_seen = 0;
+ log_file = open_output("checkpoints.jsonl");
+ const char *cpu_state = getenv("PSX_INPUT_ROUTE_CPU_STATE");
+ if (cpu_state && strcmp(cpu_state, "1") == 0)
+ cpu_file = open_output("cpu-state.jsonl");
+ if (getenv("PSX_INPUT_ROUTE_VIDEO_STATE") && strcmp(getenv("PSX_INPUT_ROUTE_VIDEO_STATE"),"1")==0)
+ video_file = open_output("video-state.jsonl");
+ if (watch_count) watch_file = open_output("ram-u16.jsonl");
+ const char *trace = getenv("PSX_INPUT_ROUTE_TRACE");
+ if (trace && strcmp(trace, "1") == 0) sio_trace_file = open_output("sio-bytes.jsonl");
+ return 1;
+}
+void input_route_observer_input(uint16_t buttons) {
+ if (!log_file) return;
+ if (pending) fail("previous input was not delivered to SIO");
+ pending_word = buttons;
+ pending = 1;
+ const uint8_t word[2] = {(uint8_t)buttons, (uint8_t)(buttons >> 8)};
+ if (delivered_inputs < total_inputs)
+ psx_sha256_update(&input_hash, word, sizeof(word));
+ else if (buttons != 0xFFFF) fail("post-input sample was not neutral");
+}
+void input_route_observer_set_end(uint32_t total) {
+ if (!total || total != delivered_inputs || pending) fail("invalid dynamic input end");
+ total_inputs=total;
+}
+void input_route_observer_applied(uint16_t buttons, int connected, int analog) {
+ if (!log_file) return;
+ if (!pending || pending_word != buttons || !connected || analog)
+ fail("SIO delivery differs from digital route");
+ const uint8_t word[2] = {(uint8_t)buttons, (uint8_t)(buttons >> 8)};
+ if (delivered_inputs < total_inputs)
+ psx_sha256_update(&delivered_hash, word, sizeof(word));
+ ++delivered_inputs;
+ pending = 0;
+ if (buttons != 0xFFFF && !first_non_neutral_seen) {
+ fprintf(stdout, "input_route_first_non_neutral: record=%u buttons=%04X connected=%d analog=%d\n",
+ (unsigned)delivered_inputs, (unsigned)buttons, connected, analog);
+ fflush(stdout);
+ first_non_neutral_seen = 1;
+ }
+}
+void input_route_observer_boundary(uint32_t completed, uint64_t runtime_frame) {
+ if (log_file && (pending || completed != delivered_inputs))
+ fail("boundary does not follow exact SIO delivery count");
+ if (video_file) {
+ uint32_t gpu[5],clock[3];
+ gpu_observer_video_state(gpu); interrupts_observer_field_state(clock);
+ fprintf(video_file,"{\"frame\":%u,\"runtime_frame\":%llu,\"boundary\":\"before_next_input\",\"cycle\":%llu,\"display_mode\":%u,\"vertical_start\":%u,\"vertical_end\":%u,\"gpu_field\":%u,\"lcf\":%u,\"cycles_since_vblank\":%u,\"next_period\":%u,\"profile_field\":%u}\n",
+ completed,(unsigned long long)runtime_frame,(unsigned long long)psx_cycle_count,
+ gpu[0],gpu[1],gpu[2],gpu[3],gpu[4],clock[0],clock[1],clock[2]);
+ if(ferror(video_file) || fflush(video_file))fail("video state write");
+ }
+ if (cpu_file) {
+ const CPUState *cpu = debug_cpu_ptr;
+ if (!cpu) fail("CPU state unavailable");
+ uint16_t counter[3], target[3];
+ uint32_t mode[3], frac[3];
+ int32_t irq_line[3];
+ timers_get_snapshot(counter, mode, target, irq_line, frac);
+ fprintf(cpu_file, "{\"frame\":%u,\"runtime_frame\":%llu,\"boundary\":\"before_next_input\",\"pc\":%u,\"ra\":%u,\"sr\":%u,\"cause\":%u,\"epc\":%u,\"i_stat\":%u,\"i_mask\":%u,\"timers\":[",
+ completed, (unsigned long long)runtime_frame, cpu->pc, cpu->gpr[31],
+ cpu->cop0[12], cpu->cop0[13], cpu->cop0[14], i_stat, i_mask);
+ for (unsigned i=0; i<3; ++i)
+ fprintf(cpu_file, "%s{\"channel\":%u,\"counter\":%u,\"mode\":%u,\"target\":%u,\"irq_line\":%d,\"fraction\":%u}",
+ i ? "," : "", i, counter[i], mode[i], target[i], irq_line[i], frac[i]);
+ fputs("]}\n", cpu_file);
+ if (ferror(cpu_file) || fflush(cpu_file)) fail("CPU state write");
+ }
+ if (sio_trace_file) {
+ const SioTraceEntry *ring;
+ int next;
+ uint32_t total = sio_get_trace(&ring, &next);
+ if (total - trace_seen > SIO_TRACE_CAP) fail("SIO trace overwritten");
+ while (trace_seen < total) {
+ const SioTraceEntry *e = &ring[trace_seen % SIO_TRACE_CAP];
+ if (e->seq != trace_seen) fail("SIO trace sequence mismatch");
+ if (fprintf(sio_trace_file,
+ "{\"boundary\":%u,\"seq\":%u,\"tx\":%u,\"rx\":%u,\"ctrl\":%u,\"dev_pre\":%u,\"dev_post\":%u}\n",
+ completed, e->seq, e->tx, e->rx, e->ctrl, e->dev_pre, e->dev_post) < 0)
+ fail("SIO trace write");
+ ++trace_seen;
+ }
+ if (fflush(sio_trace_file)) fail("SIO trace flush");
+ }
+ if (watch_file) {
+ fprintf(watch_file, "{\"frame\":%u,\"runtime_frame\":%llu,\"boundary\":\"before_next_input\",\"cpu_pc\":%u,\"cpu_ra\":%u,\"ram_u16\":{",
+ completed, (unsigned long long)runtime_frame,
+ debug_cpu_ptr ? debug_cpu_ptr->pc : 0,
+ debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0);
+ for (unsigned i = 0; i < watch_count; ++i) {
+ uint32_t a = watch_u16[i];
+ unsigned value = g_psx_ram[a] | ((unsigned)g_psx_ram[a + 1] << 8);
+ fprintf(watch_file, "%s\"%06X\":%u", i ? "," : "", a, value);
+ }
+ if (fputs("}}\n", watch_file) < 0 || fflush(watch_file)) fail("RAM watch write");
+ }
+ if (!log_file || (completed % (completed > total_inputs && capture_every > 60 ? 60 : capture_every)
+ && completed != total_inputs && completed != total_inputs + tail_ticks)) return;
+ uint8_t digest[32]; char ram_hash[65], applied_hash[65], supplied_hash[65], name[80];
+ GpuDisplayInfo di;
+ gpu_get_display_info(&di);
+ if (!di.depth24 && !di.disabled) {
+ gl_renderer_sync_cpu(); vk_renderer_sync_cpu();
+ }
+ psx_sha256_compute(g_psx_ram, PSX_MAIN_RAM_BYTES, digest);
+ hex_hash(digest, ram_hash);
+ psx_sha256_ctx supplied = input_hash;
+ psx_sha256_final(&supplied, digest); hex_hash(digest, supplied_hash);
+ psx_sha256_ctx applied = delivered_hash;
+ psx_sha256_final(&applied, digest); hex_hash(digest, applied_hash);
+ unsigned w = di.disabled ? 1u : (unsigned)di.width;
+ unsigned h = di.disabled ? 1u : (unsigned)di.height;
+ if (!w || w > 640) w = 1;
+ if (!h || h > 512) h = 1;
+ uint8_t *rgb = (uint8_t *)calloc((size_t)w*h, 3);
+ if (!rgb) fail("screenshot allocation");
+ if (!di.disabled) for (unsigned y=0; y total_inputs ? completed - total_inputs : 0), w, h,
+ di.disabled ? "true" : "false") < 0 || fflush(log_file)) fail("checkpoint write");
+ if (completed == total_inputs) {
+ FILE *input_end = open_output("input-end.json");
+ if (fprintf(input_end,
+ "{\"frame\":%u,\"applied_words_sha256\":\"%s\",\"neutral_tail_ticks_planned\":%u}\n",
+ (unsigned)completed, applied_hash, (unsigned)tail_ticks) < 0 || fclose(input_end))
+ fail("input end write");
+ }
+ if (completed == total_inputs + tail_ticks) {
+ if (watch_count) {
+ FILE *writes = open_output("ram-writes.jsonl");
+ debug_server_dump_watched_writes(writes, watch_u16, watch_count);
+ if (ferror(writes) || fclose(writes)) fail("RAM write metadata export");
+ }
+ if (watch_file && fclose(watch_file)) fail("RAM watch close");
+ if (cpu_file && fclose(cpu_file)) fail("CPU state close");
+ if (video_file && fclose(video_file)) fail("video state close");
+ video_file = NULL;
+ watch_file = NULL;
+ if (sio_trace_file) capture_cd_metadata();
+ if (sio_trace_file && fclose(sio_trace_file)) fail("SIO trace close");
+ if (fclose(log_file)) fail("checkpoint close");
+ log_file = NULL;
+ FILE *done = open_output("complete.json");
+ if (fprintf(done,
+ "{\"frame\":%u,\"input_frames\":%u,\"neutral_tail_ticks\":%u,"
+ "\"stop_reason\":\"declared_observation_end_before_next_sample\","
+ "\"applied_words_sha256\":\"%s\",\"game_outcome\":\"unclassified\"}\n",
+ (unsigned)completed, (unsigned)total_inputs, (unsigned)tail_ticks,
+ applied_hash) < 0 || fclose(done)) fail("completion write");
+ fprintf(stdout, "input_route_complete: frames=%u words_sha256=%s\n", completed, applied_hash);
+ fflush(stdout);
+ exit(0);
+ }
+}
diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c
index c7797f462..dff7f2f0a 100644
--- a/runtime/src/interrupts.c
+++ b/runtime/src/interrupts.c
@@ -36,8 +36,14 @@
#include "event_ring.h"
#include "lockstep.h"
#include "psx_cycles.h"
+#include "psx_icache.h"
+#include "psx_memory.h"
+#include "psx_cyc.h"
#include "psx_scheduler.h"
#include "spu.h"
+#include "input_route_field_clock.h"
+#include "input_route_raster_clock.h"
+#include "source_gpu_runtime.h"
#include
#include
#include
@@ -185,8 +191,30 @@ void psx_irq_raise(uint32_t bit, uint32_t detail)
* (T32, MGS PAL: measured 564,480 cycles/VBlank with GPUSTAT.20 = PAL). */
#define VBLANK_CYCLES_NTSC 564480u
#define VBLANK_CYCLES_PAL 677376u
+static int input_route_source_fields;
+static InputRouteFieldClock input_route_field_clock;
+static int input_route_source_raster;
+static int input_route_source_gpu_status;
+static InputRouteRasterClock input_route_raster;
+static uint64_t input_route_raster_deadline;
+static uint32_t input_route_raster_pending;
+static void input_route_raster_recompute(void) {
+ uint32_t distance=input_route_raster_until_rise(&input_route_raster);
+ input_route_raster_deadline=distance==UINT32_MAX ? UINT64_MAX : input_route_raster.cycle+distance;
+}
extern int gpu_video_standard_is_pal(void);
static inline uint32_t vblank_period_cycles(void) {
+ if (input_route_source_raster) {
+ if (input_route_raster_deadline==UINT64_MAX) return UINT32_MAX;
+ return (uint32_t)(input_route_raster_deadline-input_route_raster.last_rise);
+ }
+ if (input_route_source_fields) {
+ if (gpu_video_standard_is_pal()) {
+ fprintf(stderr, "[input-field-clock] NTSC comparison profile cannot run PAL display mode\n");
+ exit(4);
+ }
+ return input_route_field_clock.current_cycles;
+ }
return gpu_video_standard_is_pal() ? VBLANK_CYCLES_PAL : VBLANK_CYCLES_NTSC;
}
#define VBLANK_CYCLES vblank_period_cycles()
@@ -194,6 +222,37 @@ static inline uint32_t vblank_period_cycles(void) {
static uint32_t dispatch_count;
static uint64_t total_checks;
static uint32_t cycles_since_vblank; /* incremented by interrupts_advance_cycles */
+void interrupts_observer_field_state(uint32_t *out) {
+ out[0]=cycles_since_vblank; out[1]=VBLANK_CYCLES;
+ out[2]=input_route_source_fields ? input_route_field_clock.field : UINT32_MAX;
+ if (input_route_source_raster) out[2]=input_route_raster.field;
+}
+void interrupts_raster_gp1(uint32_t word) {
+ if (!input_route_source_raster) return;
+ if (input_route_raster.cycle!=psx_cycle_count || !input_route_raster_gp1(&input_route_raster,word)) {
+ fprintf(stderr,"[input-raster-clock] unsynchronized GP1 or unsupported PAL mode\n");
+ exit(4);
+ }
+ uint32_t command=(word>>24)&63u;
+ if (command==0 || command==7 || command==8) {
+ extern uint64_t g_psx_cycle_fast_limit;
+ input_route_raster_recompute();
+ g_psx_cycle_fast_limit=0;
+ /* Existing MMIO sync ran before this write. Publish the new deadline
+ * after changing the raster controls so a prior later deadline cannot
+ * let the CPU cross an earlier newly programmed vertical boundary. */
+ psx_devices_mmio_sync();
+ }
+}
+int interrupts_raster_gpu_status(uint32_t *bits) {
+ if (!input_route_source_gpu_status) return 0;
+ if (input_route_raster.cycle!=psx_cycle_count) {
+ fprintf(stderr,"[input-raster-status] unsynchronized GPUSTAT read\n");
+ exit(4);
+ }
+ *bits=input_route_raster_status(&input_route_raster);
+ return 1;
+}
extern uint64_t g_vblank_raise_count;
extern int g_cosim_dirty_pump_site;
@@ -203,6 +262,7 @@ static int in_exception;
* below this. 0 = no cooldown active. Counted in guest cycles (not calls) so both
* backends make the identical delivery decision — see the cooldown constants. */
static uint64_t post_exception_cooldown_until;
+static struct {uint32_t pc,target,cause;} source_irq_slot;
static uint32_t last_sio_seq_seen;
static uint64_t last_sio_progress_cycle;
@@ -413,11 +473,11 @@ uint64_t g_spu_sample_ctrl_rejects;
void psx_set_midframe_audio_pump(void (*fn)(void)) { s_midframe_audio_pump = fn; }
-/* While SPU IRQ9 is enabled, expose each 44.1-kHz sample as a device deadline
- * so guest code can acknowledge and re-arm an IRQ-address hit before the next
- * sample. Rendering a whole VBlank's accumulated samples as one chunk collapses
- * multiple hardware IRQ edges into one latch, slowing IRQ-driven audio engines
- * and blocking cutscene synchronization. Keep an explicit opt-out for bisecting
+/* Expose each 44.1-kHz sample as a device deadline. Status-control application
+ * progresses even with IRQ9 disabled; gating this on IRQ enable leaves a
+ * polling guest's applied control stale until a later VBlank pump. IRQ-enabled
+ * code also needs to acknowledge and re-arm between sample address hits.
+ * Keep an explicit opt-out for bisecting
* old captures; faithful per-sample scheduling is the production default. */
static int spu_sample_event_mode(void) {
static int enabled = -1;
@@ -429,7 +489,6 @@ static int spu_sample_event_mode(void) {
}
uint32_t psx_spu_sample_event_cycles_to_next(void) {
- SpuGlobalState state;
g_spu_sample_deadline_queries++;
if (!spu_sample_event_mode()) {
g_spu_sample_mode_rejects++;
@@ -439,11 +498,6 @@ uint32_t psx_spu_sample_event_cycles_to_next(void) {
g_spu_sample_pump_null_rejects++;
return UINT32_MAX;
}
- spu_get_global_state(&state);
- if ((state.ctrl & 0x0040u) == 0) {
- g_spu_sample_ctrl_rejects++;
- return UINT32_MAX;
- }
g_spu_sample_enabled_queries++;
/* The PS1 CPU/SPU ratio is exactly 768 CPU cycles per 44.1-kHz sample.
@@ -471,8 +525,7 @@ void psx_spu_sample_event_service(void) {
if ((psx_get_cycle_count() % 768u) != 0)
g_spu_sample_deferred_mismatches++;
}
- if ((state.ctrl & 0x0040u) != 0 &&
- (psx_get_cycle_count() % 768u) == 0) {
+ if ((psx_get_cycle_count() % 768u) == 0) {
g_spu_sample_service_pumps++;
s_midframe_audio_pump();
}
@@ -482,7 +535,13 @@ static void fire_vblank_edge(void) {
/* Subtract one VBlank period rather than reset to 0 so cycle overshoot
* carries forward. Prevents long-running blocks from rounding multiple
* VBlanks together. */
- cycles_since_vblank -= VBLANK_CYCLES;
+ if (input_route_source_raster) {
+ input_route_raster_pending--;
+ cycles_since_vblank=(uint32_t)(input_route_raster.cycle-input_route_raster.last_rise);
+ } else cycles_since_vblank -= VBLANK_CYCLES;
+ if (input_route_source_fields)
+ input_route_field_clock_next(&input_route_field_clock,
+ gpu_display_is_interlaced());
dispatch_count = 0;
/* DEQUEUE: this VBlank fired. ENQUEUE: next VBlank scheduled one period out. */
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_VBLANK,
@@ -503,7 +562,13 @@ static void fire_vblank_edge(void) {
void interrupts_service_scheduled_events(void) {
note_sio_progress_cycle();
- if (in_exception) return;
+ /* A source raster edge latches I_STAT even while an interrupt handler
+ * runs with IEc clear. CPU delivery remains gated separately below. */
+ if (in_exception && !source_gpu_runtime_active()) return;
+ if (input_route_source_raster) {
+ if (input_route_raster_pending && !should_defer_vblank_for_sio()) fire_vblank_edge();
+ return;
+ }
while (cycles_since_vblank >= VBLANK_CYCLES) {
if (should_defer_vblank_for_sio()) return;
fire_vblank_edge();
@@ -511,6 +576,11 @@ void interrupts_service_scheduled_events(void) {
}
uint32_t interrupts_cycles_to_vblank(void) {
+ if (input_route_source_raster) {
+ if (input_route_raster_pending) return 0;
+ if (input_route_raster_deadline==UINT64_MAX) return UINT32_MAX;
+ return input_route_raster_deadline<=input_route_raster.cycle ? 0 : (uint32_t)(input_route_raster_deadline-input_route_raster.cycle);
+ }
if (cycles_since_vblank >= VBLANK_CYCLES) return 0;
return VBLANK_CYCLES - cycles_since_vblank;
}
@@ -520,11 +590,30 @@ uint32_t interrupts_get_cycles_since_vblank(void) {
}
void interrupts_set_cycles_since_vblank(uint32_t v) {
+ if (input_route_source_fields || input_route_source_raster) {
+ fprintf(stderr, "[input-field-clock] comparison profile requires a cold boot; state restore is unsupported\n");
+ exit(4);
+ }
cycles_since_vblank = v;
}
void interrupts_advance_cycles(uint32_t cycles) {
cycles_since_vblank += cycles;
+ if (input_route_source_raster) {
+ uint32_t old=input_route_raster.rises;
+ if(timers_source_raster_enabled()) {
+ input_route_raster_advance_observed(&input_route_raster,cycles,timers_source_raster_event,NULL);
+ timers_source_raster_finish(input_route_raster.cycle);
+ } else input_route_raster_advance(&input_route_raster,cycles);
+ if (input_route_raster.rises!=old) {
+ input_route_raster_pending+=input_route_raster.rises-old;
+ if (input_route_raster_pending>1) {
+ fprintf(stderr,"[input-raster-clock] more than one undelivered VBlank; comparison unsupported\n");
+ exit(4);
+ }
+ input_route_raster_recompute();
+ }
+ }
interrupts_service_scheduled_events();
}
@@ -817,6 +906,39 @@ void psx_get_freeze_diag(uint64_t *out_total_checks,
}
void interrupts_init(void) {
+ const char *field_model = getenv("PSX_INPUT_ROUTE_FIELD_MODEL");
+ input_route_source_fields = 0;
+ input_route_source_raster = 0;
+ input_route_raster_pending = 0;
+ input_route_source_gpu_status = 0;
+ if (field_model && *field_model) {
+ int raster=strcmp(field_model,"octoshock-2.2.2-ntsc-raster")==0;
+ if ((!raster && strcmp(field_model, "octoshock-2.2.2-ntsc-fields") != 0) ||
+ !getenv("PSX_INPUT_ROUTE_FILE")) {
+ fprintf(stderr, "[input-field-clock] invalid model or missing input route\n");
+ exit(4);
+ }
+ if (raster) {
+ input_route_source_raster=1;
+ input_route_raster_reset(&input_route_raster);
+ input_route_raster_recompute();
+ fprintf(stderr,"[input-raster-clock] experimental NTSC raster VBlank deadlines; first=%llu; native IRQ deferral/GPUSTAT/timers retained\n",(unsigned long long)input_route_raster_deadline);
+ } else {
+ input_route_source_fields = 1;
+ input_route_field_clock_reset(&input_route_field_clock);
+ fprintf(stderr, "[input-field-clock] experimental cold-boot NTSC field durations; first=%u, ratio=103896/65536\n",
+ input_route_field_clock.current_cycles);
+ }
+ }
+ const char *status_model=getenv("PSX_GPU_STATUS_MODEL");
+ if (status_model && *status_model) {
+ if (strcmp(status_model,"octoshock-2.2.2-raster") || !input_route_source_raster) {
+ fprintf(stderr,"[input-raster-status] invalid model or missing NTSC raster clock\n");
+ exit(4);
+ }
+ input_route_source_gpu_status=1;
+ fprintf(stderr,"[input-raster-status] experimental source GPUSTAT31/13; native readiness and rendering retained\n");
+ }
dispatch_count = 0;
in_exception = 0;
exception_nest_depth = 0;
@@ -1009,8 +1131,7 @@ uint32_t cycles_to_next_event(void) {
* card-SIO case only pushes VBlank LATER, so this estimate stays a safe
* under-estimate. */
if (i_mask & (1u << IRQ_VBLANK)) {
- uint32_t d = (cycles_since_vblank >= VBLANK_CYCLES)
- ? 0u : (VBLANK_CYCLES - cycles_since_vblank);
+ uint32_t d = interrupts_cycles_to_vblank();
if (d < best) best = d;
}
uint32_t t = timers_cycles_to_irq(i_mask); if (t < best) best = t;
@@ -1051,6 +1172,11 @@ void psx_interrupt_check_path_diag(uint64_t *entry, uint64_t *fast_sr,
if (irq_deliv) *irq_deliv = g_irq_deliver_count;
}
+int psx_interrupt_cooldown_active(void) {
+ return post_exception_cooldown_until != 0 &&
+ psx_get_cycle_count() < post_exception_cooldown_until;
+}
+
int psx_interrupt_delivery_needed(const CPUState* cpu) {
if (s_defer_switch_pending) { s_need_defer++; return 1; }
if ((i_stat & i_mask) == 0) { s_skip_none++; return 0; }
@@ -1058,8 +1184,7 @@ int psx_interrupt_delivery_needed(const CPUState* cpu) {
uint32_t sr = cpu->cop0[COP0_SR];
if (!(sr & 0x01u) || !(sr & (1u << 10))) { s_skip_sr++; return 0; }
- if (post_exception_cooldown_until != 0 &&
- psx_get_cycle_count() < post_exception_cooldown_until) {
+ if (psx_interrupt_cooldown_active()) {
s_skip_cooldown++;
return 0;
}
@@ -1075,6 +1200,12 @@ int psx_interrupt_delivery_needed(const CPUState* cpu) {
return 1;
}
+int psx_irq_opcode_eligible(uint32_t pc) {
+ uint32_t instruction;
+ return !source_gpu_runtime_active() ||
+ !memory_peek_instruction_word(pc,&instruction) || (instruction>>26)!=0x12u;
+}
+
void psx_check_interrupts(CPUState* cpu) {
psx_cyc_batch_flush();
extern int g_ls_suppress_record;
@@ -1463,7 +1594,7 @@ void psx_check_interrupts(CPUState* cpu) {
* RFE before the next delivery. Gated on the guest-cycle deadline (not a
* per-call countdown) so compiled and interp agree on the delivery cycle. */
if (post_exception_cooldown_until != 0) {
- if (psx_get_cycle_count() < post_exception_cooldown_until) {
+ if (psx_interrupt_cooldown_active()) {
irq_record_outcome(EV_IRQ_GATE, GATE_COOLDOWN, 0);
#ifdef PSX_COSIM
COSIM_IRQ_NOTE(3u);
@@ -1496,6 +1627,7 @@ void psx_check_interrupts(CPUState* cpu) {
PSX_CHECK_INTERRUPTS_RETURN();
} /* No deliverable interrupt source (hw masked and no sw pending) */
+ uint32_t source_irq_cause_ce = 0;
/* Architectural take-PC = the resume PC (same selection the async-RFE block
* below uses): the dirty-interp commits the exact interrupted instruction in
* g_dirty_safe_resume_pc; compiled code passes its block-entry PC via
@@ -1505,6 +1637,42 @@ void psx_check_interrupts(CPUState* cpu) {
extern uint32_t g_dirty_safe_resume_pc;
uint32_t take_pc = g_dirty_safe_resume_pc ? g_dirty_safe_resume_pc
: s_compiled_interrupt_resume_pc;
+ /* The existing cache/cycle model fetches the interrupted opcode before
+ * selecting the interrupt operation. Preserve that fetch's tags and
+ * load-absorb effects, but never execute its register/store effects.
+ * Authored original-core IRQ/JR/RFE cases own this source-model order;
+ * it is not a claim of physical pipeline timing. Unknown legacy entry
+ * sentinels have no guest fetch identity and retain their old behavior.
+ * Keep this before SR/EPC mutation and before the delivery timestamp. */
+ uint32_t fetch_pc = source_irq_slot.pc?source_irq_slot.pc:take_pc;
+ if (fetch_pc == 0u) {
+ extern int psx_scheduler_top_level_resume_active(void);
+ if (psx_scheduler_top_level_resume_active()) fetch_pc = cpu->pc;
+ }
+ /* Source IPCache selects COP2 itself even with an IRQ pending. Do not
+ * fetch/charge a synthetic interrupt operation at that opcode; the
+ * normal executor owns its effects and the next recognition point. */
+ if(fetch_pc && !psx_irq_opcode_eligible(fetch_pc))
+ PSX_CHECK_INTERRUPTS_RETURN();
+ uint32_t fetch_phys = fetch_pc & 0x1FFFFFFFu;
+ if (fetch_pc != 0u && (fetch_pc & 3u) == 0u &&
+ (fetch_phys < 0x00200000u ||
+ (fetch_phys >= 0x1FC00000u && fetch_phys < 0x1FC80000u &&
+ psx_is_dispatchable(fetch_pc)))) {
+ extern int source_gpu_runtime_active(void);
+ if (source_gpu_runtime_active() && !source_irq_slot.pc) {
+ uint32_t instruction;
+ if (memory_peek_instruction_word(fetch_pc, &instruction))
+ source_irq_cause_ce = (instruction << 2) & 0x30000000u;
+ }
+ psx_icache_fetch(cpu, fetch_pc);
+#ifdef PSX_ENABLE_BLOCK_CYCLES
+ /* Interrupt dispatch has no dependencies on the preempted opcode.
+ * Its ordinary step still consumes base/load-absorb timing. */
+ psx_cyc_step(cpu, 0u);
+ psx_cyc_batch_flush();
+#endif
+ }
irq_record_outcome(EV_IRQ_DELIVER, 0, take_pc);
}
g_irq_deliver_count++;
@@ -1553,6 +1721,17 @@ void psx_check_interrupts(CPUState* cpu) {
* software-interrupt delivery gets IP2 reflecting the true line state
* instead of whatever bit 10 happened to be left as. */
cpu->cop0[COP0_CAUSE] = (cpu->cop0[COP0_CAUSE] & ~0x7C) | (0 << 2);
+ if(source_irq_slot.pc) {
+ cpu->cop0[COP0_CAUSE]=(cpu->cop0[COP0_CAUSE]&0x0000ff00u)|source_irq_slot.cause;
+ cpu->cop0[6]=source_irq_slot.target; /* source TAR / branch destination */
+ source_irq_slot.pc=0; /* nested handler entries have their own context */
+ } else {
+ extern int source_gpu_runtime_active(void);
+ /* Original Exception derives CE from the fetched opcode for IRQ too.
+ * This is source-model compatibility, not a physical-CPU CE claim. */
+ if(source_gpu_runtime_active())
+ cpu->cop0[COP0_CAUSE]=(cpu->cop0[COP0_CAUSE]&0x0000ff00u)|source_irq_cause_ce;
+ }
psx_irq_refresh_cause_ip2();
/* Push SR exception stack: shift bits [5:0] left by 2. */
@@ -1698,25 +1877,11 @@ void psx_check_interrupts(CPUState* cpu) {
* (e.g. VSync callback loop at 0xBFC421D8), still in exception
* context. The redirected code eventually calls ReturnFromException.
* - Code 1: ReturnFromException — exit the loop entirely. */
- uint32_t target_pc;
- if (sr & 0x00400000u) {
- target_pc = 0xBFC00180u;
- } else {
- uint32_t w0 = cpu->read_word(0x80000080u);
- uint32_t w1 = cpu->read_word(0x80000084u);
- uint32_t hi_val = (w0 & 0xFFFF) << 16;
- int16_t lo_val = (int16_t)(w1 & 0xFFFF);
- target_pc = hi_val + (uint32_t)(int32_t)lo_val;
- /* The RAM exception vector is a LUI/ADDIU pair that materializes the
- * installed handler address in $k0 before transferring to it. The
- * host-side fast path decodes that pair and dispatches straight to the
- * target, so it must also commit the pair's architectural register
- * result. Vigilante 8's handler uses $k0 as its table base on its very
- * first instruction; leaving the interrupted value in $k0 made it load
- * a BIOS instruction word (0xAD400000) as a jump target. */
- uint32_t vector_reg = (w1 >> 16) & 31u;
- if (vector_reg != 0u) cpu->gpr[vector_reg] = target_pc;
- }
+ uint32_t target_pc = (sr & 0x00400000u) ? 0xBFC00180u : 0x80000080u;
+ /* Execute the installed vector through the ordinary guest dispatcher.
+ * Decoding LUI/ADDIU here skipped its instruction fetches and effects,
+ * charged data reads instead, and assumed every vector used that encoding.
+ * The real vector sets $k0 when required by its installed handler. */
/* Record which fiber owns this setjmp. Any subsequent longjmp must
* happen on this same fiber; if a non-owner fiber needs to longjmp
@@ -2017,7 +2182,13 @@ void psx_check_interrupts(CPUState* cpu) {
* executes at least one instruction between exceptions; in our model
* each "block" is many instructions, but the handler also consumes
* hundreds of sub-dispatches per invocation. */
- if ((i_stat & i_mask) != 0 && i_stat == pre_handler_istat) {
+ extern int source_gpu_runtime_active(void);
+ if (source_gpu_runtime_active()) {
+ /* The original instruction loop recognizes an eligible IRQ again after
+ * guest RFE, including a newly armed or still-pending line. Legacy
+ * dispatcher breathing room must not add guest clocks to this profile. */
+ post_exception_cooldown_until = 0;
+ } else if ((i_stat & i_mask) != 0 && i_stat == pre_handler_istat) {
/* unclaimed: give main code guest-time to install handlers */
post_exception_cooldown_until = psx_get_cycle_count() + POST_EXC_UNCLAIMED_COOLDOWN_CYCLES;
} else {
@@ -2188,6 +2359,21 @@ void psx_check_interrupts_at(CPUState* cpu, uint32_t resume_pc) {
s_compiled_interrupt_resume_pc = prev;
}
+int psx_check_interrupts_delay_slot(CPUState *cpu,uint32_t slot_pc,
+ uint32_t target,int taken,uint32_t instruction) {
+ if(source_gpu_runtime_active() && (instruction>>26)==0x12u)return 0;
+ extern uint32_t g_dirty_safe_resume_pc;
+ uint32_t previous=g_dirty_safe_resume_pc;
+ uint64_t before=g_irq_deliver_count;
+ source_irq_slot.pc=slot_pc;source_irq_slot.target=target;
+ source_irq_slot.cause=0x80000000u|(taken?0x40000000u:0u)|((instruction<<2)&0x30000000u);
+ g_dirty_safe_resume_pc=slot_pc-4u;cpu->pc=slot_pc-4u;
+ psx_check_interrupts_at(cpu,slot_pc-4u);
+ g_dirty_safe_resume_pc=previous;source_irq_slot.pc=0;
+ if(g_irq_deliver_count!=before && !cpu->pc)cpu->pc=slot_pc-4u;
+ return g_irq_deliver_count!=before;
+}
+
int psx_interrupts_checked_at_current_cycle(uint32_t resume_pc) {
return s_last_interrupt_check_cycle == psx_get_cycle_count() &&
same_guest_pc(s_last_interrupt_check_pc, resume_pc);
diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp
index 75bb8121c..83ee54eb5 100644
--- a/runtime/src/main.cpp
+++ b/runtime/src/main.cpp
@@ -18,6 +18,7 @@
#include "bios_hle_plan.h"
#include "psx_bios_backend.h"
#include "psx_cycles.h"
+#include "source_gpu_runtime.h"
#include "starvation_ring.h"
#include "load_accel.h"
#include "savestate.h"
@@ -7041,6 +7042,9 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
sample_headless_pad_into_sio(override);
else
sample_pad_into_sio(override);
+#ifndef PSX_NO_DEBUG_TOOLS
+ debug_server_note_input_applied();
+#endif
}
/* Offline vblank boundary: record/replay/compare (PSX_RB_SELFCHECK).
* Defer opening a window while multitap arming is still pending —
@@ -13513,6 +13517,15 @@ int main(int argc, char** argv) {
interrupts_init();
sio_init();
psx_event_step_conservative_env_init();
+ const char *gpu_work_model=std::getenv("PSX_GPU_DMA_MODEL");
+ if(gpu_work_model && !std::strcmp(gpu_work_model,"octoshock-2.2.2-bounded-quad")) {
+ const char *field=std::getenv("PSX_INPUT_ROUTE_FIELD_MODEL");
+ if(!std::getenv("PSX_INPUT_ROUTE_FILE") || !field || std::strcmp(field,"octoshock-2.2.2-ntsc-raster")) {
+ std::fprintf(stderr,"[source-gpu-service] bounded quad mode requires a route and source NTSC raster clock\n");
+ return 2;
+ }
+ source_gpu_runtime_init();
+ }
/* Seed per-player device routing from the resolved [controller] config.
* SDL controller handles are opened later (after SDL_Init); here we only
* set the PSX-visible connection + pad type so the BIOS sees the right
@@ -13642,8 +13655,22 @@ int main(int argc, char** argv) {
std::atexit(game_options_save_now);
#ifndef PSX_NO_DEBUG_TOOLS
debug_server_init(debug_port);
+ /* Private deterministic route gate: no TCP-upload timing in guest
+ * history. An explicit malformed route aborts before guest execution. */
+ if (const char *route = std::getenv("PSX_INPUT_ROUTE_FILE")) {
+ if (!route[0] || net_cfg.enabled ||
+ !debug_server_preload_input_route(route)) {
+ std::fprintf(stderr, "psxrecomp: prestart input route rejected\n");
+ debug_server_shutdown();
+ return 2;
+ }
+ }
#else
(void)debug_port;
+ if (std::getenv("PSX_INPUT_ROUTE_FILE")) {
+ std::fprintf(stderr, "psxrecomp: input routes require debug tools\n");
+ return 2;
+ }
#endif
#ifdef PSX_COSIM
cosim_init(); /* first-divergence oracle server */
diff --git a/runtime/src/memory.c b/runtime/src/memory.c
index 124a6c37c..c24ea9151 100644
--- a/runtime/src/memory.c
+++ b/runtime/src/memory.c
@@ -14,6 +14,7 @@
#include "dma.h"
#include "fntrace.h"
#include "gpu.h"
+#include "source_gpu_runtime.h"
#include "mdec.h"
#include "mod_memory.h"
#include "sio.h"
@@ -24,6 +25,7 @@
#include "dirty_ram_interp.h"
#include "guest_tty.h"
#include "psx_cycles.h"
+#include "psx_icache.h"
#include "starvation_ring.h"
#include
#include
@@ -114,9 +116,24 @@ static inline uint32_t psx_phys_addr(uint32_t addr) {
/* Expose RAM pointer for oracle comparison (find_first_divergence). */
uint8_t *memory_get_ram_ptr(void) { return ram; }
+int memory_peek_instruction_word(uint32_t address, uint32_t *value) {
+ if (!value || (address & 3u) || address >= 0xC0000000u) return 0;
+ uint32_t phys = address & 0x1FFFFFFFu;
+ const uint8_t *bytes;
+ if (phys < RAM_SIZE) bytes = ram + phys;
+ else if (phys >= 0x1FC00000u && phys < 0x1FC00000u + BIOS_ROM_SIZE)
+ bytes = bios_rom + (phys - 0x1FC00000u);
+ else return 0;
+ *value = (uint32_t)bytes[0] | ((uint32_t)bytes[1] << 8) |
+ ((uint32_t)bytes[2] << 16) | ((uint32_t)bytes[3] << 24);
+ return 1;
+}
uint8_t *memory_get_scratchpad_ptr(void) { return scratchpad; }
void memory_clear_low_boot_scratch(void) {
+ /* Game entry is a host bookkeeping event. The source profile preserves
+ * guest RAM across it, including BIOS scratch and trampoline words. */
+ if (source_gpu_runtime_active()) return;
memset(ram, 0, 0x10u);
}
@@ -1103,6 +1120,14 @@ static void unmapped_fatal(uint32_t vaddr, uint32_t phys, const char* op) {
/* --- MMIO read/write helpers --- */
+static uint32_t irq_read_image(uint32_t value) {
+ /* Retained Octoshock 2.2.2 IRQ_Read supplies these fixed upper bits
+ * before selecting the byte lane. This is source compatibility, not
+ * an emulation of a measured physical open bus. Keep stored IRQ state
+ * and the default runtime's read image unchanged. */
+ return value | (source_gpu_runtime_active() ? 0x1F800000u : 0u);
+}
+
static uint32_t mmio_read32_impl(uint32_t addr) {
SHADOW_NOTE_MMIO();
/* Memory control: 0x1F801000..0x1F801020 */
@@ -1118,8 +1143,8 @@ static uint32_t mmio_read32_impl(uint32_t addr) {
return ram_size_reg;
}
/* Interrupts: 0x1F801070, 0x1F801074 */
- if (addr == 0x1F801070u) { sio_tick(0); return i_stat; }
- if (addr == 0x1F801074u) return i_mask;
+ if (addr == 0x1F801070u) { sio_tick(0); return irq_read_image(i_stat); }
+ if (addr == 0x1F801074u) return irq_read_image(i_mask);
/* DMA: 0x1F801080..0x1F8010FF */
if (addr >= 0x1F801080u && addr <= 0x1F8010FFu) {
return dma_read(addr);
@@ -1241,14 +1266,17 @@ static uint16_t mmio_read16_impl(uint32_t addr) {
if (addr >= 0x1F801070u && addr <= 0x1F801072u) {
sio_tick(0);
uint32_t shift = (addr & 2u) ? 16u : 0u;
- return (uint16_t)(i_stat >> shift);
+ return (uint16_t)(irq_read_image(i_stat) >> shift);
}
if (addr >= 0x1F801074u && addr <= 0x1F801076u) {
uint32_t shift = (addr & 2u) ? 16u : 0u;
- return (uint16_t)(i_mask >> shift);
+ return (uint16_t)(irq_read_image(i_mask) >> shift);
}
/* Timers: 0x1F801100..0x1F80112F */
if (addr >= 0x1F801100u && addr <= 0x1F80112Fu) {
+ if(source_gpu_runtime_active() && timers_source_raster_enabled() &&
+ addr>=0x1f801110u && addr<=0x1f80111fu)
+ return (uint16_t)(timers_read(addr&~3u) >> (8u*(addr&3u)));
return (uint16_t)timers_read(addr);
}
/* DMA: 0x1F801080..0x1F8010FF */
@@ -1345,6 +1373,11 @@ static void mmio_write16(uint32_t addr, uint16_t val) {
static uint8_t mmio_read8_impl(uint32_t addr) {
SHADOW_NOTE_MMIO();
+ /* The source Timer1 read decodes the register before selecting its lane.
+ * Reading an upper mode lane still performs the mode read-to-clear. */
+ if(source_gpu_runtime_active() && timers_source_raster_enabled() &&
+ addr>=0x1f801110u && addr<=0x1f80111fu)
+ return (uint8_t)(timers_read(addr&~3u) >> (8u*(addr&3u)));
/* Memory control: 0x1F801000..0x1F80103C — byte lane of the 32-bit reg. */
if (addr >= 0x1F801000u && addr <= 0x1F80103Fu) {
uint32_t v = mem_ctrl[(addr - 0x1F801000u) >> 2];
@@ -1357,7 +1390,7 @@ static uint8_t mmio_read8_impl(uint32_t addr) {
if (addr >= 0x1F801070u && addr <= 0x1F801077u) {
if (addr < 0x1F801074u) sio_tick(0);
uint32_t val = (addr < 0x1F801074u) ? i_stat : i_mask;
- return (uint8_t)(val >> (8 * (addr & 3)));
+ return (uint8_t)(irq_read_image(val) >> (8 * (addr & 3)));
}
/* SIO: 0x1F801040..0x1F80104F */
if (addr >= 0x1F801040u && addr <= 0x1F80104Fu) {
@@ -1628,6 +1661,11 @@ void psx_write_word(uint32_t addr, uint32_t val) {
}
static void psx_write_word_raw(uint32_t addr, uint32_t val) {
g_guest_store_count++;
+ /* Isolated stores target the cache before memory-address routing. */
+ if (sr_ptr && (*sr_ptr & 0x10000u)) {
+ psx_icache_isolated_store(addr, cache_ctrl);
+ return;
+ }
/* (pgxp) plain-store shadow invalidation retired: the PGXP engine
* validates tracked words against the actual packet word on read, so an
* overwritten word can never be believed (ENHANCEMENTS.md G1). */
@@ -1636,25 +1674,16 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) {
/* KSEG2 guard — see psx_read_word_raw. */
if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; }
- /* IsC (Isolate Cache): when set, writes go to D-cache only.
- * We have no cache model, so silently discard RAM/scratchpad writes. */
- if (sr_ptr && (*sr_ptr & 0x10000u)) return;
-
uint32_t phys = psx_phys_addr(addr);
- /* The generated BIOS mirrors its exception trampoline to 0x80000000 during
- * boot. On hardware this mirror copy is not visible in RAM; only the real
- * exception vector at 0x80000080 is. Tomba 2 later passes buffer=0 to the
- * BIOS card write routine, so stale mirror bytes at 0 corrupt the sector 63
- * management write and leave the load menu stuck checking the card. */
- if (fntrace_is_game_started() &&
+ /* Legacy Tomba 2 card-buffer workaround. Source execution writes ordinary
+ * RAM here, so the source profile must not suppress the guest store. */
+ if (!source_gpu_runtime_active() && fntrace_is_game_started() &&
phys < 0x10u && g_debug_last_store_pc == 0xBFC10A00u) return;
- /* BIOS helpers use RAM address zero as a tiny delay-loop scratch between
- * device-register polls. Treat these two dummy stores as non-visible; real
- * hardware / Beetle preserve Tomba 2's sector-63 card-management payload
- * when it passes buffer=0 to _card_write. */
- if (fntrace_is_game_started() && phys == 0u) {
+ /* Retain the legacy card-buffer store filter outside the source profile.
+ * BIOS delay-loop scratch stores are observable guest RAM writes. */
+ if (!source_gpu_runtime_active() && fntrace_is_game_started() && phys == 0u) {
switch (g_debug_last_store_pc) {
case 0xBFC04E90u:
case 0xBFC04EF0u:
@@ -1783,7 +1812,9 @@ static uint16_t psx_read_half_raw(uint32_t addr) {
uint32_t phys = psx_phys_addr(addr);
if (phys < RAM_SIZE) {
- return (uint16_t)ram[phys] | ((uint16_t)ram[phys + 1] << 8);
+ uint16_t v = (uint16_t)ram[phys] | ((uint16_t)ram[phys + 1] << 8);
+ if (g_ram_read_watch_active) debug_server_trace_ram_read_watch(phys, v);
+ return v;
}
{
uint32_t off;
@@ -1830,7 +1861,10 @@ void psx_write_half(uint32_t addr, uint16_t val) {
}
static void psx_write_half_raw(uint32_t addr, uint16_t val) {
g_guest_store_count++;
- if (sr_ptr && (*sr_ptr & 0x10000u)) return;
+ if (sr_ptr && (*sr_ptr & 0x10000u)) {
+ psx_icache_isolated_store(addr, cache_ctrl);
+ return;
+ }
/* KSEG2 guard — see psx_read_word_raw. */
if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; }
@@ -1902,7 +1936,9 @@ static uint8_t psx_read_byte_raw(uint32_t addr) {
uint32_t phys = psx_phys_addr(addr);
if (phys < RAM_SIZE) {
- return ram[phys];
+ uint8_t v = ram[phys];
+ if (g_ram_read_watch_active) debug_server_trace_ram_read_watch(phys, v);
+ return v;
}
{
uint32_t off;
@@ -1947,7 +1983,9 @@ static uint8_t psx_read_byte_raw(uint32_t addr) {
* (libretro.cpp:868-869). That is non-zero only while a DMA channel is actively
* stealing the bus; modeling it needs the live steal count threaded out of the DMA
* controller, and it can't be isolated by a static ruler. It remains an unmodeled
- * dynamic axis; the per-region device waits below are the static, validatable piece. */
+ * dynamic axis by default. The optional source VRAM-upload DMA profile now supplies
+ * that live count for its bounded request-mode context. It is sampled before the
+ * read charge dispatches device events, and joins the absorbed region cost. */
/* Runtime-only production cycle charge for data-load timing. Overlay DLLs
* flush their local pending-cycle accumulator before entering these host
@@ -2010,8 +2048,8 @@ static inline uint32_t psx_mmio_read_wait(uint32_t phys, uint32_t size) {
/* Beetle ReadMemory data-access timing (cpu.cpp:369-448), after §1/deps/DO_LDS.
* compl_cost = 2 (CPU load) / 1 (LWC2); arm_rt = GPR to arm as pending load, or
* 0x20 = none (LWC2, dest is a GTE reg). size = access width in bytes (1/2/4). */
-static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, uint32_t size,
- uint32_t compl_cost, uint32_t arm_rt) {
+static inline uint32_t psx_cyc_readmem_prepare(CPUState* cpu, uint32_t phys, uint32_t size,
+ uint32_t compl_cost, uint32_t arm_rt, int defer_gpu_wait) {
/* ReadMemory start (369-370): clear the current give-back slot. */
cpu->read_absorb[cpu->read_absorb_which] = 0u;
cpu->read_absorb_which = 0u;
@@ -2019,16 +2057,29 @@ static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, uint32_t size,
if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) {
cpu->ld_absorb = 0u;
cpu->ld_which_t = (uint8_t)arm_rt;
- return;
+ return 0u;
}
- /* fudge (424): +2 iff the predecessor committed no load (read_fudge==0x20).
- * Combined with region+completion into one advance — deadline catch-up
- * replays exact event boundaries, so splitting the charge is only host cost. */
- uint32_t region = psx_mmio_read_wait(phys, size); /* device-region wait */
+ /* The device supplies its value after the bus wait, before the CPU load
+ * completion cycles. Advancing through completion before an MMIO read can
+ * sample the next timer tick or consume a newly arrived device response.
+ * Preserve the existing RAM/ROM path; its DMA/read-order coverage is separate. */
+ uint32_t region = psx_mmio_read_wait(phys, size) + dma_cpu_read_penalty();
uint32_t cost = region + compl_cost; /* LDAbsorb = region + completion */
uint32_t fudge = (uint32_t)((cpu->read_fudge >> 4) & 2u);
cpu->ld_absorb = cost;
- psx_advance_cycles(fudge + cost);
+ uint32_t completion = (phys >= 0x1F801000u && phys <= 0x1F802FFFu)
+ ? compl_cost : 0u;
+ /* Source MemRW services events after ReadFudge and DMA steal, before
+ * the device's one-cycle region wait. That final wait does not service again.
+ * Returning it with completion keeps the value/GPUREAD side effect ahead
+ * of an event crossed only by that wait, while earlier events stay visible.
+ * HBlank-driven Timer1 counters likewise have no CPU tick in that wait;
+ * TIMER_Read updates timers, but does not service a newly due GPU event.
+ * DMA_Read and IRQ_Read likewise return stored registers without another
+ * event update. IRQ status must not include an edge crossed by that wait. */
+ if(defer_gpu_wait)completion+=psx_mmio_read_wait(phys,size);
+ psx_advance_cycles(fudge + cost - completion);
+ if(defer_gpu_wait)psx_devices_service_to_now();
cpu->ld_which_t = (uint8_t)arm_rt;
/* PROOF GATE (PSX_POLL_PROOF=N, default 0/off): a FLAT, non-absorbed extra N
* cycles per main-RAM data read — replicates the historical "+6 cyc/main-RAM
@@ -2043,6 +2094,14 @@ static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, uint32_t size,
if (s_pp < 0) { const char* e = getenv("PSX_POLL_PROOF"); s_pp = (e && e[0]) ? atoi(e) : 0; }
if (s_pp > 0) psx_load_charge_cycles((uint32_t)s_pp);
}
+ return completion;
+}
+
+/* Timing-only callers still consume the complete load duration. */
+static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, uint32_t size,
+ uint32_t compl_cost, uint32_t arm_rt) {
+ uint32_t completion = psx_cyc_readmem_prepare(cpu, phys, size, compl_cost, arm_rt, 0);
+ if (completion) psx_advance_cycles(completion);
}
/* Resolve PSX_LOAD_DELAY once (shared with inlined psx_cyc.h helpers). */
@@ -2056,23 +2115,24 @@ int psx_load_delay_enabled(void) {
/* The interlock half of a load (§1+deps+(cancel)+DO_LDS+ReadMemory). Gated on
* PSX_ENABLE_BLOCK_CYCLES so the Beetle-oracle build (cycles off) does a plain read. */
-static inline void psx_cyc_load_timing(CPUState* cpu, uint32_t addr, uint32_t size,
- uint32_t rt, uint32_t reg_mask) {
+static inline uint32_t psx_cyc_load_timing(CPUState* cpu, uint32_t addr, uint32_t size,
+ uint32_t rt, uint32_t reg_mask, int defer_gpu_wait) {
#ifdef PSX_ENABLE_BLOCK_CYCLES
/* Bisect gate (PSX_LOAD_DELAY=0): disable the R3000A load-delay interlock
* timing (the d8c4a8e/fade560/d597797 feature) to test whether it moves the
* MMX6 cutscene ordering. Read once; default on. */
if (!psx_load_delay_enabled()) {
(void)addr; (void)size; (void)rt; (void)reg_mask;
- return;
+ return 0u;
}
psx_cyc_base(cpu);
psx_cyc_deps(cpu, reg_mask);
if (cpu->ld_which_t == rt) cpu->ld_which_t = 0u; /* cancel pending load to same dest */
psx_cyc_lds(cpu);
- psx_cyc_readmem(cpu, addr & 0x1FFFFFFFu, size, 2u, rt);
+ return psx_cyc_readmem_prepare(cpu, addr & 0x1FFFFFFFu, size, 2u, rt, defer_gpu_wait);
#else
(void)cpu; (void)addr; (void)size; (void)rt; (void)reg_mask;
+ return 0u;
#endif
}
@@ -2104,33 +2164,64 @@ static inline int psx_cyc_main_ram_fast_addr(uint32_t addr, uint32_t width,
#endif
/* Slow paths for the inlined helpers in psx_cyc.h (MMIO / lockstep / shards). */
+static int source_hblank_counter_sample(uint32_t addr) {
+ return source_gpu_runtime_active() && addr<0xc0000000u &&
+ timers_source_hblank_counter_read(addr&0x1fffffffu);
+}
+static int source_dma_register_sample(uint32_t addr) {
+ uint32_t physical=addr&0x1fffffffu;
+ return source_gpu_runtime_active() && addr<0xc0000000u &&
+ physical>=0x1f801080u && physical<=0x1f8010ffu;
+}
+static int source_irq_register_sample(uint32_t addr) {
+ uint32_t physical=addr&0x1fffffffu;
+ return source_gpu_runtime_active() && addr<0xc0000000u &&
+ physical>=0x1f801070u && physical<=0x1f801077u;
+}
uint32_t psx_cyc_load_word_slow(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) {
- psx_cyc_load_timing(cpu, addr, 4u, rt, reg_mask);
- return psx_read_word(addr);
+ uint32_t physical=addr&0x1fffffffu;
+ int source_gpu_sample=source_gpu_runtime_active() && addr<0xc0000000u &&
+ (physical==0x1f801810u || physical==0x1f801814u);
+ uint32_t completion=psx_cyc_load_timing(cpu,addr,4u,rt,reg_mask,
+ source_gpu_sample || source_hblank_counter_sample(addr) ||
+ source_dma_register_sample(addr) || source_irq_register_sample(addr));
+ uint32_t value = psx_read_word(addr);
+ if (completion) psx_advance_cycles(completion);
+ return value;
}
uint16_t psx_cyc_load_half_slow(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) {
- psx_cyc_load_timing(cpu, addr, 2u, rt, reg_mask);
- return psx_read_half(addr);
+ uint32_t completion = psx_cyc_load_timing(cpu, addr, 2u, rt, reg_mask,
+ source_hblank_counter_sample(addr) || source_dma_register_sample(addr) ||
+ source_irq_register_sample(addr));
+ uint16_t value = psx_read_half(addr);
+ if (completion) psx_advance_cycles(completion);
+ return value;
}
void psx_cyc_load_word_timing_only(CPUState* cpu, uint32_t addr,
uint32_t rt, uint32_t reg_mask) {
- psx_cyc_load_timing(cpu, addr, 4u, rt, reg_mask);
+ uint32_t completion = psx_cyc_load_timing(cpu, addr, 4u, rt, reg_mask, 0);
+ if (completion) psx_advance_cycles(completion);
}
uint8_t psx_cyc_load_byte(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) {
- psx_cyc_load_timing(cpu, addr, 1u, rt, reg_mask);
+ uint32_t completion = psx_cyc_load_timing(cpu, addr, 1u, rt, reg_mask,
+ source_hblank_counter_sample(addr) || source_dma_register_sample(addr) ||
+ source_irq_register_sample(addr));
#if defined(PSX_NO_DEBUG_TOOLS) && !defined(PSX_COSIM)
uint32_t phys;
if (psx_cyc_main_ram_fast_addr(addr, 1u, &phys)) return ram[phys];
#endif
- return psx_read_byte(addr);
+ uint8_t value = psx_read_byte(addr);
+ if (completion) psx_advance_cycles(completion);
+ return value;
}
/* LWC2 (GTE load): §1/DO_LDS done by psx_cyc_step(cpu,0); the GTE deadline stall by
* psx_gte_stall — both emitted before this call. 32-bit access, completion +1, no
* LDWhich arm. */
uint32_t psx_cyc_lwc2_read(CPUState* cpu, uint32_t addr) {
+ uint32_t completion = 0u;
#ifdef PSX_ENABLE_BLOCK_CYCLES
- psx_cyc_readmem(cpu, addr & 0x1FFFFFFFu, 4u, 1u, 0x20u);
+ completion = psx_cyc_readmem_prepare(cpu, addr & 0x1FFFFFFFu, 4u, 1u, 0x20u, 0);
#else
(void)cpu;
#endif
@@ -2143,7 +2234,9 @@ uint32_t psx_cyc_lwc2_read(CPUState* cpu, uint32_t addr) {
| ((uint32_t)ram[phys + 3] << 24);
}
#endif
- return psx_read_word(addr);
+ uint32_t value = psx_read_word(addr);
+ if (completion) psx_advance_cycles(completion);
+ return value;
}
/* Deprecated uncharged passthroughs (the +4 flat wait-state model is gone; load
@@ -2165,7 +2258,10 @@ void psx_write_byte(uint32_t addr, uint8_t val) {
}
static void psx_write_byte_raw(uint32_t addr, uint8_t val) {
g_guest_store_count++;
- if (sr_ptr && (*sr_ptr & 0x10000u)) return;
+ if (sr_ptr && (*sr_ptr & 0x10000u)) {
+ psx_icache_isolated_store(addr, cache_ctrl);
+ return;
+ }
/* KSEG2 guard — see psx_read_word_raw. */
if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; }
diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c
index 6a0ca6864..97f9fdf9e 100644
--- a/runtime/src/overlay_loader.c
+++ b/runtime/src/overlay_loader.c
@@ -2273,6 +2273,8 @@ static void init_callbacks(void) {
extern uint8_t psx_cyc_load_byte(CPUState*, uint32_t, uint32_t, uint32_t);
extern uint32_t psx_cyc_lwc2_read(CPUState*, uint32_t);
extern void psx_icache_fetch_fn(CPUState*, uint32_t);
+ extern int psx_cpu_step_boundary_enabled(int);
+ extern void psx_cpu_step_boundary_fn(CPUState*, uint32_t);
extern void psx_muldiv_set(CPUState*, uint32_t);
extern void psx_muldiv_stall(CPUState*);
extern uint32_t psx_mult_latency_s(uint32_t);
@@ -2285,6 +2287,8 @@ static void init_callbacks(void) {
s_callbacks.cyc_load_byte = psx_cyc_load_byte;
s_callbacks.cyc_lwc2_read = psx_cyc_lwc2_read;
s_callbacks.icache_fetch = psx_icache_fetch_fn;
+ s_callbacks.cpu_step_boundary_enabled = psx_cpu_step_boundary_enabled;
+ s_callbacks.cpu_step_boundary = psx_cpu_step_boundary_fn;
s_callbacks.muldiv_set = psx_muldiv_set;
s_callbacks.muldiv_stall = psx_muldiv_stall;
s_callbacks.mult_latency_s = psx_mult_latency_s;
diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c
index 1057093f4..47f8e3ebf 100644
--- a/runtime/src/psx_cycles.c
+++ b/runtime/src/psx_cycles.c
@@ -13,6 +13,7 @@
#include "sio.h"
#include "starvation_ring.h"
#include "timers.h"
+#include "source_gpu_runtime.h"
#if defined(PSX_HAS_RECOMP_NET)
#include "psx_netplay.h"
#endif
@@ -77,6 +78,7 @@ static void advance_devices(uint32_t c) {
psx_cycle_count += (uint64_t)c;
sio_advance(c);
cdrom_advance(c);
+ source_gpu_runtime_advance();
dma_advance(c);
timers_advance(c);
interrupts_advance_cycles(c);
@@ -139,6 +141,7 @@ static uint32_t devices_cycles_to_next_internal_event(void) {
uint32_t t = timers_cycles_to_irq(0xFFFFFFFFu); if (t < best) best = t;
uint32_t c = cdrom_cycles_to_irq(0xFFFFFFFFu); if (c < best) best = c;
uint32_t d = dma_cycles_to_internal_event(); if (d < best) best = d;
+ uint32_t g = source_gpu_runtime_cycles_to_event(); if (g < best) best = g;
uint32_t s = sio_cycles_to_irq(0xFFFFFFFFu); if (s < best) best = s;
uint32_t a = psx_spu_sample_event_cycles_to_next(); if (a < best) best = a;
if (best == 0) best = 1; /* due/overdue: process within one cycle */
@@ -674,10 +677,16 @@ uint32_t psx_mult_latency_u(uint32_t rs) { /* MULTU (unsigned) */
/* DIV/DIVU latency is the fixed constant 37 — emitted directly at the op site. */
void psx_muldiv_set(CPUState* cpu, uint32_t latency) {
+ /* The deadline belongs to this instruction, including unpublished CPU
+ * work from generated blocks and local charge accumulators. */
+ psx_cyc_batch_flush();
cpu->muldiv_ts_done = psx_cycle_count + (uint64_t)latency;
}
void psx_muldiv_stall(CPUState* cpu) {
+ /* Publish once before comparing; otherwise advance would add the pending
+ * work a second time on top of a stall computed from a stale clock. */
+ psx_cyc_batch_flush();
/* MFLO/MFHI stall to the mult/div completion deadline (Beetle cpu.cpp:1723-1736).
* While stalling it CONSUMES a pending load-delay give-back (read_absorb) — each
* stalled cycle decrements read_absorb[read_absorb_which] — so cycles that would
diff --git a/runtime/src/psx_icache.c b/runtime/src/psx_icache.c
index 5ba898a34..d84952e91 100644
--- a/runtime/src/psx_icache.c
+++ b/runtime/src/psx_icache.c
@@ -11,6 +11,19 @@
#include
#include
#include
+int g_input_instruction_histogram_active;
+void (*g_psx_cpu_step_boundary_callback)(CPUState *,uint32_t,uint64_t);
+int psx_cpu_step_boundary_enabled(int include_replay) {
+ return g_psx_cpu_step_boundary_callback != NULL &&
+ (include_replay || !g_ls_replay_active);
+}
+void psx_cpu_step_boundary_fn(CPUState *cpu,uint32_t address) {
+ psx_cpu_step_boundary(cpu,address);
+}
+void (*g_input_instruction_histogram_callback)(uint32_t pc);
+void input_instruction_histogram_sample(uint32_t pc) {
+ if(g_input_instruction_histogram_callback)g_input_instruction_histogram_callback(pc);
+}
int psx_icache_enabled(void) {
static int s = -1;
@@ -67,6 +80,18 @@ void psx_icache_reset(void) {
for (int i = 0; i < 1024; i++) g_psx_icache_tv[i] = 0x1u;
}
+void psx_icache_isolated_store(uint32_t addr, uint32_t cache_control) {
+ /* PSX-SPX Memory Control: IsC + I-cache enable + tag-test mode flushes
+ * the indexed 16-byte line. The original Octoshock 2.2.2 WriteMemory
+ * comparison invalidates all four words, independent of store width/value.
+ * Keep the existing tag representation (bit 1 means invalid). This is a
+ * timing-tag correction; it does not add cache data execution semantics. */
+ if ((cache_control & 0x804u) != 0x804u) return;
+ if (g_ls_replay_active && s_icache_shadow_state != 2) return;
+ uint32_t first = (addr & 0xFF0u) >> 2;
+ for (unsigned i = 0; i < 4; i++) g_psx_icache_tv[first + i] = 0x2u;
+}
+
void psx_icache_fetch_miss(CPUState* cpu, uint32_t addr) {
{ extern int g_ls_replay_active;
/* Ordinary lockstep replay must not perturb the shared cache. The whole-
@@ -108,6 +133,8 @@ void psx_icache_fetch_miss(CPUState* cpu, uint32_t addr) {
}
void psx_icache_fetch(CPUState* cpu, uint32_t addr) {
+ psx_cpu_step_boundary(cpu,addr);
+ if (g_input_instruction_histogram_active) input_instruction_histogram_sample(addr);
psx_icache_fetch_miss(cpu, addr);
}
diff --git a/runtime/src/sio.c b/runtime/src/sio.c
index 08dcadd09..7788d18a8 100644
--- a/runtime/src/sio.c
+++ b/runtime/src/sio.c
@@ -488,7 +488,10 @@ static void card_handoff_push(uint8_t kind, uint8_t byte) {
* (bit31 / 0xFFFFFFFF) after pop. Merged IRQ7 edges leave depth≥1 so one
* pop lands on 0 and skips B4E38 — guest never re-arms bit7 / directory.
* Repair = re-edge IRQ7 only (no host B4E* / A6C10 stores).
- * Default ON; PSX_APE_CARD_UNSTICK=0 disables. */
+ * PSX-COMPAT-001: fixed Ape RAM addresses are not a device invariant.
+ * Unrelated data in Tekken triggered IRQ7 and changed I_MASK after an
+ * absent-card probe. Keep this historical repair explicit opt-in only:
+ * PSX_APE_CARD_UNSTICK=1. It is not enabled by ordinary controller/card use. */
static int s_ape_unstick_env = -1;
static int s_ape_unstick_pending = 0;
static uint64_t s_ape_unstick_cool_cyc = 0;
@@ -497,10 +500,7 @@ static int s_ape_torn_pulses = 0;
static int ape_unstick_enabled(void) {
if (s_ape_unstick_env < 0) {
const char *e = getenv("PSX_APE_CARD_UNSTICK");
- if (e && e[0] == '0')
- s_ape_unstick_env = 0;
- else
- s_ape_unstick_env = 1;
+ s_ape_unstick_env = (e && e[0] == '1') ? 1 : 0;
}
return s_ape_unstick_env;
}
@@ -637,6 +637,11 @@ volatile int g_sio_timing_active = 0;
#if SIO_MODEL_CYCLE_PACED
#define SIO_BAUD_CYCLES_DEFAULT 1088
#define SIO_ACK_CYCLES_DEFAULT 170
+/* Explicit digital-pad source compatibility, not a hardware/default profile.
+ * Original 2.2.2 gamepad.cpp requests delay 64; frontio.cpp exposes 32 clocks. */
+static int sio_source_pad_ack;
+static int sio_ack_pulse_remaining;
+static int sio_pending_ack_timed;
static int sio_tick_quantum_cycles = 64;
static int sio_shift_active = 0;
static uint8_t sio_shift_byte = 0;
@@ -934,6 +939,21 @@ static int sio_ack_visible_reads = 0;
#define SIO_CTRL_SLOT (1 << 13)
void sio_init(void) {
+#if SIO_MODEL_CYCLE_PACED
+ const char *ack_model = getenv("PSX_INPUT_ROUTE_PAD_ACK_MODEL");
+ sio_source_pad_ack = ack_model && strcmp(ack_model, "octoshock-2.2.2-digital") == 0;
+ if (ack_model && *ack_model && !sio_source_pad_ack) {
+ fprintf(stderr, "Unsupported input route pad ACK model: %s\n", ack_model);
+ abort();
+ }
+ if (sio_source_pad_ack && getenv("PSX_SIO_PAD_SYNC_RX") &&
+ getenv("PSX_SIO_PAD_SYNC_RX")[0] == '1') {
+ fprintf(stderr, "Source pad ACK timing requires cycle-paced RX\n");
+ abort();
+ }
+ sio_ack_pulse_remaining = 0;
+ sio_pending_ack_timed = 0;
+#endif
sio_tx_data = 0;
sio_rx_data = 0xFF;
sio_stat = SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
@@ -1415,7 +1435,7 @@ static void pad_process_byte(uint8_t tx_byte) {
if (tx_byte == 0x42) {
/* Read poll. Analog (or in-config) uses the 8-byte format with the
* four stick axes; a plain digital pad uses the 4-byte format. */
- const uint16_t btn = pad_buttons[lp];
+ const uint16_t btn = debug_server_update_poll(lp, pad_buttons[lp], pad_analog[lp]);
pad_response[0] = cur_id;
pad_response[1] = 0x5A;
pad_response[2] = (uint8_t)(btn & 0xFF);
@@ -2308,7 +2328,12 @@ void sio_write(uint32_t addr, uint32_t value) {
sr_record(SR_EVT_CTRL_WRITE, (uint8_t)(value & 0xFF), (uint8_t)((value >> 8) & 0xFF));
if (value & SIO_CTRL_ACK) {
sio_stat &= ~SIO_STAT_IRQ;
- sio_stat &= ~SIO_STAT_ACK;
+ /* Original source clears the IRQ latch, not the DSR pulse. Its
+ * optional level-sensitive reassert-on-clear is disabled. */
+#if SIO_MODEL_CYCLE_PACED
+ if (!sio_ack_pulse_remaining)
+#endif
+ sio_stat &= ~SIO_STAT_ACK;
sio_ack_visible_reads = 0;
}
if (value & SIO_CTRL_RESET) {
@@ -2337,6 +2362,8 @@ void sio_write(uint32_t addr, uint32_t value) {
sio_irq_countdown = 0;
sio_ack_visible_reads = 0;
#if SIO_MODEL_CYCLE_PACED
+ sio_ack_pulse_remaining = 0; sio_pending_ack_timed = 0;
+ if (sio_source_pad_ack) sio_stat &= ~SIO_STAT_ACK;
sio_shift_active = 0; sio_shift_remaining = 0;
sio_tx_buffered = 0;
sio_shift_ack_irq_en = 0; sio_tx_buffer_ack_irq_en = 0;
@@ -2368,7 +2395,7 @@ void sio_write(uint32_t addr, uint32_t value) {
sio_bus_owner = SIO_OWNER_NONE;
sio_bus_byte_index = 0;
}
- g_sio_timing_active = 0;
+ g_sio_timing_active = sio_ack_pulse_remaining > 0;
sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
}
}
@@ -2451,6 +2478,8 @@ void sio_write(uint32_t addr, uint32_t value) {
pad_current_cmd = 0;
active_device = DEV_NONE;
#if SIO_MODEL_CYCLE_PACED
+ sio_ack_pulse_remaining = 0; sio_pending_ack_timed = 0;
+ if (sio_source_pad_ack) sio_stat &= ~SIO_STAT_ACK;
sio_shift_active = 0; sio_shift_remaining = 0;
sio_tx_buffered = 0;
sio_shift_ack_irq_en = 0; sio_tx_buffer_ack_irq_en = 0;
@@ -2636,7 +2665,11 @@ static void sio_fire_ack_irq(void) {
active_device == DEV_MEMCARD);
sio_stat |= SIO_STAT_ACK;
- sio_ack_visible_reads = 2;
+ sio_ack_visible_reads = sio_pending_ack_timed ? 0 : 2;
+ if (sio_pending_ack_timed) {
+ sio_ack_pulse_remaining = 32;
+ g_sio_timing_active = 1;
+ }
sr_record(SR_EVT_ACK_FIRE, 0, 0);
int irq_enabled = sio_pending_ack_irq_en
|| ((sio_ctrl & SIO_CTRL_ACK_IRQ_EN) ? 1 : 0);
@@ -2689,7 +2722,8 @@ static void sio_fire_ack_irq(void) {
sio_irq_idx = (sio_irq_idx + 1) % SIO_IRQ_RING_CAP;
sio_irq_seq++;
s_pace_ack_fires++;
- if (!sio_shift_active && !sio_tx_buffered && !sio_pending_ack) {
+ if (!sio_shift_active && !sio_tx_buffered && !sio_pending_ack &&
+ !sio_ack_pulse_remaining) {
g_sio_timing_active = 0;
}
}
@@ -2709,13 +2743,17 @@ static void sio_handle_shift_complete(void) {
sio_irq_pending_source = (active_device == DEV_MEMCARD)
? SIO_IRQ_SRC_CARD_ACK : SIO_IRQ_SRC_PAD_ACK;
sio_irq_pending_slot = (uint8_t)selected_slot;
- sio_irq_pending_delay = (uint8_t)SIO_ACK_CYCLES_DEFAULT;
+ int timed_pad = sio_source_pad_ack && active_device == DEV_PAD &&
+ !pad_analog[pad_active_logical] && !sio_multitap_active();
+ int ack_delay = timed_pad ? 64 : SIO_ACK_CYCLES_DEFAULT;
+ sio_irq_pending_delay = (uint8_t)ack_delay;
sio_irq_pending_mc_state = (uint8_t)mc_state;
sio_irq_pending_byte_seq = sio_trace_seq;
if (acked) {
sio_pending_ack = 1;
- sio_ack_remaining = SIO_ACK_CYCLES_DEFAULT;
+ sio_ack_remaining = ack_delay;
+ sio_pending_ack_timed = timed_pad;
sio_pending_ack_irq_en = sio_shift_ack_irq_en;
}
@@ -2734,7 +2772,8 @@ static void sio_handle_shift_complete(void) {
} else {
sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
}
- if (!sio_shift_active && !sio_tx_buffered && !sio_pending_ack) {
+ if (!sio_shift_active && !sio_tx_buffered && !sio_pending_ack &&
+ !sio_ack_pulse_remaining) {
g_sio_timing_active = 0;
}
}
@@ -2765,7 +2804,10 @@ uint64_t sio_get_advance_with_work(void) { return s_sio_advance_with_work; }
static void sio_pace_walk(int cycles) {
int remaining = cycles;
int transitions = 0;
- const int MAX_TRANSITIONS = 1;
+ /* Preserve the legacy card walker. The source-pad experiment consumes
+ * the whole supplied interval across shift, ACK-on and ACK-off edges. */
+ const int MAX_TRANSITIONS = sio_source_pad_ack &&
+ (sio_bus_owner == SIO_OWNER_PAD || sio_ack_pulse_remaining) ? 32 : 1;
while (transitions < MAX_TRANSITIONS &&
(remaining > 0 ||
(sio_shift_active && sio_shift_remaining <= 0) ||
@@ -2783,6 +2825,12 @@ static void sio_pace_walk(int cycles) {
dt = sio_ack_remaining;
next_event = 1;
}
+ if (sio_ack_pulse_remaining > 0 &&
+ (sio_ack_pulse_remaining < dt ||
+ (sio_ack_pulse_remaining == dt && next_event < 0))) {
+ dt = sio_ack_pulse_remaining;
+ next_event = 2;
+ }
if (sio_shift_active && sio_shift_remaining <= 0) {
dt = 0; next_event = 0;
} else if (sio_pending_ack && sio_ack_remaining <= 0) {
@@ -2790,6 +2838,10 @@ static void sio_pace_walk(int cycles) {
}
if (sio_shift_active) sio_shift_remaining -= dt;
if (sio_pending_ack) sio_ack_remaining -= dt;
+ if (sio_ack_pulse_remaining > 0) {
+ sio_ack_pulse_remaining -= dt;
+ if (!sio_ack_pulse_remaining) sio_stat &= ~SIO_STAT_ACK;
+ }
remaining -= dt;
if (next_event == 0) {
sio_handle_shift_complete();
@@ -2798,10 +2850,15 @@ static void sio_pace_walk(int cycles) {
sio_pending_ack = 0;
sio_fire_ack_irq();
transitions++;
+ } else if (next_event == 2) {
+ sio_stat &= ~SIO_STAT_ACK;
+ transitions++;
} else {
break;
}
}
+ if (sio_source_pad_ack && !sio_shift_active && !sio_tx_buffered &&
+ !sio_pending_ack && !sio_ack_pulse_remaining) g_sio_timing_active = 0;
}
void sio_advance(uint32_t cycles) {
@@ -3137,6 +3194,9 @@ static int sio_snap_parse(PstR *r) {
}
uint32_t sio_snapshot_bytes(void) {
+#if SIO_MODEL_CYCLE_PACED
+ if (sio_source_pad_ack) return 0; /* explicit cold-boot experiment only */
+#endif
PstW w;
pst_w_init(&w, NULL, 0);
(void)sio_snap_emit(&w);
@@ -3146,6 +3206,7 @@ uint32_t sio_snapshot_bytes(void) {
void sio_snapshot_write(uint8_t *p) {
PstW w;
uint32_t n = sio_snapshot_bytes();
+ if (!n) return;
pst_w_init(&w, p, n);
(void)sio_snap_emit(&w);
}
@@ -3153,6 +3214,7 @@ void sio_snapshot_write(uint8_t *p) {
int sio_snapshot_read(const uint8_t *p, uint32_t len) {
PstR r;
const uint32_t current = sio_snapshot_bytes();
+ if (!current) return 0;
const uint32_t rumble_bytes = (uint32_t)(sizeof(pad_rumble_map) +
sizeof(pad_rumble_small) +
sizeof(pad_rumble_large));
diff --git a/runtime/src/source_gpu_runtime.c b/runtime/src/source_gpu_runtime.c
new file mode 100644
index 000000000..5c3ecdec3
--- /dev/null
+++ b/runtime/src/source_gpu_runtime.c
@@ -0,0 +1,128 @@
+#include "source_gpu_runtime.h"
+#include "psx_icache.h"
+#include "psx_cycles.h"
+#include "dma.h"
+#include "source_cpu_boundary_probe.h"
+#include "source_ram_page_probe.h"
+#include
+#include
+extern uint64_t g_psx_cycle_fast_limit;
+static int enabled;
+static SourceGPUServiceClock clock_state;
+static SourceGPUCommandProjection command_state;
+/* GPU Update consumes queued work before it advances raster state. The
+ * event clock also predicts intermediate deadlines; draw parity must instead
+ * follow only actual GPU service calls, plus ordinary GP1 register writes. */
+static InputRouteRasterClock draw_raster;
+static SourceGPUServiceClock return_clock;
+static SourceGPUCommandProjection return_command;
+static SourceGPUDispatchSink dispatch_sink;
+void source_gpu_runtime_set_dispatch_sink(SourceGPUDispatchSink sink) {dispatch_sink=sink;}
+static void dispatch(void) {
+ if(command_state.dispatch.kind && dispatch_sink)
+ command_state.budget-=dispatch_sink(&command_state.dispatch);
+ command_state.dispatch.kind=SOURCE_GPU_DISPATCH_NONE;
+}
+static void fail(const char *reason) {
+ fprintf(stderr,"[source-gpu-service] rejected draw mode=%04X (draw-to-display=%u)\n",command_state.draw_mode,(command_state.draw_mode>>10)&1u);
+ if(command_state.count>=4 && command_state.queue[0]>>24==0x28) {
+ fprintf(stderr,"[source-gpu-service] rejected first triangle vertices");
+ for(unsigned i=1;i<4;i++)fprintf(stderr," %d,%d",source_gpu_command_coord(command_state.queue[i],0)+command_state.offset_x,source_gpu_command_coord(command_state.queue[i],16)+command_state.offset_y);
+ fputc('\n',stderr);
+ }
+ fprintf(stderr,"[source-gpu-service] %s at %llu; budget=%d phase=%u queued=%u front=%08X clip=%d,%d..%d,%d offset=%d,%d mode=%02X mask=%u\n",reason,(unsigned long long)psx_get_cycle_count(),command_state.budget,command_state.phase,command_state.count,command_state.count?command_state.queue[0]:0,command_state.clip_x0,command_state.clip_y0,command_state.clip_x1,command_state.clip_y1,command_state.offset_x,command_state.offset_y,command_state.display_mode,command_state.mask_bits);exit(2);
+}
+static void service(void *context,uint64_t cycle,unsigned kind) {
+ (void)context;
+ if(!source_gpu_command_update(&command_state,cycle))fail("unsupported command service");
+ dispatch();
+ if(cycleUINT32_MAX)fail("invalid draw raster time");
+ input_route_raster_advance(&draw_raster,(uint32_t)(cycle-draw_raster.cycle));
+ command_state.skip_field=(draw_raster.y_start+draw_raster.readout_field)&1u;
+ if(kind==SOURCE_GPU_EVENT_DMA || kind==SOURCE_GPU_EVENT_WRITE)
+ dma_source_gpu_service_at(cycle);
+}
+static void cpu_boundary(CPUState *cpu,uint32_t pc,uint64_t cycle) {
+ for(;;) {
+ if(clock_state.frame_pending) {
+ psx_devices_service_to_now();
+ if(!source_gpu_service_cpu_boundary(&clock_state,cycle,service,0))fail("invalid CPU boundary");
+ return_clock=clock_state;return_command=command_state;
+ source_cpu_return_probe(cpu,pc,cycle,clock_state.frame_returns);
+ source_ram_page_probe(clock_state.frame_returns,cycle);
+ psx_next_service_cycle=0;g_psx_cycle_fast_limit=0;
+ }
+ dma_cpu_read_wait_boundary();
+ source_cpu_boundary_probe(cpu,pc,cycle);
+ if(!dma_cpu_source_halted())return;
+ /* Original RunReal fetches before selecting its halt operation.
+ * Cache fill therefore overlaps DMA, while only the base/absorb step
+ * runs. No opcode effects or deferred-load writeback occur here.
+ * COP2 bypasses the original interrupt/halt dispatch table; allow the
+ * existing decoder to execute it, with the halt retained for its next
+ * instruction. This is source compatibility, not PS1 bus arbitration. */
+ uint32_t instruction=cpu->read_word(pc);
+ if((instruction>>26)==0x12u)return;
+ psx_icache_fetch_miss(cpu,pc);
+ if(cpu->read_absorb[cpu->read_absorb_which])
+ --cpu->read_absorb[cpu->read_absorb_which];
+ else psx_advance_cycles(1u);
+ psx_cyc_batch_flush();
+ psx_devices_service_to_now();
+ cycle=psx_get_cycle_count();
+ }
+}
+
+void source_gpu_runtime_init(void) {
+ if(enabled || psx_get_cycle_count()!=0 || g_psx_cpu_step_boundary_callback)fail("cold initialization/CPU owner required");
+ source_gpu_service_cold(&clock_state);source_gpu_command_cold(&command_state);
+ input_route_raster_reset(&draw_raster);command_state.field_valid=1;
+ enabled=1;g_psx_cpu_step_boundary_callback=cpu_boundary;
+ psx_next_service_cycle=0;g_psx_cycle_fast_limit=0;
+}
+int source_gpu_runtime_active(void) {return enabled;}
+int source_gpu_runtime_ready(void) {return enabled?source_gpu_command_ready(&command_state):-2;}
+uint32_t source_gpu_runtime_status_bits(void) {
+ uint32_t bits=(command_state.dma_direction&2u)?1u<<25:0;
+ if(!command_state.phase && !command_state.count && command_state.budget>=0)bits|=1u<<26;
+ if(source_gpu_command_ready(&command_state)>0)bits|=1u<<28;
+ if(command_state.phase==8)bits|=1u<<27;
+ return bits;
+}
+void source_gpu_runtime_advance(void) {
+ if(enabled && !source_gpu_service_to(&clock_state,psx_cycle_count,service,0))fail("reversed device time");
+}
+uint32_t source_gpu_runtime_cycles_to_event(void) {
+ if(!enabled)return UINT32_MAX;
+ uint64_t next=source_gpu_service_next(&clock_state);
+ return next>psx_cycle_count?(uint32_t)(next-psx_cycle_count):1u;
+}
+void source_gpu_runtime_dma_write(void) {
+ if(enabled && !source_gpu_service_dma_write(&clock_state,psx_cycle_count,service,0))fail("invalid DMA write time");
+}
+void source_gpu_runtime_gp0(uint32_t word) {
+ if(!enabled)return;
+ unsigned previous=command_state.first_triangles;
+ if(!source_gpu_command_write(&command_state,word))fail("unsupported GP0 word");
+ dispatch();
+ if(!previous && command_state.first_triangles)
+ fprintf(stderr,"[source-gpu-service] first quad at %llu; budget=%d phase=%u queued=%u clip=%d,%d..%d,%d offset=%d,%d mode=%02X mask=%u\n",(unsigned long long)psx_get_cycle_count(),command_state.budget,command_state.phase,command_state.count,command_state.clip_x0,command_state.clip_y0,command_state.clip_x1,command_state.clip_y1,command_state.offset_x,command_state.offset_y,command_state.display_mode,command_state.mask_bits);
+}
+void source_gpu_runtime_read(void) {
+ if(enabled)source_gpu_command_read(&command_state);
+}
+void source_gpu_runtime_gp1(uint32_t word) {
+ if(!enabled)return;
+ source_gpu_runtime_advance();
+ if(!source_gpu_command_gp1(&command_state,word) || !input_route_raster_gp1(&clock_state.raster,word) || !input_route_raster_gp1(&draw_raster,word))fail("unsupported GP1 control");
+ command_state.skip_field=(draw_raster.y_start+draw_raster.readout_field)&1u;
+ psx_next_service_cycle=0;g_psx_cycle_fast_limit=0;
+}
+void source_gpu_runtime_copy(SourceGPUServiceClock *clock,SourceGPUCommandProjection *command) {
+ if(clock)*clock=clock_state;
+ if(command)*command=command_state;
+}
+void source_gpu_runtime_copy_return(SourceGPUServiceClock *clock,SourceGPUCommandProjection *command) {
+ if(clock)*clock=return_clock;
+ if(command)*command=return_command;
+}
diff --git a/runtime/src/spu.c b/runtime/src/spu.c
index dc3589550..2d7ef1c92 100644
--- a/runtime/src/spu.c
+++ b/runtime/src/spu.c
@@ -36,6 +36,9 @@ static uint8_t spu_ram[SPU_RAM_SIZE];
static uint16_t spu_regs[SPU_REG_COUNT];
static uint32_t transfer_addr;
static uint32_t key_on_count;
+static int source_key_timing;
+static uint32_t source_key_on_pending, source_key_off_pending;
+static uint8_t source_play_delay[24];
static uint64_t render_frames;
static uint64_t nonzero_frames;
static int32_t last_peak;
@@ -153,6 +156,16 @@ typedef struct {
static SpuVoice voices[SPU_VOICE_COUNT];
+/* The retained source profile decodes one four-sample word when fewer than
+ * eleven samples remain. Its END/loop/envelope readbacks belong to that
+ * decoder boundary, not to the mixer's whole-block consumption boundary.
+ * Keep this queue separate from the default renderer's 28-sample blocks. */
+typedef struct {
+ int16_t samples[32];
+ uint8_t read_pos, write_pos, available, shift, filter, ignore_loop;
+} SourceSpuDecode;
+static SourceSpuDecode source_decode[SPU_VOICE_COUNT];
+
static void spu_event_record(uint8_t kind, int voice, uint32_t addr) {
SpuEvent *e = &s_events[s_event_idx & (SPU_EVENT_CAP - 1u)];
e->seq = s_event_seq++;
@@ -804,9 +817,115 @@ typedef char spu_shadow_voice_count_check[
const void* spu_shadow_tap_buffer(void) { return s_shadow_tap; }
int spu_shadow_tap_count(void) { return s_shadow_tap_frame; }
+static void source_decode_irq(uint32_t address) {
+ uint32_t irq_address = (uint32_t)spu_regs[reg_index(0x1F801DA4u)] << 3;
+ address &= SPU_RAM_SIZE - 1u;
+ if (irq_address == address || irq_address == (address & ~15u))
+ spu_irq_check(irq_address, 1u);
+}
+
+static void source_decode_word(int idx, uint32_t noise_mask) {
+ SpuVoice *v = &voices[idx];
+ SourceSpuDecode *d = &source_decode[idx];
+ if (d->available >= 11) {
+ source_decode_irq(v->cur_addr - 2u);
+ return;
+ }
+ if (!(v->cur_addr & 15u)) {
+ if (v->flags & 1u) {
+ v->cur_addr = v->repeat_addr & ~15u;
+ endx_latch |= 1u << idx;
+ if (!(v->flags & 2u) && !(noise_mask & (1u << idx))) {
+ v->env_level = 0;
+ v->adsr_phase = ADSR_RELEASE;
+ /* Source END keeps the envelope divider; KEYOFF resets it. */
+ }
+ spu_event_record((v->flags & 2u) ? SPU_EV_END_LOOP : SPU_EV_END_STOP,
+ idx, v->cur_addr);
+ }
+ source_decode_irq(v->cur_addr);
+ uint8_t header = spu_ram[v->cur_addr];
+ v->flags = spu_ram[v->cur_addr + 1u];
+ d->shift = header & 15u;
+ d->filter = header >> 4;
+ if ((v->flags & 4u) && !d->ignore_loop) {
+ v->repeat_addr = v->cur_addr;
+ spu_regs[(uint32_t)idx * 8u + 7u] = (uint16_t)(v->cur_addr >> 3);
+ }
+ v->cur_addr = (v->cur_addr + 2u) & (SPU_RAM_SIZE - 1u);
+ } else {
+ source_decode_irq(v->cur_addr);
+ }
+ uint16_t word = (uint16_t)(spu_ram[v->cur_addr] |
+ (uint16_t)spu_ram[v->cur_addr + 1u] << 8);
+ unsigned shift = d->shift;
+ if (shift > 12u) { shift = 8u; word &= 0x8888u; }
+ static const int16_t weights[5][2] = {{0,0},{60,0},{115,-52},{98,-55},{122,-60}};
+ int w1 = d->filter < 5u ? weights[d->filter][0] : 0;
+ int w2 = d->filter < 5u ? weights[d->filter][1] : 0;
+ for (unsigned n = 0; n < 4; ++n) {
+ int32_t value = (int16_t)((word & 15u) << 12);
+ value = (value >> shift) + (((int32_t)v->hist1 * w1) >> 6) +
+ (((int32_t)v->hist2 * w2) >> 6);
+ int16_t sample = clamp16(value);
+ d->samples[(d->write_pos + n) & 31u] = sample;
+ v->hist2 = v->hist1; v->hist1 = sample;
+ word >>= 4;
+ }
+ d->write_pos = (d->write_pos + 4u) & 31u;
+ d->available += 4;
+ v->cur_addr = (v->cur_addr + 2u) & (SPU_RAM_SIZE - 1u);
+}
+
+static int16_t source_voice_sample(int idx) {
+ SpuVoice *v = &voices[idx];
+ SourceSpuDecode *d = &source_decode[idx];
+ uint32_t noise_mask = (uint32_t)spu_regs[reg_index(0x1F801D94u)] |
+ (uint32_t)spu_regs[reg_index(0x1F801D96u)] << 16;
+ source_decode_word(idx, noise_mask);
+ unsigned gi = (v->phase >> 4) & 255u;
+ int32_t raw = noise_mask & (1u << idx) ? (int16_t)noise_lfsr :
+ ((int32_t)d->samples[d->read_pos] * spu_gauss_table[255u - gi] +
+ (int32_t)d->samples[(d->read_pos + 1u) & 31u] * spu_gauss_table[511u - gi] +
+ (int32_t)d->samples[(d->read_pos + 2u) & 31u] * spu_gauss_table[256u + gi] +
+ (int32_t)d->samples[(d->read_pos + 3u) & 31u] * spu_gauss_table[gi]) >> 15;
+ int32_t shaped = (raw * (int16_t)v->env_level) >> 15;
+ /* Every voice is visited in ascending order within one sample, including
+ * silent/disabled voices. This value is only used by the following voice. */
+ static int32_t previous_voice;
+ if (!source_play_delay[idx]) {
+ adsr_run(idx, v);
+ uint32_t pitch = voice_reg(idx, 2);
+ uint32_t pmon = (uint32_t)spu_regs[reg_index(0x1F801D90u)] |
+ (uint32_t)spu_regs[reg_index(0x1F801D92u)] << 16;
+ if (idx && (pmon & (1u << idx)))
+ pitch += ((int16_t)pitch * previous_voice) >> 15;
+ if (pitch > 0x3FFFu) pitch = 0x3FFFu;
+ uint32_t phase = v->phase + pitch;
+ unsigned consumed = phase >> 12;
+ v->phase = phase & 4095u;
+ d->available -= consumed;
+ d->read_pos = (d->read_pos + consumed) & 31u;
+ } else source_play_delay[idx]--;
+ previous_voice = shaped;
+ v->sample_idx = d->read_pos; /* diagnostic position in the source ring */
+ return (int16_t)shaped;
+}
+
static int16_t voice_next_sample(int idx) {
+ if (source_key_timing) return source_voice_sample(idx);
SpuVoice *v = &voices[idx];
- if (!v->active) return 0;
+ if (!v->active) {
+ /* The retained source has no inactive-envelope shortcut. Cold
+ * voices start in Attack; a cancelled key-on still leaves their
+ * envelope clock running. Keep this readback behavior independent
+ * of our silent-voice decoder optimization. */
+ if (source_key_timing) {
+ if (source_play_delay[idx]) source_play_delay[idx]--;
+ else adsr_run(idx, v);
+ }
+ return 0;
+ }
if (v->sample_idx >= SPU_BLOCK_SAMPLES) {
if (v->flags & 0x01u) {
@@ -841,6 +960,13 @@ static int16_t voice_next_sample(int idx) {
decode_block(v);
}
+ /* Original source key-on is sample-applied, followed by four samples
+ * without envelope or pitch advancement. Keep ordinary decoder work. */
+ if (source_key_timing && source_play_delay[idx]) {
+ source_play_delay[idx]--;
+ return 0;
+ }
+
/* Noise mode (NON bit set): the voice outputs the live noise LFSR value
* INSTEAD of its interpolated ADPCM sample. Everything else — block
* decoding/address advance, ENDX latching, pitch stepping, the ADSR
@@ -926,6 +1052,13 @@ static void key_on(uint32_t mask) {
v->env_level = 0;
v->adsr_divider = 0;
v->adsr_phase = ADSR_ATTACK;
+ if (source_key_timing) {
+ SourceSpuDecode *d = &source_decode[i];
+ d->read_pos = d->write_pos = d->available = 0;
+ d->ignore_loop = 0;
+ v->repeat_addr = (uint32_t)voice_reg(i, 7) << 3;
+ /* The source retains decoded sample contents across KEYON. */
+ }
key_on_count++;
endx_latch &= ~(1u << i); /* KEYON clears ENDX bit on real hw */
spu_event_record(SPU_EV_KEYON, i, v->cur_addr);
@@ -939,7 +1072,8 @@ static void key_on(uint32_t mask) {
static void key_off(uint32_t mask) {
for (int i = 0; i < SPU_VOICE_COUNT; i++) {
if (!(mask & (1u << i))) continue;
- if (!voices[i].active) continue;
+ if (!voices[i].active && !source_key_timing) continue;
+ if (source_key_timing && voices[i].adsr_phase == ADSR_RELEASE) continue;
spu_event_record(SPU_EV_KEYOFF, i, voices[i].cur_addr);
voices[i].adsr_phase = ADSR_RELEASE;
voices[i].adsr_divider = 0;
@@ -947,7 +1081,27 @@ static void key_off(uint32_t mask) {
}
}
+static void source_apply_keys(int enabled) {
+ key_off(source_key_off_pending);
+ key_on(source_key_on_pending);
+ for (int i = 0; i < SPU_VOICE_COUNT; ++i) {
+ if (source_key_on_pending & (1u << i)) source_play_delay[i] = 4;
+ if (!enabled) {
+ voices[i].adsr_phase = ADSR_RELEASE;
+ voices[i].env_level = 0;
+ }
+ }
+ source_key_on_pending = source_key_off_pending = 0;
+}
+
void spu_init(void) {
+ const char *model = getenv("PSX_GPU_DMA_MODEL");
+ source_key_timing = model &&
+ (!strcmp(model, "octoshock-2.2.2-bounded-linked-list") ||
+ !strcmp(model, "octoshock-2.2.2-bounded-quad"));
+ source_key_on_pending = source_key_off_pending = 0;
+ memset(source_play_delay, 0, sizeof(source_play_delay));
+ memset(source_decode, 0, sizeof(source_decode));
memset(spu_ram, 0, sizeof(spu_ram));
memset(spu_regs, 0, sizeof(spu_regs));
memset(voices, 0, sizeof(voices));
@@ -987,6 +1141,11 @@ void spu_render(int16_t* out_stereo, int frames) {
if (!out_stereo || frames <= 0) return;
uint16_t ctrl = spu_regs[reg_index(0x1F801DAAu)];
+ /* SPUSTAT reports the applied low control bits, not the pending write.
+ * Apply at the existing output-sample boundary. This is the SPU model's
+ * sample granularity; it does not assert a measured hardware subphase.
+ * The register image already participates in snapshot save/restore. */
+ spu_regs[reg_index(0x1F801DAEu)] = ctrl & 0x3Fu;
int enabled = (ctrl & 0x8000u) != 0;
int cd_on = (ctrl & 0x0001u) != 0;
int cd_rev = cd_on && (ctrl & 0x0004u) != 0; /* CD reverb send needs CD enable */
@@ -1003,6 +1162,9 @@ void spu_render(int16_t* out_stereo, int frames) {
}
}
+ if (source_key_timing)
+ any_voice = 1; /* source envelopes clock even without an active voice */
+
/* Shadow tap: arm recording for this block if the float SPU shadow is on.
* Off by default => s_shadow_tap_on stays 0 and the mix loop is unchanged
* and byte-identical to upstream. */
@@ -1131,7 +1293,7 @@ void spu_render(int16_t* out_stereo, int frames) {
int16_t main_l = chan_volume(spu_regs[reg_index(0x1F801D80u)], &sweep_main_env[0]);
int16_t main_r = chan_volume(spu_regs[reg_index(0x1F801D82u)], &sweep_main_env[1]);
- if (enabled) {
+ if (enabled || source_key_timing) {
int32_t voice_l = 0;
int32_t voice_r = 0;
int32_t rev_send_l = 0;
@@ -1177,6 +1339,11 @@ void spu_render(int16_t* out_stereo, int frames) {
}
}
}
+ /* The retained source clocks capture/decoder state even with
+ * SPU enable clear. Its separate mute bit gates the voice mix
+ * and reverb sends, after voice capture and before CD mixing. */
+ if (source_key_timing && !(ctrl & 0x4000u))
+ voice_l = voice_r = rev_send_l = rev_send_r = 0;
mix_l = voice_l;
mix_r = voice_r;
if (voice_sum_pos < voice_sum_cap) {
@@ -1267,6 +1434,11 @@ void spu_render(int16_t* out_stereo, int frames) {
mix_r = ((int32_t)mix_r * main_r) >> 15;
}
+ if (source_key_timing) {
+ /* Disable forces Release/zero after each source sample; its
+ * divider and startup delay still clock before that reset. */
+ source_apply_keys(enabled);
+ }
out_stereo[f * 2 + 0] = clamp16(mix_l);
out_stereo[f * 2 + 1] = clamp16(mix_r);
int32_t frame_peak = abs32(out_stereo[f * 2 + 0]);
@@ -1343,8 +1515,8 @@ uint32_t spu_read(uint32_t addr) {
uint32_t idx = reg_index(addr);
if (idx < SPU_REG_COUNT) {
if (addr == 0x1F801DAEu) {
- /* SPUSTAT (psx-spx): bits 5-0 mirror SPUCNT bits 5-0 (the
- * current SPU mode), bit 6 is the IRQ flag (cleared by
+ /* SPUSTAT (psx-spx): bits 5-0 mirror the applied SPUCNT
+ * mode at the sample boundary, bit 6 is the IRQ flag (cleared by
* writing SPUCNT with bit 6 clear), bit 7 follows SPUCNT.5
* (DMA r/w request), bit 10 is the data-transfer busy flag —
* 0 here because this runtime completes FIFO/DMA transfers
@@ -1355,7 +1527,7 @@ uint32_t spu_read(uint32_t addr) {
* "currently writing the SECOND half of the capture
* buffers" (capture offset >= 0x200). */
uint16_t cnt = spu_regs[reg_index(0x1F801DAAu)];
- uint32_t st = (uint32_t)((cnt & 0x3Fu) | (((cnt >> 5) & 1u) << 7));
+ uint32_t st = (uint32_t)((spu_regs[idx] & 0x3Fu) | (((cnt >> 5) & 1u) << 7));
if (irq_flag) st |= 0x40u;
if (capture_pos & 0x200u) st |= 0x800u;
return st;
@@ -1439,6 +1611,12 @@ void spu_write(uint32_t addr, uint32_t value) {
audio_trace_event(AUDIO_EV_REG_WRITE, addr, value & 0xFFFFu);
spu_regs[idx] = (uint16_t)value;
+ /* Source voice register 6 writes the live envelope, without
+ * resetting its phase or divider (PS_SPU::Write, case 0x0C). */
+ if (source_key_timing && idx < (uint32_t)SPU_VOICE_COUNT * 8u &&
+ (idx & 7u) == 6u)
+ voices[idx >> 3].env_level = (uint16_t)value;
+
/* Voice repeat/loop address (voice reg 7) is LIVE state on real
* hardware: writing it after KEYON retargets where the next
* END+REPEAT block jumps (Beetle spu.cpp:1150/333). X5's driver
@@ -1452,6 +1630,10 @@ void spu_write(uint32_t addr, uint32_t value) {
/* bit0 ignored (16-byte alignment) — same masking as KEYON. */
voices[v].repeat_addr =
((uint32_t)((uint16_t)value & ~1u) << 3) & (SPU_RAM_SIZE - 1u);
+ if (source_key_timing) {
+ voices[v].repeat_addr = ((uint32_t)(uint16_t)value << 3) & (SPU_RAM_SIZE - 1u);
+ source_decode[v].ignore_loop = 1;
+ }
}
/* Volume registers feed the sweep envelopes: a direct write
@@ -1484,19 +1666,27 @@ void spu_write(uint32_t addr, uint32_t value) {
if (addr == 0x1F801D88u) {
kon_latch = (kon_latch & 0xFFFF0000u) | (uint32_t)(uint16_t)value;
- key_on((uint32_t)(uint16_t)value);
+ if (source_key_timing)
+ source_key_on_pending = (source_key_on_pending & 0xFFFF0000u) | (uint16_t)value;
+ else key_on((uint32_t)(uint16_t)value);
}
if (addr == 0x1F801D8Au) {
kon_latch = (kon_latch & 0x0000FFFFu) | ((uint32_t)(uint16_t)value << 16);
- key_on((uint32_t)(uint16_t)value << 16);
+ if (source_key_timing)
+ source_key_on_pending = (source_key_on_pending & 0x0000FFFFu) | ((uint32_t)(value & 0xFFu) << 16);
+ else key_on((uint32_t)(uint16_t)value << 16);
}
if (addr == 0x1F801D8Cu) {
koff_latch = (koff_latch & 0xFFFF0000u) | (uint32_t)(uint16_t)value;
- key_off((uint32_t)(uint16_t)value);
+ if (source_key_timing)
+ source_key_off_pending = (source_key_off_pending & 0xFFFF0000u) | (uint16_t)value;
+ else key_off((uint32_t)(uint16_t)value);
}
if (addr == 0x1F801D8Eu) {
koff_latch = (koff_latch & 0x0000FFFFu) | ((uint32_t)(uint16_t)value << 16);
- key_off((uint32_t)(uint16_t)value << 16);
+ if (source_key_timing)
+ source_key_off_pending = (source_key_off_pending & 0x0000FFFFu) | ((uint32_t)(value & 0xFFu) << 16);
+ else key_off((uint32_t)(uint16_t)value << 16);
}
if (addr == 0x1F801DA6u) {
@@ -1722,9 +1912,13 @@ static int spu_r_voice(PstR *r, int idx) {
#define SPU_SNAPSHOT_TAIL_BYTES \
(20u + 1u + 2u + 4u + 4u + 1u + 4u + 4u + 4u + 4u + 4u + 2u * (2u + 4u))
+/* SPK2 extends the source-only footer with each decoder's complete queue. */
+#define SOURCE_SPU_TAIL_BYTES (36u + SPU_VOICE_COUNT * (64u + 6u))
+
uint32_t spu_snapshot_bytes(void) {
return (uint32_t)(SPU_REG_COUNT * 2u) +
- (SPU_VOICE_COUNT * SPU_VOICE_WIRE_BYTES) + SPU_SNAPSHOT_TAIL_BYTES;
+ (SPU_VOICE_COUNT * SPU_VOICE_WIRE_BYTES) + SPU_SNAPSHOT_TAIL_BYTES +
+ (source_key_timing ? SOURCE_SPU_TAIL_BYTES : 0u);
}
void spu_snapshot_write(uint8_t *p) {
@@ -1754,6 +1948,19 @@ void spu_snapshot_write(uint8_t *p) {
pst_w_i16(&w, sweep_main_env[ch].level);
pst_w_u32(&w, sweep_main_env[ch].divider);
}
+ if (source_key_timing) {
+ pst_w_u32(&w, 0x324B5053u); /* SPK2: source keys and decoder queue */
+ pst_w_u32(&w, source_key_on_pending);
+ pst_w_u32(&w, source_key_off_pending);
+ for (int i = 0; i < SPU_VOICE_COUNT; ++i) pst_w_u8(&w, source_play_delay[i]);
+ for (int i = 0; i < SPU_VOICE_COUNT; ++i) {
+ const SourceSpuDecode *d = &source_decode[i];
+ for (int j = 0; j < 32; ++j) pst_w_i16(&w, d->samples[j]);
+ pst_w_u8(&w, d->read_pos); pst_w_u8(&w, d->write_pos);
+ pst_w_u8(&w, d->available); pst_w_u8(&w, d->shift);
+ pst_w_u8(&w, d->filter); pst_w_u8(&w, d->ignore_loop);
+ }
+ }
}
int spu_snapshot_read(const uint8_t *p, uint32_t len) {
@@ -1780,6 +1987,25 @@ int spu_snapshot_read(const uint8_t *p, uint32_t len) {
!pst_r_u32(&r, &sweep_main_env[ch].divider))
return 0;
}
+ if (source_key_timing) {
+ uint32_t magic;
+ if (!pst_r_u32(&r, &magic) || magic != 0x324B5053u ||
+ !pst_r_u32(&r, &source_key_on_pending) ||
+ !pst_r_u32(&r, &source_key_off_pending)) return 0;
+ for (int i = 0; i < SPU_VOICE_COUNT; ++i)
+ if (!pst_r_u8(&r, &source_play_delay[i]) || source_play_delay[i] > 4) return 0;
+ for (int i = 0; i < SPU_VOICE_COUNT; ++i) {
+ SourceSpuDecode *d = &source_decode[i];
+ for (int j = 0; j < 32; ++j)
+ if (!pst_r_i16(&r, &d->samples[j])) return 0;
+ if (!pst_r_u8(&r, &d->read_pos) || d->read_pos > 31 ||
+ !pst_r_u8(&r, &d->write_pos) || d->write_pos > 31 ||
+ !pst_r_u8(&r, &d->available) || d->available > 14 ||
+ !pst_r_u8(&r, &d->shift) || d->shift > 15 ||
+ !pst_r_u8(&r, &d->filter) || d->filter > 15 ||
+ !pst_r_u8(&r, &d->ignore_loop) || d->ignore_loop > 1) return 0;
+ }
+ }
return 1;
}
uint8_t* spu_get_ram_ptr(void){ return spu_ram; }
@@ -1810,7 +2036,8 @@ void spu_snapshot_part_digests(SpuSnapPartDigests *out)
spu_snapshot_write(buf);
regs_n = (uint32_t)(SPU_REG_COUNT * 2u);
voices_n = (uint32_t)(SPU_VOICE_COUNT * SPU_VOICE_WIRE_BYTES);
- if (regs_n + voices_n + SPU_SNAPSHOT_TAIL_BYTES != n)
+ uint32_t tail_n = SPU_SNAPSHOT_TAIL_BYTES + (source_key_timing ? SOURCE_SPU_TAIL_BYTES : 0u);
+ if (regs_n + voices_n + tail_n != n)
return;
crc = 0xFFFFFFFFu;
crc = crc32_update(crc, buf, regs_n);
@@ -1819,6 +2046,6 @@ void spu_snapshot_part_digests(SpuSnapPartDigests *out)
crc = crc32_update(crc, buf + regs_n, voices_n);
out->voices = crc ^ 0xFFFFFFFFu;
crc = 0xFFFFFFFFu;
- crc = crc32_update(crc, buf + regs_n + voices_n, SPU_SNAPSHOT_TAIL_BYTES);
+ crc = crc32_update(crc, buf + regs_n + voices_n, tail_n);
out->tail = crc ^ 0xFFFFFFFFu;
}
diff --git a/runtime/src/timers.c b/runtime/src/timers.c
index ef1599589..0d2ac01e7 100644
--- a/runtime/src/timers.c
+++ b/runtime/src/timers.c
@@ -15,6 +15,10 @@
*/
#include "timers.h"
+#include "timer1_source_clock.h"
+#include "timer2_source_clock.h"
+#include
+#include
#include "event_ring.h"
#include
@@ -47,6 +51,34 @@ typedef struct {
} Timer;
static Timer timers[3];
+static PsxTimer1Source source_timer1;
+static int source_timer1_enabled;
+static uint64_t source_timer1_cycle;
+static PsxTimer2Source source_timer2;
+static int source_timer2_enabled;
+static uint32_t source_timer2_elapsed,source_timer2_deadline;
+static void source_timer2_flush(void);
+int timers_source_raster_enabled(void) { return source_timer1_enabled; }
+int timers_source_hblank_counter_read(uint32_t addr) {
+ return source_timer1_enabled && (addr&~3u)==0x1f801110u &&
+ (source_timer1.mode&0x100u);
+}
+void timers_source_raster_finish(uint64_t cycle) {
+ if(!source_timer1_enabled) return;
+ if(cycleUINT32_MAX) {
+ fprintf(stderr,"[timer1-source-clock] invalid ordered clock interval\n");exit(4);
+ }
+ timer1_source_cpu(&source_timer1,(uint32_t)(cycle-source_timer1_cycle));
+ source_timer1_cycle=cycle;
+}
+void timers_source_raster_event(void *context,uint64_t cycle,unsigned event,int blank) {
+ (void)context;
+ if(!source_timer1_enabled) return;
+ timers_source_raster_finish(cycle);
+ if(event==1) timer1_source_hblank(&source_timer1,1);
+ else if(event==2) timer1_source_blank(&source_timer1,blank);
+}
+
static uint32_t timer_frac[3];
/* Always-on RootCounter/Timer IRQ-fire telemetry (Tomba 2 RCnt-wait diag):
@@ -71,6 +103,28 @@ extern uint32_t i_stat;
extern void psx_irq_raise(uint32_t bit, uint32_t detail);
void timers_init(void) {
+ const char *model=getenv("PSX_TIMER1_MODEL");
+ source_timer1_enabled=model && *model;
+ if(source_timer1_enabled) {
+ const char *field=getenv("PSX_INPUT_ROUTE_FIELD_MODEL");
+ if(strcmp(model,"octoshock-2.2.2") || !getenv("PSX_INPUT_ROUTE_FILE") ||
+ !field || strcmp(field,"octoshock-2.2.2-ntsc-raster")) {
+ fprintf(stderr,"[timer1-source-clock] requires named model, file route and NTSC raster clock\n");exit(4);
+ }
+ fprintf(stderr,"[timer1-source-clock] experimental original source timer1; IRQ-enabled modes and restore unsupported; timer2 has a separate option\n");
+ }
+ timer1_source_reset(&source_timer1);source_timer1_cycle=0;
+ model=getenv("PSX_TIMER2_MODEL");source_timer2_enabled=model && *model;
+ if(source_timer2_enabled) {
+#ifndef PSX_ENABLE_BLOCK_CYCLES
+ fprintf(stderr,"[timer2-source-clock] block-cycle execution required\n");exit(4);
+#endif
+ if(strcmp(model,"octoshock-2.2.2") || !source_timer1_enabled) {
+ fprintf(stderr,"[timer2-source-clock] requires named model and source timer1 profile\n");exit(4);
+ }
+ fprintf(stderr,"[timer2-source-clock] experimental original source timer2; timer0 IRQ modes and restore unsupported\n");
+ }
+ timer2_source_reset(&source_timer2);source_timer2_elapsed=0;source_timer2_deadline=1024;
memset(timers, 0, sizeof(timers));
memset(timer_frac, 0, sizeof(timer_frac));
}
@@ -85,11 +139,24 @@ void timers_get_snapshot(uint16_t counter[3], uint32_t mode[3],
irq_line[i] = timers[i].irq_line;
frac[i] = timer_frac[i];
}
+ if(source_timer1_enabled) {
+ counter[1]=(uint16_t)source_timer1.counter; mode[1]=source_timer1.mode;
+ target[1]=(uint16_t)source_timer1.target; irq_line[1]=0; frac[1]=0;
+ }
+ if(source_timer2_enabled) {
+ PsxTimer2Source view=source_timer2;
+ (void)timer2_source_cpu(&view,source_timer2_elapsed);
+ counter[2]=(uint16_t)view.counter;mode[2]=view.mode;target[2]=(uint16_t)view.target;
+ irq_line[2]=0;frac[2]=view.divider;
+ }
}
void timers_set_snapshot(const uint16_t counter[3], const uint32_t mode[3],
const uint16_t target[3], const int32_t irq_line[3],
const uint32_t frac[3]) {
+ if(source_timer1_enabled || source_timer2_enabled) {
+ fprintf(stderr,"[timer1-source-clock] cold boot only; restore unsupported\n");exit(4);
+ }
for (int i = 0; i < 3; i++) {
timers[i].counter = counter[i];
timers[i].mode = mode[i];
@@ -99,6 +166,18 @@ void timers_set_snapshot(const uint16_t counter[3], const uint32_t mode[3],
}
}
+static void source_timer2_pulses(unsigned pulses) {
+ while(pulses--) {
+ g_timer_irq_fired[2]++;psx_irq_raise(IRQ_TIMER2,2);
+ event_ring_record_aux(EV_DEQ,(uint8_t)(SRC_TIMER0+2),source_timer2.counter);
+ }
+}
+static void source_timer2_flush(void) {
+ if(!source_timer2_enabled)return;
+ source_timer2_pulses(timer2_source_cpu(&source_timer2,source_timer2_elapsed));
+ source_timer2_elapsed=0;source_timer2_deadline=timer2_source_next(&source_timer2);
+}
+
/* Determine whether this timer uses system clock ticks */
static int timer_uses_sysclk(int t) {
int src = (timers[t].mode >> 8) & 3;
@@ -203,6 +282,17 @@ void timers_advance(uint32_t cycles) {
if (cycles == 0) return;
for (int t = 0; t < 3; t++) {
+ if(t==1 && source_timer1_enabled) continue; /* supplied by raster events */
+ if(t==2 && source_timer2_enabled) {
+ /* Only source timer deadlines and timer MMIO update this model.
+ * Unrelated device service boundaries must not reset its cadence. */
+ if(cycles>source_timer2_deadline-source_timer2_elapsed) {
+ fprintf(stderr,"[timer2-source-clock] scheduler crossed timer deadline\n");exit(4);
+ }
+ source_timer2_elapsed+=cycles;
+ if(source_timer2_elapsed==source_timer2_deadline)source_timer2_flush();
+ continue;
+ }
int src = (timers[t].mode >> 8) & 3;
if (t == 2 && (src == 2 || src == 3)) {
timer_advance_divided(t, cycles, 8);
@@ -240,6 +330,12 @@ static uint32_t timer_divisor(int t) {
uint32_t timers_cycles_to_irq(uint32_t i_mask) {
uint32_t best = 0xFFFFFFFFu;
for (int t = 0; t < 3; t++) {
+ if(t==1 && source_timer1_enabled) continue; /* IRQ modes rejected on write */
+ if(t==2 && source_timer2_enabled) {
+ uint32_t next=source_timer2_deadline-source_timer2_elapsed;
+ if(nextmode & MODE_IRQ_TARGET) != 0;
@@ -275,6 +371,9 @@ uint32_t timers_read(uint32_t addr) {
int reg = (addr - TIMER_BASE) & 0x0F;
if (timer < 0 || timer > 2) return 0;
+ source_timer2_flush(); /* Original TIMER_Read updates every timer first. */
+ if(timer==2 && source_timer2_enabled)return timer2_source_read(&source_timer2,(unsigned)reg);
+ if(timer==1 && source_timer1_enabled) return timer1_source_read(&source_timer1,(unsigned)reg);
switch (reg) {
case 0x00: {
@@ -313,6 +412,28 @@ void timers_write(uint32_t addr, uint32_t value) {
int reg = (addr - TIMER_BASE) & 0x0F;
if (timer < 0 || timer > 2) return;
+ source_timer2_flush(); /* Original TIMER_Write updates every timer first. */
+ if(source_timer2_enabled) {
+ /* The MMIO wrapper synchronized against the OLD timer configuration.
+ * Its cached absolute deadline cannot survive a guest reprogramming
+ * the timer to an earlier event. The next CPU charge must recompute
+ * both scheduler paths; no guest clocks or counter values are added. */
+ extern uint64_t psx_next_service_cycle,g_psx_cycle_fast_limit;
+ psx_next_service_cycle=0;g_psx_cycle_fast_limit=0;
+ }
+ if(source_timer2_enabled && timer==0 && reg==4 && (value&0x30)) {
+ fprintf(stderr,"[timer2-source-clock] timer0 IRQ modes unsupported in combined source profile\n");exit(4);
+ }
+ if(timer==2 && source_timer2_enabled) {
+ source_timer2_pulses(timer2_source_write(&source_timer2,(unsigned)reg,(uint16_t)value));
+ source_timer2_deadline=timer2_source_next(&source_timer2);return;
+ }
+ if(timer==1 && source_timer1_enabled) {
+ if(!timer1_source_write(&source_timer1,(unsigned)reg,(uint16_t)value)) {
+ fprintf(stderr,"[timer1-source-clock] IRQ-enabled mode 0x%04X unsupported\n",(unsigned)value);exit(4);
+ }
+ return;
+ }
switch (reg) {
case 0x00:
diff --git a/runtime/src/traps.c b/runtime/src/traps.c
index d6ea41819..9af780e69 100644
--- a/runtime/src/traps.c
+++ b/runtime/src/traps.c
@@ -18,6 +18,7 @@
#include "savestate.h" /* scheduler-top coherent disk save/load boundary */
#include "parity_trace.h" /* general two-process control-flow parity ring */
#include "pst_wire.h"
+#include "source_gpu_runtime.h"
/* RAM reader adapter for the parity trace (cpu->read_word takes only addr). */
static uint32_t traps_parity_rw(void* ctx, uint32_t addr) {
@@ -1028,6 +1029,21 @@ void psx_scheduler_run(CPUState* cpu)
}
}
+/* Explicit research control: execute the installed guest SYS01/02 handler.
+ * This does not select a BIOS image, scheduler, delay, or title-specific PC.
+ * The old direct mode remains the default until the exception route has wider
+ * validation. Non-delay-slot entry is the existing psx_syscall API contract.
+ * Nested synchronous IRQ windows need a separate RFE owner stack; fail closed
+ * rather than let an inner RFE unwind the outer handler's native stack. */
+static int critical_exception_enabled(void) {
+ static int mode = -1;
+ if (mode < 0) {
+ const char* value = getenv("PSX_CRITICAL_SECTION_MODEL");
+ mode = value && strcmp(value, "exception") == 0;
+ }
+ return mode;
+}
+
int psx_syscall(CPUState* cpu, uint32_t code) {
/*
* PS1 BIOS SYSCALL convention:
@@ -1046,6 +1062,24 @@ int psx_syscall(CPUState* cpu, uint32_t code) {
uint32_t func = cpu->gpr[4]; /* $a0 = syscall function number */
uint32_t sr = cpu->cop0[12];
+ if ((func == 1 || func == 2) && critical_exception_enabled()) {
+ if (psx_get_in_exception()) {
+ fprintf(stderr, "SYS01/02 exception model: nested synchronous IRQ entry unsupported at %08X\n", cpu->pc);
+ exit(1);
+ }
+ /* Return the vector to the flat dispatcher. Calling psx_dispatch here
+ * would create a host continuation that could outlive the guest RFE.
+ * Guest code owns result registers, TCB stores, and EPC advancement. */
+ cpu->cop0[14] = cpu->pc;
+ /* Source exception entry retains only pending interrupt bits. A
+ * non-delay SYSCALL has no CE, BD, or BT bits of its own. */
+ cpu->cop0[13] = (cpu->cop0[13] & (source_gpu_runtime_active()
+ ? 0x0000FF00u : ~(0x80000000u | 0x7Cu))) | (8u << 2);
+ cpu->cop0[12] = (sr & ~0x3Fu) | ((sr & 0x0Fu) << 2);
+ cpu->pc = (sr & 0x00400000u) ? 0xBFC00180u : 0x80000080u;
+ return 1;
+ }
+
switch (func) {
case 1: /* EnterCriticalSection: disable interrupts */
cpu->cop0[12] = sr & ~1u; /* clear IEc (bit 0) */
diff --git a/runtime/tests/test_bios_alias_admission.c b/runtime/tests/test_bios_alias_admission.c
new file mode 100644
index 000000000..a58c92134
--- /dev/null
+++ b/runtime/tests/test_bios_alias_admission.c
@@ -0,0 +1,40 @@
+#include
+#include
+#include
+#include "cpu_state.h"
+#include "psx_bios_backend.h"
+extern const PsxBiosBackend Test_psx_bios_backend;
+#include "psx_icache.h"
+#include "psx_cycles.h"
+extern unsigned char* memory_get_ram_ptr(void);
+extern void dirty_ram_mark_executable_range(uint32_t,uint32_t);
+extern void memory_set_sr_ptr(const uint32_t*);
+extern uint32_t psx_read_word(uint32_t);
+extern uint16_t psx_read_half(uint32_t);
+extern uint8_t psx_read_byte(uint32_t);
+extern void psx_write_word(uint32_t,uint32_t);
+extern void psx_write_half(uint32_t,uint16_t);
+extern void psx_write_byte(uint32_t,uint8_t);
+extern void Test_psx_dispatch_call(CPUState*,uint32_t,uint32_t);
+extern uint64_t g_dispatch_static_hits;
+extern CPUState *debug_cpu_ptr;
+extern int g_input_instruction_histogram_active;
+static uint32_t fetches[256]; static unsigned nf;
+void __wrap_input_instruction_histogram_sample(uint32_t pc) { if(nf<256)fetches[nf++]=pc; }
+int main(int argc,char**argv) {
+ if(argc!=5)return 2;
+ uint32_t alias=(uint32_t)strtoul(argv[1],0,16), entry=(uint32_t)strtoul(argv[2],0,16), stop=0x1000;
+ FILE*f=fopen(argv[3],"rb"); if(!f)return 3;
+ fread(memory_get_ram_ptr()+0x500,1,0x130,f);fclose(f);if(atoi(argv[4]))dirty_ram_mark_executable_range(0x500,0x130);
+ CPUState cpu={0};cpu.read_word=psx_read_word;cpu.read_half=psx_read_half;cpu.read_byte=psx_read_byte;
+ cpu.write_word=psx_write_word;cpu.write_half=psx_write_half;cpu.write_byte=psx_write_byte;
+ cpu.read_fudge=32;cpu.ld_which_t=32;cpu.gpr[29]=0x1ff000;cpu.gpr[31]=0x1000;cpu.gpr[17]=0x1000;cpu.gpr[8]=99;cpu.gpr[4]=1;
+ if(entry==0xa0){memcpy(memory_get_ram_ptr()+0xa0,memory_get_ram_ptr()+0x500,16);if(atoi(argv[4]))dirty_ram_mark_executable_range(0xa0,16);}
+ if(entry==0x500 || entry==0xa0)stop=0x598;
+ if(entry==0x5c0)stop=0x80000080;
+ psx_bios_activate(&Test_psx_bios_backend);debug_cpu_ptr=&cpu;memory_set_sr_ptr(&cpu.cop0[12]);
+ psx_icache_reset();g_psx_icache_active=1;g_input_instruction_histogram_active=1;g_input_instruction_histogram_callback=__wrap_input_instruction_histogram_sample;psx_next_service_cycle=1000000;
+ Test_psx_dispatch_call(&cpu,alias|entry,stop);psx_cyc_batch_flush();
+ printf("{\"alias\":%u,\"entry\":%u,\"cycles\":%llu,\"static_hits\":%llu,\"s0\":%u,\"v0\":%u,\"t0\":%u,\"epc\":%u,\"cause\":%u,\"pc\":%u,\"fetches\":[",alias,entry,(unsigned long long)psx_cycle_count,(unsigned long long)g_dispatch_static_hits,cpu.gpr[16],cpu.gpr[2],cpu.gpr[8],cpu.cop0[14],cpu.cop0[13],cpu.pc);
+ for(unsigned i=0;i
+#include
+#include "cpu_state.h"
+#include "psx_bios_backend.h"
+#include "psx_icache.h"
+#include "psx_cycles.h"
+extern const PsxBiosBackend Test_psx_bios_backend;
+extern void Test_psx_dispatch_call(CPUState*,uint32_t,uint32_t);
+extern uint32_t psx_read_word(uint32_t);
+extern uint16_t psx_read_half(uint32_t);
+extern uint8_t psx_read_byte(uint32_t);
+extern void psx_write_word(uint32_t,uint32_t);
+extern void psx_write_half(uint32_t,uint16_t);
+extern void psx_write_byte(uint32_t,uint8_t);
+extern void memory_set_sr_ptr(const uint32_t*);
+extern CPUState *debug_cpu_ptr;
+extern uint64_t g_dispatch_static_hits;
+extern int g_input_instruction_histogram_active;
+static unsigned char code[0x48];
+static uint32_t fetches[32]; static unsigned nf;
+static void sample(uint32_t pc) { if(nf<32) fetches[nf++]=pc; }
+void __wrap_input_instruction_histogram_sample(uint32_t pc) { sample(pc); }
+static uint32_t read_word(uint32_t addr) {
+ uint32_t phys=addr&0x1fffffff,value;
+ if(phys>=0x1fc00000 && phys+4<=0x1fc00000+sizeof(code)) {
+ memcpy(&value,code+(phys-0x1fc00000),4);return value;
+ }
+ return psx_read_word(addr);
+}
+int main(int argc,char**argv) {
+ if(argc!=3)return 2;
+ FILE*f=fopen(argv[1],"rb");if(!f)return 3;
+ if(fread(code,1,sizeof(code),f)!=sizeof(code)){fclose(f);return 4;}fclose(f);
+ CPUState cpu={0};cpu.read_word=read_word;cpu.read_half=psx_read_half;cpu.read_byte=psx_read_byte;
+ cpu.write_word=psx_write_word;cpu.write_half=psx_write_half;cpu.write_byte=psx_write_byte;
+ cpu.read_fudge=32;cpu.ld_which_t=32;cpu.gpr[29]=0x1ff000;cpu.gpr[31]=0x1000;cpu.gpr[16]=0x1000;cpu.gpr[4]=1;
+ psx_bios_activate(&Test_psx_bios_backend);debug_cpu_ptr=&cpu;memory_set_sr_ptr(&cpu.cop0[12]);
+ psx_icache_reset();g_psx_icache_active=1;g_input_instruction_histogram_active=1;g_input_instruction_histogram_callback=sample;psx_next_service_cycle=1000000;
+ uint64_t entry_cycles=0;uint32_t resumed_pc=0;
+ if(!strcmp(argv[2],"sys")) {
+ Test_psx_dispatch_call(&cpu,0xbfc00000,0x80000080);psx_cyc_batch_flush();entry_cycles=psx_cycle_count;
+ /* Supply the ordinary non-delay-slot EPC+4 return to isolate admission.
+ This authored fixture does not simulate the guest handler or RFE. */
+ resumed_pc=cpu.cop0[14]+4;
+ Test_psx_dispatch_call(&cpu,resumed_pc,0x1000);
+ } else Test_psx_dispatch_call(&cpu,0xbfc00020,0x1000);
+ psx_cyc_batch_flush();
+ printf("{\"entry_cycles\":%llu,\"cycles\":%llu,\"static_hits\":%llu,\"s1\":%u,\"epc\":%u,\"cause\":%u,\"resumed_pc\":%u,\"pc\":%u,\"fetches\":[",(unsigned long long)entry_cycles,(unsigned long long)psx_cycle_count,(unsigned long long)g_dispatch_static_hits,cpu.gpr[17],cpu.cop0[14],cpu.cop0[13],resumed_pc,cpu.pc);
+ for(unsigned i=0;inear_delay+10000000,"far implicit seek includes distance latency");
+ CHECK(reverse==far_delay,"ReadS uses the same distance rule in reverse direction");
+ /* Same target while already reading must preserve the buffered stream. */
+ setup(100); reading=1; stat_reg=CDSTAT_MOTOR|CDSTAT_READ;
+ read_delay=12345; RB_.size=2048; RB_.pos=4;
+ target(100); command(6);
+ CHECK(read_delay==12345&&RB_.size==2048&&RB_.pos==4,"same-stream read is not restarted");
+ /* Existing command latency is independent of the long sector deadline. */
+ setup(103948); target(251926); command(6);
+ CHECK(cdrom_irq_present_due==CDROM_IRQ_PRESENT_DELAY,"ACK presentation latency unchanged");
+ ack(); command(9); ack(); advance(100000000);
+ CHECK(reads==0,"Pause cancels an implicit seek before any sector is delivered");
+ /* A data-ready result is asynchronous, unlike a response created inside
+ * the guest's command store. Its readable flag and INTC line must agree.
+ * Otherwise the SDK can clear a newly visible INT1 before INTC ever sees
+ * it when preparing its next GetStat, losing the first sector header. */
+ setup(100); target(200); command(6); ack(); i_stat=0; irq_raises=0;
+ advance(read_delay);
+ CHECK(irq_flag==CDIRQ_DATA_READY,"first target owns the visible response");
+ CHECK((i_stat&(1u<<2))!=0,"async INT1 reaches INTC when the ready flag becomes visible");
+ CdTimingPub timing;
+ CHECK(cdrom_timing_record(cdrom_timing_total()-1,&timing)&&
+ timing.lba==200&&timing.intc_cycle==psx_cycle_count&&
+ timing.irq_arm_cycle==timing.intc_cycle,
+ "immediate INTC timestamp belongs to the sector that raised it");
+ command(1);
+ CHECK(queued_cmd.pending&&irq_flag==CDIRQ_DATA_READY,
+ "GetStat cannot replace an outstanding sector response");
+ CHECK(last_sector_lba==200&&s_ring_read==s_ring_write,
+ "queued GetStat preserves the first target ring owner");
+ advance(2472);
+ CHECK(irq_raises==1,"unacked asynchronous INT1 raises INTC once");
+ ack(); i_stat=0; irq_raises=0;
+ CHECK(irq_flag==CDIRQ_ACK,"acknowledging data-ready releases queued GetStat");
+ advance(CDROM_IRQ_PRESENT_DELAY-1);
+ CHECK(i_stat==0,"queued command retains command-response delay");
+ advance(1);
+ CHECK((i_stat&(1u<<2))&&irq_raises==1,"queued command ACK presents once at its own deadline");
+ /* An interrupt mask suppresses INTC, without hiding the controller flag. */
+ setup(100); target(200); command(6); ack(); irq_enable=0; i_stat=0;
+ advance(read_delay);
+ CHECK(irq_flag==CDIRQ_DATA_READY&&i_stat==0,"masked async result retains its pending flag");
+ irq_enable=7; advance(0);
+ CHECK((i_stat&(1u<<2))!=0,"enabling a pending async result presents INTC");
+ /* Source-profile deadline control: a two-slot source pipeline does not
+ * present the first sector until its third fetch. Default stays two. */
+ for (int double_speed=0; double_speed<=1; ++double_speed) {
+ SET_MODEL(""); setup(100); mode_reg=double_speed?0x80:0;
+ target(200); command(6); int baseline=read_delay;
+ SET_MODEL("octoshock-2.2.2-pipeline");
+ setup(100); mode_reg=double_speed?0x80:0; target(200); command(6);
+ int period=double_speed?225792:451584;
+ CHECK(read_delay==baseline+period,"profile adds exactly one sector period to first delivery");
+ ack(); advance(baseline); CHECK(reads==0,"profile target not presented at old deadline");
+ advance(period-1); CHECK(reads==0,"profile target not presented before third-fetch deadline");
+ advance(1); CHECK(reads==1&&last_sector_lba==200,"profile presents requested target at third-fetch deadline");
+ ack(); advance(period); CHECK(reads==2&&last_sector_lba==201,"profile preserves subsequent sector cadence");
+ }
+ SET_MODEL("");
+ if(failures) { fprintf(stderr,"FAILED (%d)\n",failures); return 1; }
+ puts("CD implicit seek: ALL PASS"); return 0;
+}
diff --git a/runtime/tests/test_cdrom_random_tape.c b/runtime/tests/test_cdrom_random_tape.c
new file mode 100644
index 000000000..24a5f5b0d
--- /dev/null
+++ b/runtime/tests/test_cdrom_random_tape.c
@@ -0,0 +1,45 @@
+#include "cdrom_random_tape.h"
+static int failures;
+#define CHECK(x) do { if (!(x)) { fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#x); failures++; } } while(0)
+int main(int argc,char **argv) {
+ CdRandomTape tape={0}; uint32_t value=99;
+ static uint8_t authored[]={3,0,0,0,2,0,0,0,0,0,0,0};
+ tape.bytes=malloc(sizeof(authored));memcpy(tape.bytes,authored,sizeof(authored));tape.count=3;
+ CHECK(cd_tape_bounded(&tape,2,&value) && value==2 && tape.cursor==2);
+ CHECK(cd_tape_bounded(&tape,0,&value) && value==0 && tape.cursor==3);
+ value=99;CHECK(!cd_tape_bounded(&tape,5,&value) && value==99 && tape.cursor==3);
+ uint8_t changed[32]={1};
+ CHECK(!cd_tape_restore(&tape,changed,3,1) && tape.cursor==3);
+ CHECK(!cd_tape_restore(&tape,tape.sha256,2,1) && tape.cursor==3);
+ CHECK(!cd_tape_restore(&tape,tape.sha256,3,4) && tape.cursor==3);
+ CHECK(cd_tape_restore(&tape,tape.sha256,3,1) && tape.cursor==1);
+ CHECK(cd_tape_bounded(&tape,2,&value) && value==2 && tape.cursor==2);
+ cd_tape_reset(&tape);CHECK(tape.cursor==0);
+ CHECK(cd_tape_bounded(&tape,UINT32_MAX,&value) && value==3 && tape.cursor==1);
+ cd_tape_free(&tape);
+ if (argc==3 && !strcmp(argv[1],"--reject")) {
+ CHECK(!cd_tape_load(&tape,argv[2]));
+ }
+ if (argc==2) {
+ CHECK(cd_tape_load(&tape,argv[1]));
+ printf("tape SHA256 ");for(unsigned i=0;i<32;i++)printf("%02x",tape.sha256[i]);puts("");
+ static const uint32_t vectors[][3]={
+ {3000,1137,3},{3000,103,1},{3000,900,1},{3000,952,2},{3000,2893,1},
+ {25000,24420,1},{3000,2573,1},{3000,637,1},{3000,1936,2},{3000,2431,3},
+ {25000,5306,5},{3000,2586,1},{3000,2704,1},{25000,22008,1},{3000,1775,1},
+ {3000,2376,1},{3000,2138,2},{25000,19069,1},{3000,1518,2},{3000,1518,1},
+ {25000,9362,3},{3000,1698,1}};
+ for (unsigned i=0;is_source_ready_due?s_source_command_due:s_source_ready_due;
+ CHECK(due>=psx_cycle_count,"command stage deadline monotonic");
+ advance((int)(due-psx_cycle_count));
+ }
+ CHECK(!queued_cmd.pending,"command admitted within phase bound");
+}
+int main(int argc,char **argv) {
+ if(argc<2)return 2;
+ tape_path=argv[1];
+ if(argc==3) {
+ clock_boot();
+ if(!strcmp(argv[2],"late-argument")) {command(1);cdrom_write(0x1f801802,0);}
+ else if(!strcmp(argv[2],"cdda"))command(3);
+ else if(!strcmp(argv[2],"exhausted")) {s_source_clock_tape.cursor=s_source_clock_tape.count;command(1);}
+ return 1; /* The invalid operation must have stopped the process. */
+ }
+ set_model("PSX_CD_SOURCE_CLOCK_TAPE","");
+ CHECK(baseline_explicit_timing_main()==0,"old explicit timing controls remain passing");
+ for(int size=0;size<=4;size++) {
+ clock_boot();request_reg=CDROM_REQUEST_BFRD;RB_.pos=0;RB_.size=size;
+ uint32_t expected=0;
+ for(int i=0;is_source_ready_due?s_source_command_due:s_source_ready_due;
+ advance((int)(due-psx_cycle_count));
+ }
+ CHECK(!queued_cmd.pending,"command admitted within phase bound");
+}
+
+static uint64_t begin_reset(void) {
+ clock_boot();
+ mode_reg=0x80; read_min=0;read_sec=2;read_sect=10;
+ seek_min=0;seek_sec=2;seek_sect=5;s_setloc_lba=5;
+ cd_muted=1;command(0x0a);receive_command();
+ CHECK(irq_flag==3,"reset acknowledges before drive completion");
+ CHECK(mode_reg==0x80 && read_sect==10,"ACK keeps old head/mode");
+ return psx_cycle_count;
+}
+static void check_reset_state(void) {
+ CHECK(mode_reg==0x20 && read_min==0 && read_sec==2 && read_sect==0,
+ "reset completes with source mode and physical LBA zero");
+ CHECK(seek_min==0 && seek_sec==2 && seek_sect==0 && s_setloc_lba==0,
+ "reset clears command location to LBA zero");
+ CHECK(!cd_muted && s_source_seek_paused,"reset completes demuted and paused");
+}
+int main(int argc,char **argv) {
+ if(argc<2)return 2;tape_path=argv[1];
+ if(argc==3) {
+ begin_reset();ack();
+ if(!strcmp(argv[2],"during-reset")) {command(6);receive_command();}
+ else if(!strcmp(argv[2],"active-read")) {clock_boot();reading=1;read_cmd=6;read_delay=2000000;command(10);receive_command();}
+ return 1;
+ }
+ uint64_t start=begin_reset();ack();
+ advance(485399);CHECK(mode_reg==0x80 && read_sect==10 && !irq_flag,"source frame700 retained state");
+ advance(563976);CHECK(mode_reg==0x80 && read_sect==10 && !irq_flag,"source frame701 retained state");
+ advance(86624);CHECK(mode_reg==0x80 && !irq_flag,"no early reset completion");
+ advance(1);CHECK(psx_cycle_count-start==1136000 && irq_flag==2,"completion at exact source reset deadline");
+ check_reset_state();ack();
+
+ start=begin_reset();ack();advance(300000);
+ command(0x0a);receive_command();ack();
+ advance((int)(start+1135999-psx_cycle_count));
+ CHECK(mode_reg==0x80 && !irq_flag,"repeated reset never completes early");
+ advance(1);CHECK(irq_flag==2,"repeated reset does not rearm physical deadline");check_reset_state();
+
+ start=begin_reset();ack();advance(300000);
+ command(1);receive_command();ack();
+ advance((int)(start+1136000-psx_cycle_count));
+ CHECK(irq_flag==2,"GetStat does not cancel independent drive reset");check_reset_state();
+
+ begin_reset();advance(1136000);
+ CHECK(irq_flag==3,"busy ACK remains owner at drive completion");check_reset_state();
+ ack();advance(2000);CHECK(!irq_flag,"source discards transient blocked reset completion");
+ begin_reset();advance(1135999);ack();advance(1);
+ CHECK(!irq_flag,"receive cooldown also blocks transient reset completion");check_reset_state();
+ advance(2000);CHECK(!irq_flag,"no delayed stale reset completion after cooldown");
+
+ begin_reset();ack();advance(485399);
+ unsigned n=cdrom_snapshot_bytes();uint8_t *wire=malloc(n),*before=malloc(n),*after=malloc(n);
+ cdrom_snapshot_write(wire);advance(650601);check_reset_state();ack();
+ CHECK(cdrom_snapshot_read(wire,n),"active reset snapshot round trip");
+ CHECK(mode_reg==0x80 && read_sect==10,"snapshot restores pre-completion state");
+ advance(650600);CHECK(!irq_flag && mode_reg==0x80,"snapshot preserves remaining reset delay");
+ advance(1);CHECK(irq_flag==2,"snapshot restored deadline completes once");check_reset_state();
+ cdrom_snapshot_write(before);
+ memset(wire+n-4,0xff,4);CHECK(!cdrom_snapshot_read(wire,n),"invalid reset remaining rejected");
+ cdrom_snapshot_write(after);CHECK(!memcmp(before,after,n),"bad reset snapshot changes no state");
+ free(wire);free(before);free(after);
+ begin_reset();clock_boot();advance(1200000);CHECK(!irq_flag && mode_reg==0,"cold reset clears active drive reset");
+
+ set_model("PSX_CD_SOURCE_CLOCK_TAPE","");psx_cycle_count=0;cdrom_init("synthetic");
+ mode_reg=0x80;read_sec=2;read_sect=10;command(10);ack();
+ advance(131071);CHECK(!irq_flag,"default OpenBIOS accommodation remains unchanged");
+ advance(1);CHECK(irq_flag==2 && mode_reg==0x80 && read_sect==10,"default does not acquire source reset model");
+ if(failures){fprintf(stderr,"source reset failures: %d\n",failures);return 1;}
+ puts("CD source reset: ALL PASS");return 0;
+}
diff --git a/runtime/tests/test_compiled_dma_read_wait.c b/runtime/tests/test_compiled_dma_read_wait.c
new file mode 100644
index 000000000..195324473
--- /dev/null
+++ b/runtime/tests/test_compiled_dma_read_wait.c
@@ -0,0 +1,35 @@
+#include "psx_cyc.h"
+#include
+#include
+uint64_t psx_cycle_count,psx_next_service_cycle=1000000;
+uint32_t g_psx_cyc_batch,g_psx_cyc_batch_limit,g_dma_cpu_read_wait; int g_psx_cyc_bb_defer;
+uint32_t *g_psx_cyc_local_acc;
+int psx_in_device_service,g_event_step_conservative,g_ls_replay_active,g_ls_mode;
+volatile int g_ds_recording;
+static uint8_t ram[0x200000];uint8_t *g_psx_ram=ram;
+int g_psx_load_delay=1,g_ram_read_watch_active;
+static unsigned slow_words,slow_halves;
+void psx_devices_service_to_now(void) {assert(0);}
+void psx_advance_cycles_slow(uint32_t n) {psx_cycle_count+=n;}
+int psx_load_delay_enabled(void) {return 1;}
+void debug_server_trace_ram_read_watch(uint32_t a,uint32_t v) {(void)a;(void)v;assert(0);}
+uint32_t psx_cyc_load_word_slow(CPUState *cpu,uint32_t a,uint32_t r,uint32_t m) {
+ (void)cpu;(void)r;(void)m;assert((a&0x1fffff)==0x1000);slow_words++;return 0x12345678;
+}
+uint16_t psx_cyc_load_half_slow(CPUState *cpu,uint32_t a,uint32_t r,uint32_t m) {
+ (void)cpu;(void)r;(void)m;assert((a&0x1fffff)==0x1000);slow_halves++;return 0x5678;
+}
+int main(void) {
+ CPUState cpu={0};cpu.ld_which_t=32;
+ uint32_t v=0x12345678;memcpy(ram+0x1000,&v,4);
+ assert(psx_cyc_load_word(&cpu,0x80001000,2,0)==v && slow_words==0);
+ const uint32_t aliases[]={0x00001000,0x80001000,0xa0001000,0x80201000};
+ g_dma_cpu_read_wait=15;
+ for(unsigned i=0;i<4;i++) {
+ assert(psx_cyc_load_word(&cpu,aliases[i],2,0)==v);
+ assert(psx_cyc_load_half(&cpu,aliases[i],2,0)==0x5678);
+ }
+ if(slow_words!=4 || slow_halves!=4) {puts("FAIL compiled RAM path bypasses active DMA load wait");return 1;}
+ g_dma_cpu_read_wait=0;assert(psx_cyc_load_word(&cpu,0x80001000,2,0)==v && slow_words==4);
+ puts("PASS compiled word/half active-DMA routing, aliases, and unchanged inactive fast path");return 0;
+}
diff --git a/runtime/tests/test_compiled_ram_read_watch.c b/runtime/tests/test_compiled_ram_read_watch.c
new file mode 100644
index 000000000..1b4b3363a
--- /dev/null
+++ b/runtime/tests/test_compiled_ram_read_watch.c
@@ -0,0 +1,38 @@
+#include "psx_cyc.h"
+#include
+#include
+uint64_t psx_cycle_count,psx_next_service_cycle=1000000;
+uint32_t g_psx_cyc_batch,g_psx_cyc_batch_limit; int g_psx_cyc_bb_defer;
+uint32_t g_dma_cpu_read_wait;
+uint32_t *g_psx_cyc_local_acc;
+int psx_in_device_service,g_event_step_conservative,g_ls_replay_active,g_ls_mode;
+volatile int g_ds_recording;
+static uint8_t ram[0x200000]; uint8_t *g_psx_ram=ram;
+int g_psx_load_delay=1,g_ram_read_watch_active;
+static uint32_t observed,count;
+void psx_devices_service_to_now(void) { assert(0); }
+void psx_advance_cycles_slow(uint32_t n) { psx_cycle_count+=n; }
+int psx_load_delay_enabled(void) { return 1; }
+uint32_t psx_cyc_load_word_slow(CPUState *cpu,uint32_t a,uint32_t r,uint32_t m) {
+ (void)cpu;(void)a;(void)r;(void)m;assert(0);return 0;
+}
+uint16_t psx_cyc_load_half_slow(CPUState *cpu,uint32_t a,uint32_t r,uint32_t m) {
+ (void)cpu;(void)a;(void)r;(void)m;assert(0);return 0;
+}
+void debug_server_trace_ram_read_watch(uint32_t a,uint32_t v) {
+ assert(a==0x1000);observed=v;count++;
+}
+int main(void) {
+ CPUState off={0},on={0};off.ld_which_t=on.ld_which_t=32;
+ uint32_t expected=0x12345678; memcpy(ram+0x1000,&expected,4);
+ assert(psx_cyc_load_word(&off,0x80001000,2,0)==expected);assert(count==0);
+ uint64_t cycles=psx_cycle_count;uint32_t deferred=g_psx_cyc_batch;
+ psx_cycle_count=0;g_psx_cyc_batch=0;g_psx_cyc_batch_limit=0;
+ g_ram_read_watch_active=1;
+ assert(psx_cyc_load_word(&on,0x80201000,2,0)==expected);
+ if(count!=1||observed!=expected){puts("FAIL: compiled RAM watch omitted");return 1;}
+ assert(!memcmp(&off,&on,sizeof(off)));assert(cycles==psx_cycle_count&&deferred==g_psx_cyc_batch);
+ assert(psx_cyc_load_half(&on,0x80001000,2,0)==0x5678);
+ assert(count==2&&observed==0x5678);
+ puts("compiled RAM word read watch: value, mirror, disabled watch and timing parity PASS");
+}
diff --git a/runtime/tests/test_cpu_step_boundary.c b/runtime/tests/test_cpu_step_boundary.c
new file mode 100644
index 000000000..61318d647
--- /dev/null
+++ b/runtime/tests/test_cpu_step_boundary.c
@@ -0,0 +1,39 @@
+/* Execute authored output from each real emitter against production objects.
+ * Functional callback and instruction observer have independent enable flags.
+ * Devices/retail traffic are excluded here; cold fetch and instruction effects
+ * establish every generated boundary, including coalesced cached followers. */
+#include "cpu_state.h"
+#include "psx_icache.h"
+#include "psx_cyc.h"
+#include
+#include
+#include
+extern void TEST_ENTRY(CPUState *);
+extern CPUState *debug_cpu_ptr;
+extern void memory_set_sr_ptr(const uint32_t *);
+static uint32_t base,seen,trace_count;
+static int uncached;
+static void trace(uint32_t pc){(void)pc;trace_count++;}
+static void step(CPUState *cpu,uint32_t pc,uint64_t cycle) {
+ static const uint32_t values[6]={10,1,2,3,4,4};
+ static const uint32_t cached_cycles[6]={0,8,9,10,11,19};
+ assert(seen<6 && pc==base+4*seen && cpu->gpr[8]==values[seen]);
+ assert(cycle==(uncached?5*seen:cached_cycles[seen]));
+ printf("step %08X %llu %u\n",pc,(unsigned long long)cycle,cpu->gpr[8]);seen++;
+}
+int main(int argc,char **argv) {
+ assert(argc==4);base=strtoul(argv[1],0,16);uncached=base>=0xa0000000u;
+ int active=atoi(argv[2]),tracing=atoi(argv[3]);
+ CPUState cpu={0};cpu.gpr[8]=10;cpu.gpr[31]=0x80090000;
+ cpu.read_fudge=32;cpu.ld_which_t=32;debug_cpu_ptr=&cpu;
+ memory_set_sr_ptr(&cpu.cop0[12]);psx_icache_reset();g_psx_icache_active=1;
+ psx_next_service_cycle=1000000;
+ g_input_instruction_histogram_active=tracing;g_input_instruction_histogram_callback=trace;
+ g_psx_cpu_step_boundary_callback=active?step:0;
+ TEST_ENTRY(&cpu);psx_cyc_batch_flush();
+ assert(cpu.gpr[8]==4 && cpu.pc==cpu.gpr[31]);
+ assert(psx_get_cycle_count()==(uncached?30:20));
+ assert(seen==(active?6:0));assert(trace_count==(tracing?(uncached?6:2):0));
+ printf("final %u %u %llu %u %u\n",cpu.gpr[8],cpu.pc,(unsigned long long)psx_get_cycle_count(),seen,trace_count);
+ return 0;
+}
diff --git a/runtime/tests/test_critical_exception.c b/runtime/tests/test_critical_exception.c
new file mode 100644
index 000000000..3ce1aab1d
--- /dev/null
+++ b/runtime/tests/test_critical_exception.c
@@ -0,0 +1,55 @@
+/* Production SYS01/02 entry only; no BIOS, game data, or timing stubs.
+ * LTO discards unrelated scheduler branches for the constant selectors. */
+#include
+#include
+#include
+#include
+#include "cpu_state.h"
+#include "../src/traps.c"
+
+static int nested;
+int psx_get_in_exception(void) { return nested; }
+
+static inline __attribute__((always_inline)) void check(unsigned func, unsigned sr, int enabled) {
+ CPUState cpu = {0}, expected;
+ for (unsigned i = 0; i < 32; ++i) cpu.gpr[i] = 0x12340000u + i;
+ cpu.gpr[4] = func;
+ cpu.pc = 0x80012340u;
+ cpu.cop0[12] = sr;
+ cpu.cop0[13] = 0x8000057Cu;
+ cpu.cop0[14] = 0x80099990u;
+ cpu.hi = 0xAABBCCDDu; cpu.lo = 0xEEFF0011u;
+ expected = cpu;
+ if (enabled) {
+ expected.cop0[14] = cpu.pc;
+ expected.cop0[13] = 0x520u;
+ expected.cop0[12] = (sr & ~0x3Fu) | ((sr & 15u) << 2);
+ expected.pc = sr & 0x400000u ? 0xBFC00180u : 0x80000080u;
+ } else {
+ expected.cop0[12] = func == 1 ? sr & ~1u : sr | 0x401u;
+ expected.gpr[2] = func == 1 ? sr & 1u : 0;
+ expected.pc = 0;
+ }
+ int continuation = 0;
+ if (!psx_syscall(&cpu, 0)) continuation = 1;
+ assert(continuation == !enabled);
+ assert(memcmp(&cpu, &expected, sizeof(cpu)) == 0);
+}
+
+int main(int argc, char** argv) {
+ int enabled = argc > 1 && strcmp(argv[1], "exception") == 0;
+#ifdef _WIN32
+ if (enabled) _putenv("PSX_CRITICAL_SECTION_MODEL=exception");
+ else _putenv("PSX_CRITICAL_SECTION_MODEL=");
+#else
+ if (enabled) setenv("PSX_CRITICAL_SECTION_MODEL", "exception", 1);
+ else unsetenv("PSX_CRITICAL_SECTION_MODEL");
+#endif
+ nested = argc > 2;
+ for (unsigned i = 0; i < 64; ++i) {
+ check(1, 0x40000000u | i, enabled);
+ check(2, 0x40400400u | i, enabled);
+ }
+ puts("critical_exception: PASS (128 entry/continuation states)");
+ return 0;
+}
diff --git a/runtime/tests/test_dma_cd_deadline.c b/runtime/tests/test_dma_cd_deadline.c
new file mode 100644
index 000000000..42d36d1ee
--- /dev/null
+++ b/runtime/tests/test_dma_cd_deadline.c
@@ -0,0 +1,91 @@
+/* Actual production scheduler + DMA controller. Other devices expose a single
+ * optional timer boundary. No game, BIOS, MMIO payload model or timing tune. */
+#define PSX_TEST_REAL_CYCLE_SCHEDULER
+#define PSX_TEST_SOURCE_CD_IMPLEMENTED
+#define main cd_fixture_baseline_main
+#include "test_dma_cd_source.c"
+#undef main
+#include "../src/psx_cycles.c"
+
+static uint64_t timer_due, timer_fired_cycle, dma_irq_cycle;
+static unsigned timer_events;
+uint32_t i_mask;
+uint32_t interrupts_cycles_to_vblank(void) { return 1000000; }
+uint32_t timers_cycles_to_irq(uint32_t mask) {
+ (void)mask;
+ return timer_due && !timer_events ? (uint32_t)(timer_due-psx_cycle_count) : 1000000;
+}
+uint32_t cdrom_cycles_to_irq(uint32_t mask) { (void)mask; return 1000000; }
+uint32_t sio_cycles_to_irq(uint32_t mask) { (void)mask; return 1000000; }
+uint32_t psx_spu_sample_event_cycles_to_next(void) { return 1000000; }
+void psx_spu_sample_event_service(void) {}
+void interrupts_service_scheduled_events(void) {}
+void sio_advance(uint32_t cycles) { (void)cycles; }
+void cdrom_advance(uint32_t cycles) { (void)cycles; }
+void timers_advance(uint32_t cycles) {
+ (void)cycles;
+ if (timer_due && !timer_events && psx_cycle_count>=timer_due) {
+ timer_events++;timer_fired_cycle=psx_cycle_count;
+ }
+ if (irqs && !dma_irq_cycle) dma_irq_cycle=psx_cycle_count;
+}
+void interrupts_advance_cycles(uint32_t cycles) { (void)cycles; }
+void starvation_watchdog_check(void) {}
+void starvation_ring_pc_sample(void) {}
+int psx_netplay_active(void) { return 0; }
+int psx_selfcheck_enabled(void) { return 0; }
+int psx_get_in_exception(void) { return 0; }
+void dirty_ram_ld_delay_discard(void) {}
+void dirty_ram_irq_ambient_resync_after_restore(void) {}
+uint64_t g_guest_store_count, g_mmio_access_count;
+int g_ls_mode, g_precise_mode, g_psx_call_bail;
+
+static int run_case(uint64_t start, uint32_t future, int chopped) {
+ setup(512,start);
+ s_devices_synced_cycle=start;s_next_watchdog=s_next_pc_sample=UINT64_MAX;
+ psx_in_device_service=0;g_psx_cycle_fast_limit=0;g_psx_cyc_batch=0;
+ timer_due=future?start+future:0;timer_events=0;timer_fired_cycle=dma_irq_cycle=0;
+ channels[3].madr=0xB070;channels[3].bcr=0x00010200;
+ channels[3].chcr=0;
+ /* Same barrier order as the production memory MMIO wrapper. */
+ psx_devices_mmio_sync();
+ uint64_t cached=psx_next_service_cycle;
+ dma_write(0x1f8010b8u,chopped?0x11000100u:0x11000000u);
+ uint64_t returned=psx_cycle_count;
+ psx_devices_mmio_sync();
+ uint64_t expected=((start+4536u+127u)/128u)*128u;
+ if(chopped) {
+ if(returned!=start || writes!=8 || irqs) return 1;
+ psx_advance_cycles((uint32_t)(expected-start));
+ if(writes!=512 || irqs!=1) return 1;
+ } else if(returned!=expected || writes!=512 || irqs!=1) {
+ fprintf(stderr,"cached deadline: start=%llu cache=%llu returned=%llu expected=%llu words=%u IRQ=%u\n",
+ (unsigned long long)start,(unsigned long long)cached,(unsigned long long)returned,
+ (unsigned long long)expected,writes,irqs);
+ return 1;
+ }
+ if(dma_irq_cycle!=expected) {
+ fprintf(stderr,"DMA IRQ changed: %llu expected %llu\n",(unsigned long long)dma_irq_cycle,(unsigned long long)expected);return 1;
+ }
+ if(future && timer_due<=expected) {
+ if(timer_events!=1 || timer_fired_cycle!=timer_due)return 1;
+ } else if(timer_events)return 1;
+ if(ram[0xB070/4]!=0xCA000000u || ram[(0xB070+2044)/4]!=0xCA0001FFu)return 1;
+ return 0;
+}
+int main(void) {
+ set_option("PSX_INPUT_ROUTE_FILE","authored-fixture");
+ set_option("PSX_CD_DMA_MODEL","octoshock-2.2.2");
+ /* Actual observed CHCR timestamp and quiet 16K scheduler cache. */
+ if(run_case(407914091u,0,0))return 1;
+ for(uint32_t phase=0;phase<128;phase++) {
+ if(run_case(phase,0,0) || run_case(phase,2000,0) ||
+ run_case(phase,9000,0) || run_case(phase,9000,1))return 1;
+ }
+ puts("PASS production CD DMA scheduler: observed trigger plus 512 cache/phase/order controls");
+ return 0;
+}
+
+/* No source GPU consumer in this device-isolation control. */
+void source_gpu_runtime_advance(void) {}
+uint32_t source_gpu_runtime_cycles_to_event(void) {return UINT32_MAX;}
diff --git a/runtime/tests/test_dma_cd_source.c b/runtime/tests/test_dma_cd_source.c
new file mode 100644
index 000000000..5caa82939
--- /dev/null
+++ b/runtime/tests/test_dma_cd_source.c
@@ -0,0 +1,135 @@
+/* Authored production-controller fixture. No BIOS, disc or retail code. */
+#define _POSIX_C_SOURCE 200809L
+#include "../src/dma.c"
+#include
+uint64_t s_frame_count;
+int g_ls_replay_active;
+#ifndef PSX_TEST_REAL_CYCLE_SCHEDULER
+uint64_t psx_cycle_count,psx_next_service_cycle;
+int psx_in_device_service,g_event_step_conservative;
+uint32_t g_psx_cyc_batch,g_psx_cyc_batch_limit;
+#endif
+uint32_t i_stat,g_debug_current_func_addr,g_debug_last_store_pc;
+static uint32_t ram[0x80000],writes,irqs,read_words,available;
+static void set_option(const char *key,const char *value) {
+#ifdef _WIN32
+ _putenv_s(key,value);
+#else
+ setenv(key,value,1);
+#endif
+}
+#ifndef PSX_TEST_REAL_CYCLE_SCHEDULER
+void psx_devices_service_to_now(void) {
+#ifdef PSX_TEST_SOURCE_CD_IMPLEMENTED
+ advance_source_cdrom();
+#endif
+}
+void psx_advance_cycles_slow(uint32_t n) {psx_cycle_count+=n;psx_devices_service_to_now();}
+#endif
+void psx_write_word(uint32_t addr,uint32_t value) {ram[(addr&0x1ffffc)/4]=value;writes++;}
+void psx_irq_raise(uint32_t bit,uint32_t detail) {(void)detail;i_stat|=1u<last_sector_lba=4;s->sector_size=2048;s->sector_available=1;}
+int cdrom_get_setloc_lba(void) {return 4;}
+uint8_t *memory_get_ram_ptr(void) {return (uint8_t*)ram;}
+void overlay_capture_before_dma(uint32_t a,uint32_t n) {(void)a;(void)n;}
+void overlay_capture_on_dma(uint32_t a,uint32_t n,const uint8_t *p) {(void)a;(void)n;(void)p;}
+void dirty_ram_mark_executable_range(uint32_t a,uint32_t n) {(void)a;(void)n;}
+/* Unrelated device entry points must never execute in this fixture. */
+uint64_t g_io_openbus_reads,g_io_openbus_writes;
+uint32_t psx_read_word(uint32_t a) {return ram[(a&0x1ffffc)/4];}
+void psx_fatal_halt(const char *s) {fprintf(stderr,"%s\n",s);abort();}
+int mdec_dma_write_ready(void) {abort();}
+int mdec_dma_read_ready(void) {abort();}
+uint32_t mdec_dma_write_words(const uint32_t *p,uint32_t n) {(void)p;(void)n;abort();}
+void mdec_dma_write_word(uint32_t v) {(void)v;abort();}
+uint32_t mdec_dma_read_word(void) {abort();}
+#define UNUSED_DMA_STUB(name) void name(uint32_t a,uint32_t n) {(void)a;(void)n;abort();}
+UNUSED_DMA_STUB(mdec_debug_dma_in_start)
+UNUSED_DMA_STUB(mdec_debug_dma_out_start)
+UNUSED_DMA_STUB(mdec_debug_dma_in_end)
+UNUSED_DMA_STUB(mdec_debug_dma_out_end)
+UNUSED_DMA_STUB(gpu_set_gp0_linked_list_node)
+uint32_t gpu_read_gpuread(void) {abort();}
+uint32_t gpu_dma_vram_upload_words(void) {abort();}
+void gpu_set_gp0_source(uint32_t a) {(void)a;abort();}
+void gpu_write_gp0(uint32_t v) {(void)v;abort();}
+void gpu_ws_begin_linked_list(void) {abort();}
+void gpu_ws_end_linked_list(void) {abort();}
+void gpu_ws_prepass_linked_list(uint32_t a) {(void)a;abort();}
+uint32_t psx_mod_gpu_dma_resolve_address(uint32_t a) {(void)a;abort();}
+void spu_dma_write(uint32_t v) {(void)v;abort();}
+uint32_t spu_dma_read(void) {abort();}
+void audio_trace_event(uint16_t k,uint32_t a,uint32_t b) {(void)k;(void)a;(void)b;abort();}
+static void setup(uint32_t count,uint64_t phase) {
+ dma_init();memset(ram,0xCC,sizeof(ram));writes=irqs=i_stat=read_words=0;
+ available=count?count:65536;psx_cycle_count=phase;psx_next_service_cycle=0;
+ channels[3].madr=0x10000;channels[3].bcr=count;channels[3].chcr=0x11000000;
+ dpcr|=8u<<12;dicr=(1u<<23)|(1u<<19);
+}
+int main(int argc,char **argv) {
+ set_option("PSX_INPUT_ROUTE_FILE","authored-fixture");
+ set_option("PSX_CD_DMA_MODEL","octoshock-2.2.2");
+#ifdef PSX_TEST_SOURCE_CD_IMPLEMENTED
+ if(argc==2) {
+ setup(512,0);
+ if(!strcmp(argv[1],"request-mode"))channels[3].chcr|=0x200;
+ else if(!strcmp(argv[1],"capture-active")) {
+ start_async_cdrom_transfer();start_source_cdrom();dma_snapshot_write(NULL);return 1;
+ }
+ execute_ch3_cdrom();return 1;
+ }
+#else
+ (void)argc;(void)argv;
+#endif
+ setup(512,27);execute_ch3_cdrom();
+ if(writes!=512 || irqs!=1 || psx_cycle_count!=4608) {
+ fprintf(stderr,"manual CD source wait absent: words=%u irqs=%u cycle=%llu expected=512/1/4608\n",writes,irqs,(unsigned long long)psx_cycle_count);
+ return 1;
+ }
+#ifdef PSX_TEST_SOURCE_CD_IMPLEMENTED
+ for(uint32_t phase=0;phase<128;phase++) {
+ setup(512,phase);start_async_cdrom_transfer();start_source_cdrom();
+ assert(writes==8 && cdrom_async.remaining_words==504 && irqs==0);
+ assert(ram[0x10000/4]==0xCA000000 && ram[0x10020/4]==0xCCCCCCCC);
+ uint64_t expected=((phase+4536+127)/128)*128;
+ psx_cycle_count=expected-1;advance_source_cdrom();
+ assert(cdrom_async.active && irqs==0 && writes<512);
+ psx_cycle_count++;advance_source_cdrom();
+ assert(writes==512 && irqs==1 && !cdrom_async.active);
+ assert(channels[3].madr==0x10800 && ram[0x107FC/4]==0xCA0001FF);
+ psx_cycle_count+=256;advance_source_cdrom();assert(writes==512 && irqs==1);
+ }
+ for(uint32_t count=1;count<=17;count++) {
+ setup(count,0);execute_ch3_cdrom();assert(writes==count && irqs==1);
+ assert((psx_cycle_count==0)==(count<=8));
+ }
+ setup(512,27);channels[3].chcr|=0x100;execute_ch3_cdrom();
+ assert(psx_cycle_count==27 && writes==8 && cdrom_async.active && irqs==0);
+ psx_cycle_count=4608;advance_source_cdrom();assert(writes==512 && irqs==1);
+ setup(1,43);channels[3].chcr=0x11400100;available=0;execute_ch3_cdrom();
+ assert(psx_cycle_count==43 && writes==1 && ram[0x10000/4]==0 && irqs==1 && !cdrom_async.active);
+ setup(12,0);available=2;execute_ch3_cdrom();
+ assert(writes==12 && ram[0x10000/4]==0xCA000000 && ram[0x10004/4]==0xCA000001 && ram[0x10008/4]==0 && irqs==1);
+ setup(0,0);execute_ch3_cdrom();assert(writes==65536 && irqs==1);
+ assert(dma_snapshot_read(NULL,0)==0);
+ set_option("PSX_CD_DMA_MODEL","");setup(512,0);execute_ch3_cdrom();
+ assert(writes==0 && psx_cycle_count==0 && cdrom_async.active);
+#endif
+ puts("PASS source CD manual service phases, partial data, wait, IRQ, short/zero count, restore guard and default control");return 0;
+}
+
+/* LL option is off in this adjacent fixture. */
+int gpu_dma_source_ll_ready(void) {abort();}
+
+/* Source GPU projection is inactive in this isolated controller fixture. */
+int source_gpu_runtime_active(void) {return 0;}
+void source_gpu_runtime_copy(SourceGPUServiceClock *clock, SourceGPUCommandProjection *command) {
+ (void)clock; (void)command;
+ assert(!"inactive source GPU must not be sampled");
+}
+void source_gpu_runtime_dma_write(void) {}
diff --git a/runtime/tests/test_dma_completion_deadline.c b/runtime/tests/test_dma_completion_deadline.c
new file mode 100644
index 000000000..9ea2eaed3
--- /dev/null
+++ b/runtime/tests/test_dma_completion_deadline.c
@@ -0,0 +1,155 @@
+#define PSX_TEST_REAL_CYCLE_SCHEDULER
+/* Authored production-controller fixture. No BIOS, disc or retail code. */
+#define _POSIX_C_SOURCE 200809L
+#include "../src/dma.c"
+#include
+uint64_t s_frame_count;
+int g_ls_replay_active;
+#ifndef PSX_TEST_REAL_CYCLE_SCHEDULER
+uint64_t psx_cycle_count,psx_next_service_cycle;
+int psx_in_device_service,g_event_step_conservative;
+uint32_t g_psx_cyc_batch,g_psx_cyc_batch_limit;
+#endif
+uint32_t i_stat,g_debug_current_func_addr,g_debug_last_store_pc;
+static uint32_t ram[0x80000],writes,irqs,read_words,available,gpu_words,gpu_headers;
+static void set_option(const char *key,const char *value) {
+#ifdef _WIN32
+ _putenv_s(key,value);
+#else
+ setenv(key,value,1);
+#endif
+}
+#ifndef PSX_TEST_REAL_CYCLE_SCHEDULER
+void psx_devices_service_to_now(void) {
+#ifdef PSX_TEST_SOURCE_CD_IMPLEMENTED
+ advance_source_cdrom();
+#endif
+}
+void psx_advance_cycles_slow(uint32_t n) {psx_cycle_count+=n;psx_devices_service_to_now();}
+#endif
+void psx_write_word(uint32_t addr,uint32_t value) {ram[(addr&0x1ffffc)/4]=value;writes++;}
+void psx_irq_raise(uint32_t bit,uint32_t detail) {(void)detail;i_stat|=1u<last_sector_lba=4;s->sector_size=2048;s->sector_available=1;}
+int cdrom_get_setloc_lba(void) {return 4;}
+uint8_t *memory_get_ram_ptr(void) {return (uint8_t*)ram;}
+void overlay_capture_before_dma(uint32_t a,uint32_t n) {(void)a;(void)n;}
+void overlay_capture_on_dma(uint32_t a,uint32_t n,const uint8_t *p) {(void)a;(void)n;(void)p;}
+void dirty_ram_mark_executable_range(uint32_t a,uint32_t n) {(void)a;(void)n;}
+/* Unrelated device entry points must never execute in this fixture. */
+uint64_t g_io_openbus_reads,g_io_openbus_writes;
+uint32_t psx_read_word(uint32_t a) {return ram[(a&0x1ffffc)/4];}
+void psx_fatal_halt(const char *s) {fprintf(stderr,"%s\n",s);abort();}
+int mdec_dma_write_ready(void) {abort();}
+int mdec_dma_read_ready(void) {abort();}
+uint32_t mdec_dma_write_words(const uint32_t *p,uint32_t n) {(void)p;(void)n;abort();}
+void mdec_dma_write_word(uint32_t v) {(void)v;abort();}
+uint32_t mdec_dma_read_word(void) {abort();}
+#define UNUSED_DMA_STUB(name) void name(uint32_t a,uint32_t n) {(void)a;(void)n;abort();}
+UNUSED_DMA_STUB(mdec_debug_dma_in_start)
+UNUSED_DMA_STUB(mdec_debug_dma_out_start)
+UNUSED_DMA_STUB(mdec_debug_dma_in_end)
+UNUSED_DMA_STUB(mdec_debug_dma_out_end)
+void gpu_set_gp0_linked_list_node(uint32_t a,uint32_t n){(void)a;(void)n;gpu_headers++;}
+uint32_t gpu_read_gpuread(void) {abort();}
+uint32_t gpu_dma_vram_upload_words(void) {abort();}
+void gpu_set_gp0_source(uint32_t a) {(void)a;}
+void gpu_write_gp0(uint32_t v) {(void)v;gpu_words++;}
+void gpu_ws_begin_linked_list(void) {}
+void gpu_ws_end_linked_list(void) {}
+void gpu_ws_prepass_linked_list(uint32_t a) {(void)a;}
+uint32_t psx_mod_gpu_dma_resolve_address(uint32_t a) {return a&0x1ffffcu;}
+void spu_dma_write(uint32_t v) {(void)v;abort();}
+uint32_t spu_dma_read(void) {abort();}
+void audio_trace_event(uint16_t k,uint32_t a,uint32_t b) {(void)k;(void)a;(void)b;abort();}
+#include "../src/psx_cycles.c"
+
+static uint64_t timer_due, timer_fired_cycle, dma_irq_cycle;
+static unsigned timer_events;
+uint32_t i_mask;
+uint32_t interrupts_cycles_to_vblank(void) { return 1000000; }
+uint32_t timers_cycles_to_irq(uint32_t mask) {
+ (void)mask;
+ return timer_due && !timer_events ? (uint32_t)(timer_due-psx_cycle_count) : 1000000;
+}
+uint32_t cdrom_cycles_to_irq(uint32_t mask) { (void)mask; return 1000000; }
+uint32_t sio_cycles_to_irq(uint32_t mask) { (void)mask; return 1000000; }
+uint32_t psx_spu_sample_event_cycles_to_next(void) { return 1000000; }
+void psx_spu_sample_event_service(void) {}
+void interrupts_service_scheduled_events(void) {}
+void sio_advance(uint32_t cycles) { (void)cycles; }
+void cdrom_advance(uint32_t cycles) { (void)cycles; }
+void timers_advance(uint32_t cycles) {
+ (void)cycles;
+ if (timer_due && !timer_events && psx_cycle_count>=timer_due) {
+ timer_events++;timer_fired_cycle=psx_cycle_count;
+ }
+ if (irqs && !dma_irq_cycle) dma_irq_cycle=psx_cycle_count;
+}
+void interrupts_advance_cycles(uint32_t cycles) { (void)cycles; }
+void starvation_watchdog_check(void) {}
+void starvation_ring_pc_sample(void) {}
+int psx_netplay_active(void) { return 0; }
+int psx_selfcheck_enabled(void) { return 0; }
+int psx_get_in_exception(void) { return 0; }
+void dirty_ram_ld_delay_discard(void) {}
+void dirty_ram_irq_ambient_resync_after_restore(void) {}
+uint64_t g_guest_store_count, g_mmio_access_count;
+int g_ls_mode, g_precise_mode, g_psx_call_bail;
+
+
+/* This test validates service of the existing advertised completion deadline.
+ * It does not validate the legacy eager GP0 payload or one-cycle/word model. */
+static int run_case(uint64_t start,unsigned words,unsigned future,int empty,int masked) {
+ dma_init();memset(ram,0,sizeof(ram));irqs=i_stat=0;gpu_words=gpu_headers=0;
+ psx_cycle_count=start;s_devices_synced_cycle=start;
+ s_next_watchdog=s_next_pc_sample=UINT64_MAX;
+ psx_in_device_service=0;g_psx_cycle_fast_limit=0;g_psx_cyc_batch=0;
+ g_psx_cyc_batch_limit=0;g_psx_cyc_local_acc=NULL;
+ timer_due=future?start+future:0;timer_events=0;timer_fired_cycle=dma_irq_cycle=0;
+ i_mask=masked?0:8;dpcr|=8u<<8;dicr=(1u<<23)|(1u<<18);
+ ram[0x1000/4]=((words-1)<<24)|0x00ffffff;
+ channels[2].madr=0x1000;channels[2].chcr=0;
+ psx_devices_mmio_sync();
+ if(empty)psx_next_service_cycle=0; /* control: cache already invalid */
+ uint64_t cached=psx_next_service_cycle;
+ dma_write(0x1f8010a8,0x01000401);
+ uint32_t advertised=dma_cycles_to_internal_event();
+ assert(advertised==words && psx_cycle_count==start && !irqs);
+ assert(gpu_words==words-1 && gpu_headers==1);
+ if(words>1)psx_advance_cycles(words-1);
+ assert(!irqs && (channels[2].chcr&(1u<<24)));
+ psx_advance_cycles(1);
+ unsigned timely=irqs==1 && !(channels[2].chcr&(1u<<24));
+ printf("start=%llu words=%u future=%u empty=%d masked=%d cached=%llu now=%llu irq=%u busy=%u timely=%u\n",
+ (unsigned long long)start,words,future,empty,masked,(unsigned long long)cached,
+ (unsigned long long)psx_cycle_count,irqs,(channels[2].chcr>>24)&1,timely);
+ if(!timely){psx_devices_service_to_now();assert(irqs==1);return 1;}
+ if(timer_due && timer_due<=start+words)assert(timer_events==1 && timer_fired_cycle==timer_due);
+ else assert(!timer_events);
+ psx_advance_cycles(100);psx_devices_service_to_now();assert(irqs==1 && gpu_words==words-1);
+ return 0;
+}
+int main(void) {
+ set_option("PSX_GPU_DMA_MODEL","");set_option("PSX_CD_DMA_MODEL","");set_option("PSX_DMA_MODEL","");
+ unsigned failures=0,cases=0;
+ uint64_t starts[]={0,123,54337406};unsigned counts[]={1,7,17},futures[]={0,2,7,100};
+ for(unsigned s=0;s<3;s++)for(unsigned w=0;w<3;w++)for(unsigned t=0;t<4;t++)
+ for(int e=0;e<2;e++)for(int m=0;m<2;m++){cases++;failures+=run_case(starts[s],counts[w],futures[t],e,m);}
+ printf("RESULT cases=%u failures=%u\n",cases,failures);return failures?1:0;
+}
+
+/* LL option is off in this adjacent fixture. */
+int gpu_dma_source_ll_ready(void) {abort();}
+
+/* Source GPU projection is inactive in this isolated controller fixture. */
+int source_gpu_runtime_active(void) {return 0;}
+void source_gpu_runtime_dma_write(void) {}
+
+/* No source GPU consumer in this device-isolation control. */
+void source_gpu_runtime_advance(void) {}
+uint32_t source_gpu_runtime_cycles_to_event(void) {return UINT32_MAX;}
diff --git a/runtime/tests/test_dma_gpu_linked_list_source.c b/runtime/tests/test_dma_gpu_linked_list_source.c
new file mode 100644
index 000000000..24fe0a68f
--- /dev/null
+++ b/runtime/tests/test_dma_gpu_linked_list_source.c
@@ -0,0 +1,140 @@
+/* Authored production-controller fixture. No BIOS, disc or retail code. */
+#define _POSIX_C_SOURCE 200809L
+#include "../src/dma.c"
+#include
+uint64_t s_frame_count,psx_cycle_count,psx_next_service_cycle;
+int psx_in_device_service,g_event_step_conservative,g_ls_replay_active;
+uint32_t g_psx_cyc_batch,g_psx_cyc_batch_limit;
+uint32_t i_stat,g_debug_current_func_addr,g_debug_last_store_pc;
+static uint32_t ram[0x80000],writes,irqs,read_words,available;
+static uint32_t upload_left, uploaded[65536], upload_count;
+static void set_option(const char *key,const char *value) {
+#ifdef _WIN32
+ _putenv_s(key,value);
+#else
+ setenv(key,value,1);
+#endif
+}
+void psx_devices_service_to_now(void) {
+#ifdef PSX_TEST_SOURCE_GPU_IMPLEMENTED
+ advance_source_gpu();
+#ifdef PSX_TEST_SOURCE_LL_IMPLEMENTED
+ advance_source_gpu_ll();
+#endif
+#endif
+}
+void psx_advance_cycles_slow(uint32_t n) {psx_cycle_count+=n;psx_devices_service_to_now();}
+void psx_write_word(uint32_t addr,uint32_t value) {ram[(addr&0x1ffffc)/4]=value;writes++;}
+void psx_irq_raise(uint32_t bit,uint32_t detail) {(void)detail;i_stat|=1u<last_sector_lba=4;s->sector_size=2048;s->sector_available=1;}
+int cdrom_get_setloc_lba(void) {return 4;}
+uint8_t *memory_get_ram_ptr(void) {return (uint8_t*)ram;}
+void overlay_capture_before_dma(uint32_t a,uint32_t n) {(void)a;(void)n;}
+void overlay_capture_on_dma(uint32_t a,uint32_t n,const uint8_t *p) {(void)a;(void)n;(void)p;}
+void dirty_ram_mark_executable_range(uint32_t a,uint32_t n) {(void)a;(void)n;}
+/* Unrelated device entry points must never execute in this fixture. */
+uint64_t g_io_openbus_reads,g_io_openbus_writes;
+uint32_t psx_read_word(uint32_t a) {return ram[(a&0x1ffffc)/4];}
+void psx_fatal_halt(const char *s) {fprintf(stderr,"%s\n",s);abort();}
+int mdec_dma_write_ready(void) {abort();}
+int mdec_dma_read_ready(void) {abort();}
+uint32_t mdec_dma_write_words(const uint32_t *p,uint32_t n) {(void)p;(void)n;abort();}
+void mdec_dma_write_word(uint32_t v) {(void)v;abort();}
+uint32_t mdec_dma_read_word(void) {abort();}
+#define UNUSED_DMA_STUB(name) void name(uint32_t a,uint32_t n) {(void)a;(void)n;abort();}
+UNUSED_DMA_STUB(mdec_debug_dma_in_start)
+UNUSED_DMA_STUB(mdec_debug_dma_out_start)
+UNUSED_DMA_STUB(mdec_debug_dma_in_end)
+UNUSED_DMA_STUB(mdec_debug_dma_out_end)
+void gpu_set_gp0_linked_list_node(uint32_t a,uint32_t b) {(void)a;(void)b;}
+uint32_t gpu_read_gpuread(void) {abort();}
+void gpu_set_gp0_source(uint32_t a) {(void)a;}
+static int ready_state=1;
+int gpu_dma_source_ll_ready(void) {return ready_state;}
+void gpu_write_gp0(uint32_t v) {uploaded[upload_count++]=v;}
+uint32_t gpu_dma_vram_upload_words(void) {return upload_left;}
+void gpu_ws_begin_linked_list(void) {}
+void gpu_ws_end_linked_list(void) {}
+void gpu_ws_prepass_linked_list(uint32_t a) {(void)a;}
+uint32_t psx_mod_gpu_dma_resolve_address(uint32_t a) {return a;}
+void spu_dma_write(uint32_t v) {(void)v;abort();}
+uint32_t spu_dma_read(void) {abort();}
+void audio_trace_event(uint16_t k,uint32_t a,uint32_t b) {(void)k;(void)a;(void)b;abort();}
+
+static void setup(uint32_t words,int ready) {
+ dma_init();memset(ram,0,sizeof(ram));irqs=i_stat=upload_count=0;
+ ready_state=ready;psx_cycle_count=27;psx_next_service_cycle=0;
+ ram[0x1000/4]=(words<<24)|0xffffff;
+ channels[2].madr=0x1000;channels[2].bcr=0;
+ channels[2].chcr=0x01000401;dpcr|=8u<<8;dicr=(1u<<23)|(1u<<18);
+}
+int main(int argc,char **argv) {
+ set_option("PSX_INPUT_ROUTE_FILE","authored-fixture");
+#ifdef PSX_TEST_SOURCE_LL_IMPLEMENTED
+ set_option("PSX_GPU_DMA_MODEL","octoshock-2.2.2-bounded-linked-list");
+#else
+ set_option("PSX_GPU_DMA_MODEL","");
+#endif
+ if(argc==2) {
+ setup(65,1);
+ if(!strcmp(argv[1],"parser"))ready_state=-1;
+ else if(!strcmp(argv[1],"draw"))ram[0x1004/4]=0x20000000;
+ else if(!strcmp(argv[1],"reverse"))channels[2].chcr&=~1u;
+ else if(!strcmp(argv[1],"address"))channels[2].madr=0x800000;
+ else if(!strcmp(argv[1],"replace")){try_execute(2);dma_write(0x1f8010a0,0x2000);return 1;}
+ else if(!strcmp(argv[1],"dpcr")){try_execute(2);dma_write(0x1f8010f0,dpcr^0x800);return 1;}
+ else if(!strcmp(argv[1],"capture")){try_execute(2);dma_snapshot_write(NULL);return 1;}
+ try_execute(2);return 1;
+ }
+ /* Ten independent original-DLL expectations: GPU-ready x 0/6/49/50/65.
+ * Only immediate completion/busy and eventual release are asserted here.
+ * Source receipt owns exact guest read timing and source GPU behavior. */
+ const unsigned words[]={0,6,49,50,65};
+ for(int ready=0;ready<2;ready++)for(unsigned i=0;i<5;i++) {
+ unsigned n=words[i];setup(n,ready);try_execute(2);
+ unsigned expected=ready&&n<=49;
+ if(irqs!=expected || !!(channels[2].chcr&(1u<<24))==expected ||
+ (!ready && upload_count)) {
+ fprintf(stderr,"source startup mismatch ready=%d words=%u irq=%u expected=%u busy=%u sent=%u\n",ready,n,irqs,expected,!!(channels[2].chcr&(1u<<24)),upload_count);return 1;
+ }
+#ifdef PSX_TEST_SOURCE_LL_IMPLEMENTED
+ psx_cycle_count=512;advance_source_gpu_ll();
+ if(!ready){assert(!upload_count&&!irqs);ready_state=1;}
+ psx_cycle_count=640;advance_source_gpu_ll();
+ assert(upload_count==n&&irqs==1&&channels[2].chcr==0x401);
+ psx_cycle_count=768;advance_source_gpu_ll();assert(irqs==1&&upload_count==n);
+#endif
+ }
+#ifdef PSX_TEST_SOURCE_LL_IMPLEMENTED
+ /* Generic PSX-DMA-001 ownership: a future payload and future node remain
+ * live after kick. Neither may be snapshotted by a metadata prepass. */
+ setup(65,1);try_execute(2);assert(upload_count==49);
+ ram[(0x1004+49*4)/4]=0xe1000001;
+ psx_cycle_count=128;advance_source_gpu_ll();assert(uploaded[49]==0xe1000001&&irqs==1);
+ setup(49,1);ram[0x1000/4]=(49u<<24)|0x2000;ram[0x2000/4]=0xffffff;
+ try_execute(2);assert(upload_count==49&&!irqs);
+ ram[0x2000/4]=(1u<<24)|0xffffff;ram[0x2004/4]=0xe2000002;
+ psx_cycle_count=128;advance_source_gpu_ll();assert(upload_count==50&&uploaded[49]==0xe2000002&&irqs==1);
+ /* Source does not bank positive credit while not ready. */
+ setup(255,0);try_execute(2);psx_cycle_count=512;advance_source_gpu_ll();
+ ready_state=1;psx_cycle_count=640;advance_source_gpu_ll();assert(upload_count==113&&!irqs);
+ /* Partial service precedes DICR replacement: completion sees old mask. */
+ setup(50,1);dicr=1u<<23;try_execute(2);psx_cycle_count=28;
+ dma_write(0x1f8010f4,(1u<<23)|(1u<<18));assert(upload_count==50&&!irqs);
+ setup(50,1);try_execute(2);psx_cycle_count=28;
+ dma_write(0x1f8010f4,0);assert(upload_count==50&&irqs==1);
+ assert(!dma_snapshot_read(NULL,0));
+ set_option("PSX_GPU_DMA_MODEL","");setup(6,1);try_execute(2);
+ assert(upload_count==6&&!irqs&&delayed_complete[2].active);
+#endif
+ puts("PASS ten source startup states,49/50 boundary,live future words/nodes,stalls,write ordering,default");return 0;
+}
+
+/* Source GPU projection is inactive in this isolated controller fixture. */
+int source_gpu_runtime_active(void) {return 0;}
+void source_gpu_runtime_dma_write(void) {}
diff --git a/runtime/tests/test_dma_gpu_upload_source.c b/runtime/tests/test_dma_gpu_upload_source.c
new file mode 100644
index 000000000..51eac3880
--- /dev/null
+++ b/runtime/tests/test_dma_gpu_upload_source.c
@@ -0,0 +1,153 @@
+/* Authored production-controller fixture. No BIOS, disc or retail code. */
+#define _POSIX_C_SOURCE 200809L
+#include "../src/dma.c"
+#include
+uint64_t s_frame_count,psx_cycle_count,psx_next_service_cycle;
+int psx_in_device_service,g_event_step_conservative,g_ls_replay_active;
+uint32_t g_psx_cyc_batch,g_psx_cyc_batch_limit;
+uint32_t i_stat,g_debug_current_func_addr,g_debug_last_store_pc;
+static uint32_t ram[0x80000],writes,irqs,read_words,available;
+static uint32_t upload_left, uploaded[65536], upload_count;
+static void set_option(const char *key,const char *value) {
+#ifdef _WIN32
+ _putenv_s(key,value);
+#else
+ setenv(key,value,1);
+#endif
+}
+void psx_devices_service_to_now(void) {
+#ifdef PSX_TEST_SOURCE_GPU_IMPLEMENTED
+ advance_source_gpu();
+#endif
+}
+void psx_advance_cycles_slow(uint32_t n) {psx_cycle_count+=n;psx_devices_service_to_now();}
+void psx_write_word(uint32_t addr,uint32_t value) {ram[(addr&0x1ffffc)/4]=value;writes++;}
+void psx_irq_raise(uint32_t bit,uint32_t detail) {(void)detail;i_stat|=1u<last_sector_lba=4;s->sector_size=2048;s->sector_available=1;}
+int cdrom_get_setloc_lba(void) {return 4;}
+uint8_t *memory_get_ram_ptr(void) {return (uint8_t*)ram;}
+void overlay_capture_before_dma(uint32_t a,uint32_t n) {(void)a;(void)n;}
+void overlay_capture_on_dma(uint32_t a,uint32_t n,const uint8_t *p) {(void)a;(void)n;(void)p;}
+void dirty_ram_mark_executable_range(uint32_t a,uint32_t n) {(void)a;(void)n;}
+/* Unrelated device entry points must never execute in this fixture. */
+uint64_t g_io_openbus_reads,g_io_openbus_writes;
+uint32_t psx_read_word(uint32_t a) {return ram[(a&0x1ffffc)/4];}
+void psx_fatal_halt(const char *s) {fprintf(stderr,"%s\n",s);abort();}
+int mdec_dma_write_ready(void) {abort();}
+int mdec_dma_read_ready(void) {abort();}
+uint32_t mdec_dma_write_words(const uint32_t *p,uint32_t n) {(void)p;(void)n;abort();}
+void mdec_dma_write_word(uint32_t v) {(void)v;abort();}
+uint32_t mdec_dma_read_word(void) {abort();}
+#define UNUSED_DMA_STUB(name) void name(uint32_t a,uint32_t n) {(void)a;(void)n;abort();}
+UNUSED_DMA_STUB(mdec_debug_dma_in_start)
+UNUSED_DMA_STUB(mdec_debug_dma_out_start)
+UNUSED_DMA_STUB(mdec_debug_dma_in_end)
+UNUSED_DMA_STUB(mdec_debug_dma_out_end)
+UNUSED_DMA_STUB(gpu_set_gp0_linked_list_node)
+uint32_t gpu_read_gpuread(void) {abort();}
+void gpu_set_gp0_source(uint32_t a) {(void)a;}
+void gpu_write_gp0(uint32_t v) {assert(upload_left);upload_left--;uploaded[upload_count++]=v;}
+uint32_t gpu_dma_vram_upload_words(void) {return upload_left;}
+void gpu_ws_begin_linked_list(void) {abort();}
+void gpu_ws_end_linked_list(void) {abort();}
+void gpu_ws_prepass_linked_list(uint32_t a) {(void)a;abort();}
+uint32_t psx_mod_gpu_dma_resolve_address(uint32_t a) {(void)a;abort();}
+void spu_dma_write(uint32_t v) {(void)v;abort();}
+uint32_t spu_dma_read(void) {abort();}
+void audio_trace_event(uint16_t k,uint32_t a,uint32_t b) {(void)k;(void)a;(void)b;abort();}
+static void setup(uint32_t blocks,uint32_t words,uint64_t phase) {
+ dma_init();memset(ram,0,sizeof(ram));irqs=i_stat=upload_count=0;
+ for(uint32_t i=0;i<65536;i++)ram[0x10000/4+i]=0xAB000000+i;
+ upload_left=blocks*words;psx_cycle_count=phase;psx_next_service_cycle=0;
+ channels[2].madr=0x10000;channels[2].bcr=(blocks<<16)|words;
+ channels[2].chcr=0x01000201;dpcr|=8u<<8;dicr=(1u<<23)|(1u<<18);
+}
+int main(int argc,char **argv) {
+ set_option("PSX_INPUT_ROUTE_FILE","authored-fixture");
+ set_option("PSX_GPU_DMA_MODEL","octoshock-2.2.2-vram-upload");
+ setup(12,16,27);
+#ifdef PSX_TEST_SOURCE_GPU_IMPLEMENTED
+ if(argc==3 && !strcmp(argv[1],"--oracle")) {
+ FILE *f=fopen(argv[2],"r");assert(f);
+ for(unsigned line=0;line<512;line++) {
+ unsigned phase,t,count,madr,bcr,chcr,flags;
+ assert(fscanf(f,"%u %u %u %u %u %u %u",&phase,&t,&count,&madr,&bcr,&chcr,&flags)==7);
+ if(line%4==0){setup(12,16,phase);try_execute(2);}
+ psx_cycle_count=t;advance_source_gpu();
+ if(upload_count!=count || channels[2].madr!=madr || channels[2].bcr!=bcr ||
+ channels[2].chcr!=chcr || irqs!=!!(flags&4)) {
+ fprintf(stderr,"source oracle mismatch phase=%u time=%u words=%u expected=%u IRQ=%u expected=%u\n",phase,t,upload_count,count,irqs,!!(flags&4));return 1;
+ }
+ }
+ for(unsigned i=0;i<2;i++) {
+ char kind[8];unsigned t,count,chcr,flags;
+ assert(fscanf(f,"%7s %u %u %u %u",kind,&t,&count,&chcr,&flags)==5&&!strcmp(kind,"WRITE"));
+ setup(12,16,127);dicr=1u<<23;try_execute(2);
+ psx_cycle_count=128;advance_source_gpu();psx_cycle_count=256;advance_source_gpu();
+ psx_cycle_count=t;dma_write(0x1f8010f4,(1u<<23)|(1u<<18));
+ assert(upload_count==count && channels[2].chcr==chcr && irqs==!!(flags&4));
+ assert(fscanf(f,"%7s %u %u %u %u",kind,&t,&count,&chcr,&flags)==5&&!strcmp(kind,"END"));
+ psx_cycle_count=384;advance_source_gpu();
+ assert(upload_count==count && channels[2].chcr==chcr && irqs==!!(flags&4));
+ }
+ unsigned extra;assert(fscanf(f,"%u",&extra)==EOF);fclose(f);
+ puts("PASS exact-source512phase states and two completion/write-order pairs");return 0;
+ }
+ if(argc==2) {
+ if(!strcmp(argv[1],"no-upload"))upload_left=0;
+ else if(!strcmp(argv[1],"short-upload"))upload_left=191;
+ else if(!strcmp(argv[1],"reverse"))channels[2].chcr&=~1u;
+ else if(!strcmp(argv[1],"chopped"))channels[2].chcr|=0x100;
+ else if(!strcmp(argv[1],"replace")) {try_execute(2);dma_write(0x1f8010a0,0x20000);return 1;}
+ else if(!strcmp(argv[1],"dpcr")) {try_execute(2);dma_write(0x1f8010f0,dpcr^(8u<<8));return 1;}
+ else if(!strcmp(argv[1],"capture")) {try_execute(2);dma_snapshot_write(NULL);return 1;}
+ try_execute(2);return 1;
+ }
+#else
+ (void)argc;(void)argv;
+#endif
+ try_execute(2);
+ if(upload_count!=43 || psx_cycle_count!=27 || irqs) {
+ fprintf(stderr,"GPU upload source start absent: words=%u cycle=%llu irq=%u expected43/27/0\n",upload_count,(unsigned long long)psx_cycle_count,irqs);return 1;
+ }
+#ifdef PSX_TEST_SOURCE_GPU_IMPLEMENTED
+ assert(dma_cpu_read_penalty()==15);
+ assert(channels[2].bcr==0x00090010 && channels[2].madr==0x10080);
+ assert(uploaded[42]==0xAB00002A);
+ ram[0x10000/4+43]=0xDEADBEEF; /* unread future RAM must remain live */
+ psx_cycle_count=127;advance_source_gpu();assert(upload_count==43);
+ psx_cycle_count=128;advance_source_gpu();assert(upload_count>43 && uploaded[43]==0xDEADBEEF && !irqs);
+ psx_cycle_count=255;advance_source_gpu();assert(upload_count<192 && dma_cpu_read_penalty()==15);
+ psx_cycle_count=256;advance_source_gpu();assert(upload_count==192 && irqs==1 && dma_cpu_read_penalty()==0);
+ assert(channels[2].madr==0x10300 && channels[2].bcr==16 && !(channels[2].chcr&(1u<<24)));
+ psx_cycle_count=512;advance_source_gpu();assert(upload_count==192 && irqs==1);
+ for(uint32_t phase=0;phase<128;phase++) {
+ setup(12,16,phase);try_execute(2);
+ assert(upload_count==43 && dma_cycles_to_internal_event()==128-phase);
+ /* Independent source oracle establishes 276 total clocks for this
+ * transfer, of which64 are supplied at kick. Service uses actual
+ * elapsed time; the first partial interval is not a full128. */
+ uint32_t end=((phase+212u+127u)/128u)*128u;
+ psx_cycle_count=end-1;advance_source_gpu();assert(upload_count<192);
+ psx_cycle_count=end;advance_source_gpu();assert(upload_count==192 && irqs==1);
+ }
+ setup(1,1,0);try_execute(2);assert(upload_count==1 && irqs==1 && dma_cpu_read_penalty()==0);
+ setup(2,256,0);try_execute(2);assert(dma_cpu_read_penalty()==200);
+ assert(!dma_snapshot_read(NULL,0));
+ set_option("PSX_GPU_DMA_MODEL","");setup(12,16,0);try_execute(2);
+ assert(upload_count==192 && !irqs && dma_cpu_read_penalty()==0 && delayed_complete[2].active);
+#endif
+ puts("PASS source VRAM request DMA phases, live RAM, block overhead, read penalty, partial completion, IRQ and default");return 0;
+}
+
+/* LL option is off in this adjacent fixture. */
+int gpu_dma_source_ll_ready(void) {abort();}
+
+/* Source GPU projection is inactive in this isolated controller fixture. */
+int source_gpu_runtime_active(void) {return 0;}
+void source_gpu_runtime_dma_write(void) {}
diff --git a/runtime/tests/test_dma_otc_timing.c b/runtime/tests/test_dma_otc_timing.c
new file mode 100644
index 000000000..2c638614d
--- /dev/null
+++ b/runtime/tests/test_dma_otc_timing.c
@@ -0,0 +1,57 @@
+/* Synthetic controller test: no BIOS, retail code, or disc is needed.
+ * Include the implementation to isolate channel 6 from unrelated devices. */
+#include "../src/dma.c"
+#include
+uint64_t s_frame_count;
+uint64_t psx_cycle_count, psx_next_service_cycle;
+int psx_in_device_service, g_event_step_conservative, g_ls_replay_active;
+uint32_t g_psx_cyc_batch, g_psx_cyc_batch_limit;
+uint32_t i_stat, g_debug_current_func_addr, g_debug_last_store_pc;
+static uint32_t ram[0x80000], writes, irqs;
+void psx_devices_service_to_now(void) { advance_source_otc(); }
+void psx_advance_cycles_slow(uint32_t n) { psx_cycle_count+=n; advance_source_otc(); }
+void psx_write_word(uint32_t addr, uint32_t value) { ram[(addr & 0x1ffffc)/4]=value; writes++; }
+void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)detail; i_stat|=1u<
+extern void memory_init(const char *);
+extern void memory_set_sr_ptr(const uint32_t *);
+extern uint32_t psx_read_word(uint32_t);
+extern uint16_t psx_read_half(uint32_t);
+extern uint8_t psx_read_byte(uint32_t);
+extern void psx_write_word(uint32_t,uint32_t);
+extern void psx_write_half(uint32_t,uint16_t);
+extern void psx_write_byte(uint32_t,uint8_t);
+extern CPUState *debug_cpu_ptr;
+extern void psx_rfe_escape_check(CPUState *);
+static CPUState *observed;
+static unsigned boundary_pending, boundary_count, store_count, handler_count;
+#ifdef TEST_GPU_CONSUMER
+static void (*consumer)(CPUState*,uint32_t,uint64_t);
+#endif
+static uint32_t ram_word(unsigned offset) {
+ uint32_t value;memcpy(&value,memory_get_ram_ptr()+offset,4);return value;
+}
+static void counted_write(uint32_t address,uint32_t value) {
+ if((address&0x1fffffff)==0x1000)store_count++;
+ if((address&0x1fffffff)==0x1004)handler_count++;
+ psx_write_word(address,value);
+}
+static void authored_call(CPUState *cpu,uint32_t pc,uint32_t stop) {
+ for(unsigned i=0;i<100;i++) {
+ if(pc==stop || pc==0xbfc00200u){cpu->pc=pc;return;}
+ cpu->pc=pc;
+ psx_check_interrupts_at(cpu,pc);
+ assert(dirty_ram_dispatch(cpu,pc,stop));
+ psx_rfe_escape_check(cpu);pc=cpu->pc;assert(pc);
+ }
+ assert(!"authored dispatch bound");
+}
+static void authored_dispatch(CPUState *cpu,uint32_t pc) {
+ authored_call(cpu,pc,cpu->cop0[14]);
+}
+static const PsxBiosImageInfo authored_info={.image_id="AUTHORED-FRAME-IRQ",.image_sha256="authored"};
+static const PsxBiosBackend authored_backend={&authored_info,authored_dispatch,authored_call,0,0};
+static void blank(void) {
+ boundary_pending=1;
+ printf("vblank %llu\n",(unsigned long long)psx_get_cycle_count());
+}
+static void sample(uint32_t pc) {
+ printf("before_fetch %08X %llu\n",pc,(unsigned long long)psx_get_cycle_count());
+}
+static void functional_boundary(CPUState *cpu,uint32_t pc,uint64_t cycle) {
+ assert(cpu==observed);
+#ifdef TEST_GPU_CONSUMER
+ consumer(cpu,pc,cycle);
+ SourceGPUServiceClock clock;SourceGPUCommandProjection s;
+ source_gpu_runtime_copy_return(&clock,&s);
+ if(clock.frame_returns>boundary_count) {
+ printf("gpu_return %llu %u %u %u %u %llu %u %u\n",(unsigned long long)clock.cycle,(uint32_t)s.budget,s.phase,s.count,(unsigned)source_gpu_command_ready(&s),(unsigned long long)s.last_update,s.command,s.count?s.queue[0]:0);
+ }
+#endif
+ if(boundary_pending && observed) {
+ boundary_pending=0;boundary_count++;
+ printf("boundary %08X %llu %u %u %u %u %u\n",pc,
+ (unsigned long long)cycle,observed->gpr[8],
+ ram_word(0x1000),ram_word(0x1004),observed->cop0[12],observed->cop0[14]);
+ assert(cycle==1117177);
+ assert(!ram_word(0x1000) && !ram_word(0x1004));
+ }
+}
+int main(int argc,char **argv) {
+ assert(argc==3 || argc==6);memory_init(argv[1]);
+ unsigned ram_size=argc==3?0x3000:0x2000;
+ FILE *ram=fopen(argv[2],"rb");assert(ram);assert(fread(memory_get_ram_ptr(),1,ram_size,ram)==ram_size);fclose(ram);
+ timers_init();dma_init();interrupts_init();
+ if(argc==3)assert(psx_read_word(0xa0000200)==0x08000080 && psx_read_word(0xa0000204)==0);
+ CPUState cpu={0};cpu.read_word=psx_read_word;cpu.read_half=psx_read_half;cpu.read_byte=psx_read_byte;
+ cpu.write_word=psx_write_word;cpu.write_half=psx_write_half;cpu.write_byte=psx_write_byte;
+ cpu.read_fudge=32;cpu.ld_which_t=32;cpu.cop0[13]=0x400;
+ debug_cpu_ptr=&cpu;memory_set_sr_ptr(&cpu.cop0[12]);psx_icache_reset();g_psx_icache_active=1;
+ if(argc==6) {
+ int irq=atoi(argv[3]),branch=atoi(argv[4]);observed=&cpu;
+ cpu.gpr[8]=10;cpu.gpr[16]=0x1000;cpu.gpr[17]=0x1004;
+ cpu.gpr[24]=0xbfc00200u;cpu.gpr[29]=0x1ff000;
+ cpu.cop0[12]=irq==2?1:0x101;cpu.cop0[13]=irq?0x100:0;
+ cpu.write_word=counted_write;cpu.pc=0xa0000508u;
+ dirty_ram_mark_executable_range(0,0x2000);psx_bios_activate(&authored_backend);
+#ifdef TEST_GPU_CONSUMER
+ source_gpu_runtime_init();consumer=g_psx_cpu_step_boundary_callback;
+#endif
+ psx_advance_cycles(1117172);psx_devices_service_to_now();
+ gpu_set_vblank_callback(blank);g_input_instruction_histogram_active=atoi(argv[5]);
+ g_input_instruction_histogram_callback=sample;g_precise_mode=1;g_dirty_interp_active=1;
+ g_psx_cpu_step_boundary_callback=functional_boundary;
+ if(irq==1)psx_check_interrupts_at(&cpu,cpu.pc);
+ assert(cpu.pc==0xa0000508u);
+ uint32_t next=0;int moved=exec_one(&cpu,cpu.pc,&next);psx_cyc_batch_flush();
+ uint32_t continuation=moved?cpu.pc:next;
+ if(!branch)psx_cpu_step_boundary(&cpu,continuation);
+ assert(boundary_count==1 && !boundary_pending);
+ assert(cpu.gpr[8]==11 && !ram_word(0x1000));
+ authored_call(&cpu,continuation,0xbfc00200u);psx_cyc_batch_flush();
+ assert(cpu.gpr[8]==11 && ram_word(0x1000)==11 && store_count==1);
+ assert(handler_count==(irq==1) && ram_word(0x1004)==(irq==1));
+ assert(cpu.cop0[14]==(irq==1?0xa0000508u:0));
+ printf("final %08X %u %u %u %u %u\n",cpu.pc,cpu.gpr[8],ram_word(0x1000),store_count,handler_count,boundary_count);
+ return 0;
+ }
+ /* Source callback1117166 observes this uncached J after its four-cycle fetch.
+ * Advance real devices from cold to the corresponding pre-fetch boundary. */
+ psx_advance_cycles(1117162);psx_devices_service_to_now();
+ gpu_set_vblank_callback(blank);
+ g_input_instruction_histogram_active=1;g_input_instruction_histogram_callback=sample;
+ g_precise_mode=1;g_dirty_interp_active=1;
+ uint32_t pc=0xa0000200;
+ for(unsigned i=0;i<3;++i) {
+ uint32_t next=0;cpu.pc=pc;
+ int moved=exec_one(&cpu,pc,&next);psx_cyc_batch_flush();
+ printf("pair_return next_out=%08X cpu_pc=%08X %llu %d\n",next,cpu.pc,(unsigned long long)psx_get_cycle_count(),moved);
+ pc=moved?cpu.pc:next;assert(pc==0xa0000200);
+ }
+ return 0;
+}
diff --git a/runtime/tests/test_icache_isolated_store.c b/runtime/tests/test_icache_isolated_store.c
new file mode 100644
index 000000000..fdaf40aca
--- /dev/null
+++ b/runtime/tests/test_icache_isolated_store.c
@@ -0,0 +1,39 @@
+/* Source-independent driver of the real store guard prefixes and cache timing.
+ * The oracle vectors come from the unmodified original CPU WriteMemory method.
+ * No BIOS instructions, ROM, or retail data is present in this fixture. */
+#include "psx_icache.h"
+#include "psx_cycles.h"
+#include
+#include
+#include
+uint64_t psx_cycle_count,psx_next_service_cycle;
+int psx_in_device_service,g_ls_replay_active,g_event_step_conservative;
+uint32_t g_psx_cyc_batch,g_psx_cyc_batch_limit;
+uint64_t g_guest_store_count,g_kseg2_ignored_writes;
+static uint32_t sr,cache_ctrl,*sr_ptr=&sr;
+static unsigned ordinary_writes;
+void psx_devices_service_to_now(void){}
+void psx_advance_cycles_slow(uint32_t n){psx_cycle_count+=n;}
+#include PSX_TEST_STORE_PREFIXES
+static void write_width(unsigned w,uint32_t a){
+ if(w==1)psx_write_byte_raw(a,0xa5);else if(w==2)psx_write_half_raw(a,0xa5a5);else psx_write_word_raw(a,0xa5a5a5a5);
+}
+int main(void){
+ unsigned biu,status,width,addr,expected_hash,expected_writes,count=0,fail=0;
+ while(scanf("%u %u %u %u %u %u",&biu,&status,&width,&addr,&expected_hash,&expected_writes)==6){
+ psx_icache_reset();sr=0;psx_write_word_raw(0xfffe0130u,biu);
+ for(unsigned i=0;i<1024;i++)g_psx_icache_tv[i]=0x2000+i*4;
+ sr=status;ordinary_writes=0;write_width(width,addr);
+ unsigned hash=2166136261u;for(unsigned i=0;i<1024;i++)hash=(hash^g_psx_icache_tv[i])*16777619u;
+ if(hash!=expected_hash || ordinary_writes!=expected_writes){if(!fail)fprintf(stderr,"case %u biu%x sr%x width%u addr%x hash%x/%x writes%u/%u\n",count,biu,status,width,addr,hash,expected_hash,ordinary_writes,expected_writes);fail++;}
+ if(status && (biu&0x804)==0x804){
+ CPUState cpu;memset(&cpu,0,sizeof(cpu));cpu.read_absorb_which=3;cpu.read_absorb[3]=9;psx_cycle_count=0;
+ psx_icache_fetch(&cpu,0x27a0);psx_icache_fetch_interp(&cpu,0x27b0);
+ /* Only the selected 16-byte line was invalidated. Adjacent line stays warm. */
+ if(psx_cycle_count!=7 || cpu.read_absorb[3] || cpu.read_absorb_which)fail++;
+ psx_icache_fetch_interp(&cpu,0x27a0);if(psx_cycle_count!=7)fail++;
+ }
+ count++;
+ }
+ printf("%s %u source store cases, %u failed checks; cache refill and give-back clear\n",fail?"FAIL":"PASS",count,fail);return !!fail;
+}
diff --git a/runtime/tests/test_input_histogram_start.c b/runtime/tests/test_input_histogram_start.c
new file mode 100644
index 000000000..f46e7ca0d
--- /dev/null
+++ b/runtime/tests/test_input_histogram_start.c
@@ -0,0 +1,30 @@
+/* Passive observer range/ordering regression. No guest execution is involved. */
+#include
+#include
+#include
+#include
+#include
+uint64_t psx_cycle_count;
+uint32_t g_psx_cyc_batch,*g_psx_cyc_local_acc,g_psx_icache_tv[1024];
+int g_psx_icache_active=1,g_ls_replay_active,g_input_instruction_histogram_active;
+void(*g_input_instruction_histogram_callback)(uint32_t);
+static FILE*input_route_observer_output(const char *name){FILE *f=fopen(name,"rb");assert(!f);f=fopen(name,"wb");assert(f);return f;}
+static void debug_server_freeze_dump_dma_trace_json(FILE*f,int n){(void)n;fputs("{}\n",f);}
+static void debug_server_freeze_dump_sio_pc_json(FILE*f,int n){(void)n;fputs("{}\n",f);}
+#include PSX_TEST_HISTOGRAM_IMPLEMENTATION
+int main(int argc,char**argv){
+ assert(argc==3);int kind=atoi(argv[2]);
+ _putenv_s("PSX_INPUT_HISTOGRAM_RANGE",strcmp(argv[1],"none")?argv[1]:"");
+ if(!strcmp(argv[1],"none"))_putenv_s("PSX_INPUT_HISTOGRAM_RANGE","");
+ _putenv_s("PSX_INPUT_HISTOGRAM_SITES",kind==3?"00001000,00001000":"");
+ int ok=input_instruction_histogram_configure();
+ if(kind==2||kind==3){printf("rejected=%d\n",!ok);return ok?1:0;}
+ if(!ok){fprintf(stderr,"valid range rejected: %s\n",argv[1]);return 1;}
+ uint32_t tags[1024];memcpy(tags,g_psx_icache_tv,sizeof(tags));
+ psx_cycle_count=7;input_instruction_histogram_record(0x1000);
+ psx_cycle_count=10;input_instruction_histogram_boundary(1);
+ psx_cycle_count=11;input_instruction_histogram_record(0x1004);
+ psx_cycle_count=20;input_instruction_histogram_boundary(2);
+ assert(!memcmp(tags,g_psx_icache_tv,sizeof(tags)) && psx_cycle_count==20);
+ printf("PASS passive range %s\n",argv[1]);return 0;
+}
diff --git a/runtime/tests/test_input_instruction_histogram.c b/runtime/tests/test_input_instruction_histogram.c
new file mode 100644
index 000000000..2f688708e
--- /dev/null
+++ b/runtime/tests/test_input_instruction_histogram.c
@@ -0,0 +1,104 @@
+#include
+#include
+#include
+#include
+#include
+int g_input_instruction_histogram_active;
+void (*g_input_instruction_histogram_callback)(uint32_t pc);
+int g_ls_replay_active;
+uint64_t psx_cycle_count=1000;
+uint32_t g_psx_cyc_batch;
+uint32_t *g_psx_cyc_local_acc;
+uint32_t g_psx_icache_tv[1024];
+int g_psx_icache_active=1;
+int g_precise_mode;
+uint32_t i_stat,i_mask,g_slice_last_block,g_slice_last_committed;
+uint64_t g_slice_fired,g_slice_irq_taken;
+static int in_exception;
+int psx_get_in_exception(void){return in_exception;}
+static unsigned outputs;
+static unsigned partial_outputs;
+static FILE *input_route_observer_output(const char *name) {
+ if(strstr(name,"partial-")==name){partial_outputs++;name+=8;}
+ assert(strstr(name,"instructions-")==name || strstr(name,"instruction-first-")==name || strstr(name,"instruction-sites-")==name || strstr(name,"instruction-context-")==name || strstr(name,"dma-at-")==name || strstr(name,"sio-at-")==name);
+ if(strstr(name,"instructions-")==name)outputs++;
+ return tmpfile();
+}
+static void debug_server_freeze_dump_dma_trace_json(FILE *f,uint32_t count) {
+ assert(count==128);fputs("{}",f);
+}
+static void debug_server_freeze_dump_sio_pc_json(FILE *f,uint32_t count) {
+ assert(count==128);fputs("[]",f);
+}
+#include "input_instruction_histogram_impl.h"
+int main(void) {
+ assert(input_instruction_sites_parse(NULL));
+ const char *invalid[]={"","8000000","80000000,","80000002","80000000,80000000","8000000G","80000000x"};
+ for(unsigned i=0;i
+#include
+
+/* Independent scanline-by-scanline oracle; mixed display modes also test
+ * phase continuity. No retail data, emulator source or desired outcome. */
+int main(void) {
+ InputRouteFieldClock s;
+ uint64_t gpu_total = 0, cpu_total = 0;
+ unsigned phase = 0, field = 0;
+ input_route_field_clock_reset(&s);
+ for (unsigned n = 0; n < 100000; ++n) {
+ int interlaced = n >= 17 && (n % 97) < 89;
+ if (n) {
+ input_route_field_clock_next(&s, interlaced);
+ field = interlaced ? !field : 0;
+ } else interlaced = 0;
+ unsigned lines = interlaced ? 263 - field : 263;
+ for (unsigned line = 0; line < lines; ++line) {
+ gpu_total += 3412 + phase;
+ phase ^= 1;
+ }
+ cpu_total += s.current_cycles;
+ assert(cpu_total == (gpu_total * 65536 + 103895) / 103896);
+ assert(s.line_phase == phase && s.field == field);
+ assert(s.remainder < 103896);
+ assert(s.current_cycles >= 563968 && s.current_cycles <= 566122);
+ }
+ input_route_field_clock_reset(&s);
+ assert(s.current_cycles == (263ull * 3412 * 65536 + 131ull * 65536 + 103895) / 103896);
+ puts("field clock: 100000 mixed fields match independent scanline oracle");
+ return 0;
+}
diff --git a/runtime/tests/test_input_route_file.c b/runtime/tests/test_input_route_file.c
new file mode 100644
index 000000000..18fedb8be
--- /dev/null
+++ b/runtime/tests/test_input_route_file.c
@@ -0,0 +1,49 @@
+#include "input_route_file.h"
+#include
+
+static void check(int ok, const char *what) {
+ if (!ok) { fprintf(stderr, "FAIL: %s\n", what); exit(1); }
+}
+static void le32(unsigned char *p, uint32_t n) {
+ for (unsigned i=0; i<4; ++i) p[i]=(unsigned char)(n>>(8*i));
+}
+static FILE *fixture(unsigned n, int fault) {
+ unsigned char h[24] = "PSXRTI1", r[8] = {0};
+ FILE *f=tmpfile(); check(f != NULL, "tmpfile");
+ le32(h+8,1); le32(h+12,8); le32(h+16,n);
+ if (fault==1) h[7]=1;
+ if (fault==2) h[8]=2;
+ if (fault==3) h[12]=9;
+ if (fault==4) h[20]=1;
+ fwrite(h,1,fault==5 ? 23 : 24,f);
+ if (n <= INPUT_ROUTE_MAX_FRAMES) for (unsigned i=0;i=2 ? 1 : 0));
+ if (fault==6 && i==1) le32(r,i);
+ if (fault==7) r[6]=1;
+ fwrite(r,1,fault==8 && i==n-1 ? 7 : 8,f);
+ }
+ if (fault==9) fputc(0,f);
+ rewind(f); return f;
+}
+int main(void) {
+ InputRouteStep steps[INPUT_ROUTE_MAX_STEPS]; uint32_t count,n;
+ FILE *f=fixture(5,0);
+ check(input_route_read(f,steps,&count,&n)==NULL,"valid route"); fclose(f);
+ check(n==5 && count==2 && steps[0].frames==2 && steps[0].buttons==0 &&
+ steps[1].frames==3 && steps[1].buttons==1,"exact RLE and first/last input");
+ for (int fault=1;fault<=9;++fault) {
+ f=fixture(5,fault);
+ check(input_route_read(f,steps,&count,&n)!=NULL,"bad input rejected"); fclose(f);
+ check(!count && !n,"failure cannot publish a partial route");
+ }
+ f=fixture(0,0); check(input_route_read(f,steps,&count,&n)!=NULL,"empty rejected"); fclose(f);
+ f=fixture(INPUT_ROUTE_MAX_FRAMES+1,0);
+ check(input_route_read(f,steps,&count,&n)!=NULL,"frame bound"); fclose(f);
+ f=fixture(INPUT_ROUTE_MAX_STEPS,10);
+ check(input_route_read(f,steps,&count,&n)==NULL && count==INPUT_ROUTE_MAX_STEPS,"capacity boundary"); fclose(f);
+ f=fixture(INPUT_ROUTE_MAX_STEPS+1,10);
+ check(input_route_read(f,steps,&count,&n)!=NULL && !count && !n,"step overflow rejected"); fclose(f);
+ f=fixture(INPUT_ROUTE_MAX_FRAMES,0);
+ check(input_route_read(f,steps,&count,&n)==NULL && n==INPUT_ROUTE_MAX_FRAMES,"frame capacity boundary"); fclose(f);
+ puts("input_route_file: 15 parser cases passed"); return 0;
+}
diff --git a/runtime/tests/test_input_route_observer.c b/runtime/tests/test_input_route_observer.c
new file mode 100644
index 000000000..93e4ee2be
--- /dev/null
+++ b/runtime/tests/test_input_route_observer.c
@@ -0,0 +1,72 @@
+/* Source-owned observer fixture; driven by test_input_route_observer.py. */
+#include "input_route_observer.h"
+#include "debug_server.h"
+#include
+CPUState *debug_cpu_ptr = NULL;
+uint32_t i_stat = 5, i_mask = 13;
+uint64_t psx_cycle_count = 123456;
+void gpu_observer_video_state(uint32_t *out) { const uint32_t v[5]={100,16,256,1,0}; memcpy(out,v,sizeof v); }
+void interrupts_observer_field_state(uint32_t *out) { const uint32_t v[3]={7,563969,1}; memcpy(out,v,sizeof v); }
+/* Snapshot accessor copies all three channels without servicing devices. */
+void timers_get_snapshot(uint16_t counter[3], uint32_t mode[3], uint16_t target[3], int32_t irq_line[3], uint32_t frac[3]) {
+ for (unsigned i=0;i<3;i++) { counter[i]=(uint16_t)(0xFFFF-i); mode[i]=0x148+i;
+ target[i]=(uint16_t)i; irq_line[i]=(int32_t)i-1; frac[i]=2145-i; }
+}
+#include "psx_memory.h"
+#include "gpu.h"
+#include "sio.h"
+#include "cdrom.h"
+#include "dma.h"
+#include
+#include
+#include
+static uint8_t ram[PSX_MAIN_RAM_BYTES];
+uint8_t *g_psx_ram = ram;
+int event_ring_dump_stream(FILE *f) { fputs("[]\n", f); return 0; }
+uint64_t cdrom_debug_get_command_history(const CDROMCommandHistoryEntry **out) { *out = NULL; return 0; }
+uint64_t cdrom_debug_get_trace(const CDROMTraceEntry **out) { *out = NULL; return 0; }
+uint64_t cdrom_timing_total(void) { return 0; }
+int cdrom_timing_record(uint64_t seq, CdTimingPub *out) { (void)seq; (void)out; return 0; }
+uint64_t cdrom_debug_get_sector_history(const CDROMSectorHistoryEntry **out) { *out = NULL; return 0; }
+int cdrom_get_bursts(void *out, int max) { (void)out; (void)max; return 0; }
+uint64_t dma_debug_get_cdrom_history(const DMACDROMHistoryEntry **out) { *out = NULL; return 0; }
+uint32_t sio_get_trace(const SioTraceEntry **out, int *index) {
+ *out = NULL; *index = 0; return 0;
+}
+void gl_renderer_sync_cpu(void) {}
+void debug_server_dump_watched_writes(FILE *f, const uint32_t *a, uint32_t count) {
+ (void)a; (void)count; fputs("{\"kind\":\"coverage\",\"total_writes\":0,\"retained_writes\":0}\n", f);
+}
+void vk_renderer_sync_cpu(void) {}
+void gpu_get_display_info(GpuDisplayInfo *out) {
+ memset(out, 0, sizeof(*out)); out->disabled = 1;
+}
+void gpu_display_pixel_rgb(const GpuDisplayInfo *di, uint32_t x, uint32_t y,
+ uint8_t *r, uint8_t *g, uint8_t *b) {
+ (void)di; (void)x; (void)y; *r = *g = *b = 0;
+}
+int main(int argc, char **argv) {
+ if (argc != 2) return 9;
+ CPUState cpu = {0};
+ if (!strcmp(argv[1], "cpu")) {
+ cpu.pc=0x80012340; cpu.gpr[31]=0x80054320;
+ cpu.cop0[12]=0x40000401; cpu.cop0[13]=0x420; cpu.cop0[14]=0x80012000;
+ debug_cpu_ptr=&cpu;
+ }
+ if (!input_route_observer_init(2)) return 4;
+ g_psx_ram[0] = 0x34; g_psx_ram[1] = 0x12;
+ g_psx_ram[PSX_MAIN_RAM_BYTES - 2] = 0xCD; g_psx_ram[PSX_MAIN_RAM_BYTES - 1] = 0xAB;
+ input_route_observer_boundary(0, 1);
+ input_route_observer_input(0xFFF7);
+ if (!strcmp(argv[1], "missing")) input_route_observer_boundary(1, 2);
+ input_route_observer_applied(!strcmp(argv[1], "wrong") ? 0xFFFF : 0xFFF7,
+ strcmp(argv[1], "disconnected") != 0, !strcmp(argv[1], "analog"));
+ input_route_observer_boundary(1, 2);
+ input_route_observer_input(0xFFFF);
+ input_route_observer_applied(0xFFFF, 1, 0);
+ input_route_observer_boundary(2, 3);
+ input_route_observer_input(!strcmp(argv[1], "tail_pressed") ? 0xFFF7 : 0xFFFF);
+ input_route_observer_applied(0xFFFF, 1, 0);
+ input_route_observer_boundary(3, 4);
+ return 8; /* observer must exit at the declared boundary */
+}
diff --git a/runtime/tests/test_input_route_observer.py b/runtime/tests/test_input_route_observer.py
new file mode 100644
index 000000000..ca6c14513
--- /dev/null
+++ b/runtime/tests/test_input_route_observer.py
@@ -0,0 +1,57 @@
+"""Exercise the compiled observer with synthetic RAM and no game assets."""
+import hashlib
+import json
+import os
+from pathlib import Path
+import subprocess
+import sys
+import tempfile
+
+exe = Path(sys.argv[1]).resolve()
+with tempfile.TemporaryDirectory() as root:
+ root = Path(root)
+ bad_watches = {"watch_unaligned":"1", "watch_range":"0x200000", "watch_empty":"", "watch_duplicate":"0,0",
+ "watch_trailing":"0,", "watch_space":"\t0", "watch_limit":",".join(str(2*i) for i in range(33))}
+ for case in ("valid", "cpu", "video", "cpu_missing", "missing", "wrong", "disconnected", "analog", "tail_pressed", "collision", "bad_tail", *bad_watches):
+ out = root / case
+ out.mkdir()
+ env = dict(os.environ, PSX_INPUT_ROUTE_CAPTURE_DIR=str(out), PSX_INPUT_ROUTE_NEUTRAL_TAIL="1", PSX_INPUT_ROUTE_TRACE="1")
+ env["PSX_INPUT_ROUTE_WATCH_U16"] = bad_watches.get(case, "0,0x1ffffe")
+ env["PSX_INPUT_ROUTE_CPU_STATE"] = "1" if case.startswith("cpu") else "0"
+ env["PSX_INPUT_ROUTE_VIDEO_STATE"] = "1" if case=="video" else "0"
+ if case == "collision":
+ (out / "checkpoints.jsonl").write_bytes(b"preserve")
+ if case == "bad_tail":
+ env["PSX_INPUT_ROUTE_NEUTRAL_TAIL"] = "1garbage"
+ run = subprocess.run([str(exe), case], env=env, capture_output=True)
+ expected = 0 if case in ("valid", "cpu", "video") else 4 if case == "bad_tail" or case in bad_watches else 3
+ assert run.returncode == expected, (case, run.returncode, run.stderr)
+ if case in ("valid", "cpu", "video"):
+ if case == "video":
+ rows=[json.loads(x) for x in (out/"video-state.jsonl").read_text().splitlines()]
+ assert [x["frame"] for x in rows]==[0,1,2,3]
+ assert all(x["cycle"]==123456 and x["display_mode"]==100 and x["vertical_start"]==16 and x["vertical_end"]==256 and x["gpu_field"]==1 and x["lcf"]==0 and x["cycles_since_vblank"]==7 and x["next_period"]==563969 and x["profile_field"]==1 for x in rows)
+ else:assert not (out/"video-state.jsonl").exists()
+ if case == "cpu":
+ rows=[json.loads(x) for x in (out/"cpu-state.jsonl").read_text().splitlines()]
+ assert [x["frame"] for x in rows] == [0,1,2,3]
+ assert all(x["timers"] == [dict(channel=i,counter=0xFFFF-i,mode=0x148+i,target=i,irq_line=i-1,fraction=2145-i) for i in range(3)] for x in rows)
+ assert all(x["sr"]==0x40000401 and x["cause"]==0x420 and x["epc"]==0x80012000 and x["i_stat"]==5 and x["i_mask"]==13 for x in rows)
+ else:
+ assert not (out/"cpu-state.jsonl").exists()
+ done = json.loads((out / "complete.json").read_text())
+ end = json.loads((out / "input-end.json").read_text())
+ assert done["frame"] == 3 and done["input_frames"] == 2 and done["neutral_tail_ticks"] == 1
+ assert end["frame"] == 2
+ assert done["applied_words_sha256"] == hashlib.sha256(bytes.fromhex("f7ffffff")).hexdigest()
+ assert end["applied_words_sha256"] == done["applied_words_sha256"]
+ assert json.loads((out / "device-events.json").read_text()) == []
+ watches = [json.loads(line) for line in (out / "ram-u16.jsonl").read_text().splitlines()]
+ assert [w["frame"] for w in watches] == [0,1,2,3]
+ assert all(w["ram_u16"] == {"000000":0x1234,"1FFFFE":0xABCD} for w in watches)
+ assert (out / "frame-000003.png").read_bytes()[:8] == b"\x89PNG\r\n\x1a\n"
+ else:
+ assert not (out / "complete.json").exists()
+ if case == "collision":
+ assert (out / "checkpoints.jsonl").read_bytes() == b"preserve"
+print("input_route_observer: 18 compiled cases passed")
diff --git a/runtime/tests/test_input_route_raster_clock.c b/runtime/tests/test_input_route_raster_clock.c
new file mode 100644
index 000000000..8cfce3f7d
--- /dev/null
+++ b/runtime/tests/test_input_route_raster_clock.c
@@ -0,0 +1,93 @@
+#include "input_route_raster_clock.h"
+#include
+#include
+
+static void next_line(InputRouteRasterClock *s) {
+ uint32_t line=s->scanline;
+ while (s->scanline==line) input_route_raster_advance(s,1);
+}
+
+static void status_cases(void) {
+ InputRouteRasterClock s;
+ input_route_raster_reset(&s);
+ assert(input_route_raster_status(&s)==0x2000u);
+ while (s.scanline!=16) next_line(&s);
+ assert(!s.blank && !(input_route_raster_status(&s)>>31));
+ next_line(&s); assert(input_route_raster_status(&s)==0x80002000u);
+ next_line(&s); assert(input_route_raster_status(&s)==0x2000u);
+ input_route_raster_reset(&s);
+ assert(input_route_raster_gp1(&s,0x08000024));
+ while (s.scanline!=256) next_line(&s);
+ assert(s.blank && s.readout_field==1 && input_route_raster_status(&s)==0x2000u);
+ while (s.scanline!=262) next_line(&s);
+ assert(s.field==1 && s.blank && input_route_raster_status(&s)==0);
+ while (s.scanline!=16) next_line(&s);
+ assert(!s.blank && input_route_raster_status(&s)==0x80000000u);
+ next_line(&s); assert(input_route_raster_status(&s)==0x80000000u);
+ assert(input_route_raster_gp1(&s,0x05000400));
+ assert(input_route_raster_status(&s)==0x80000000u); /* next scanline owns readout */
+ next_line(&s); assert(input_route_raster_status(&s)==0);
+ uint32_t offset=s.y_offset, readout=s.readout_y, field=s.readout_field;
+ assert(input_route_raster_gp1(&s,0));
+ assert(s.y_start==0 && s.y_offset==offset && s.readout_y==readout && s.readout_field==field);
+ puts("raster field/parity independence, progressive lines, interlace blanking, display origin and soft reset pass");
+}
+
+int main(int argc, char **argv) {
+ InputRouteRasterClock a,b;
+ if (argc==2) {
+ FILE *f=fopen(argv[1],"r"); assert(f);
+ char op; unsigned long long cycle; unsigned word; unsigned samples=0;
+ input_route_raster_reset(&a);
+ while (fscanf(f," %c",&op)==1) {
+ if (op=='A') {
+ assert(fscanf(f,"%llu",&cycle)==1 && cycle>=a.cycle && cycle-a.cycle<=UINT32_MAX);
+ input_route_raster_advance(&a,(uint32_t)(cycle-a.cycle));
+ } else if (op=='G') {
+ assert(fscanf(f,"%x",&word)==1 && input_route_raster_gp1(&a,word));
+ } else if (op=='Q') {
+ assert(fscanf(f,"%x",&word)==1 && input_route_raster_status(&a)==word);
+ samples++;
+ } else {
+ assert(op=='S');
+ uint32_t expected[11];
+ uint32_t actual[11]={a.mode,a.start,a.end,a.blank,a.lines,a.scanline,a.field,a.phase,a.remaining,a.fraction,103896};
+ for (unsigned i=0;i<11;i++) assert(fscanf(f,"%u",&expected[i])==1);
+ assert(!memcmp(expected,actual,sizeof(actual))); samples++;
+ }
+ }
+ assert(!ferror(f)); fclose(f); printf("source raster scalar samples pass: %u\n",samples); return 0;
+ }
+ input_route_raster_reset(&a); b=a;
+ assert(input_route_raster_until_rise(&a)==551053);
+ input_route_raster_advance(&a,551052); assert(!a.rises);
+ input_route_raster_advance(&a,1); assert(a.rises==1 && a.last_rise==551053);
+ /* Independent one-CPU-clock stepping versus event-sized batches. */
+ for (uint32_t i=0;i<551053;i++) input_route_raster_advance(&b,1);
+ assert(!memcmp(&a,&b,sizeof(a)));
+ for (uint32_t i=0;i<400;i++) {
+ uint32_t word=i%7==0 ? 0 : i%3==0 ? 0x08000024u : 0x08000000u;
+ assert(input_route_raster_gp1(&a,word)); assert(input_route_raster_gp1(&b,word));
+ uint32_t span=1000u+(i*7919u)%20000u;
+ input_route_raster_advance(&a,span);
+ for (uint32_t j=0;j0 && distance<100000);
+ uint32_t count=a.rises;input_route_raster_advance(&a,distance-1);assert(a.rises==count);
+ input_route_raster_advance(&a,1);assert(a.rises==count+1);
+ assert(input_route_raster_gp1(&a,0x07000000));
+ assert(input_route_raster_until_rise(&a)==UINT32_MAX);
+ status_cases();
+ puts("raster clock chunking, deadline, mode, soft reset, range and PAL guards pass");
+ return 0;
+}
diff --git a/runtime/tests/test_input_update_clock.c b/runtime/tests/test_input_update_clock.c
new file mode 100644
index 000000000..4cf215b76
--- /dev/null
+++ b/runtime/tests/test_input_update_clock.c
@@ -0,0 +1,205 @@
+#include "input_update_clock.h"
+#include
+#include
+int main(void) {
+ const uint16_t w[]={0xffff,0xffff,0xbfdf,0xbfdf,0xffff,0xfff7};
+ InputUpdateClock s; uint16_t v;
+ assert(input_update_init(&s,w,6));
+ assert(input_update_predict(&s,1,0,0));
+ assert(!input_update_predict(&s,1,1,1)); /* No invented initial acceptance. */
+ assert(input_update_poll(&s,1,1,&v)&&v==0xffff); /* unprimed */
+ assert(input_update_accept(&s,2,0xffff));
+ assert(!input_update_predict(&s,3,1,0)); /* Default remains gate-only. */
+ assert(input_update_predict(&s,3,1,1));
+ assert(!input_update_predict(&s,2,1,1)); /* Same-frame calls are not continuation. */
+ assert(!input_update_predict(&s,4,1,1)); /* Stalled frame does not extend prediction. */
+ assert(!input_update_predict(&s,3,2,1)); /* Closed multi-frame gate. */
+ assert(input_update_auxiliary(&s,0xffff,0)&&s.accepted==1);
+ assert(input_update_poll(&s,3,1,&v)&&v==0xbfdf);
+ assert(input_update_auxiliary(&s,0xffff,0)&&s.accepted==1&&s.predicted);
+ assert(input_update_accept(&s,3,0xffff)); /* consecutive neutral */
+ assert(input_update_auxiliary(&s,0xffff,0)&&s.accepted==2&&!s.predicted);
+ assert(input_update_poll(&s,4,0,&v)&&v==0xbfdf);
+ assert(input_update_poll(&s,5,0,&v)&&v==0xbfdf); /* closed gate holds */
+ assert(input_update_poll(&s,6,1,&v)&&v==0xbfdf);
+ assert(input_update_accept(&s,6,0xbfdf));
+ assert(input_update_poll(&s,7,1,&v)&&v==0xffff);
+ assert(input_update_accept(&s,7,0xbfdf)); /* held duration preserved */
+ assert(input_update_poll(&s,8,1,&v)&&v==0xfff7);
+ assert(input_update_accept(&s,8,0xffff));
+ assert(input_update_poll(&s,9,1,&v)&&v==0xffff);
+ assert(input_update_accept(&s,9,0xfff7)&&s.accepted==6); /* drain */
+ assert(input_update_poll(&s,10,1,&v)&&v==0xffff);
+ assert(input_update_init(&s,w,6)); assert(input_update_accept(&s,1,0xffff));
+ assert(input_update_poll(&s,2,input_update_predict(&s,2,1,1),&v));
+ assert(!input_update_poll(&s,3,input_update_predict(&s,3,1,1),&v)); /* Wrong prediction still fails. */
+ assert(input_update_init(&s,w,6)); assert(input_update_accept(&s,1,0xffff));
+ assert(input_update_poll(&s,2,0,&v)); assert(!input_update_accept(&s,2,0xffff)); /* unexpected acceptance */
+ assert(input_update_init(&s,w,6)); assert(input_update_accept(&s,1,0xffff));
+ assert(input_update_poll(&s,2,1,&v)); assert(!input_update_accept(&s,2,0xfff7)); /* wrong pipeline packet */
+ assert(input_update_init(&s,w,6)); assert(input_update_accept(&s,1,0xffff));
+ assert(input_update_poll(&s,2,0,&v)); assert(!input_update_poll(&s,2,0,&v)); /* duplicate poll */
+ assert(!input_update_init(&s,w+1,5));
+ assert(input_update_init(&s,w,6)); assert(!input_update_auxiliary(&s,0xfff7,0));
+ assert(input_update_init(&s,w,6)); assert(!input_update_auxiliary(&s,0xffff,1));
+ /* Prediction differences are harmless only while the physical packets
+ * remain equal. Count actual acceptances, never predicted ones. */
+ const uint16_t repeated[]={0xffff,0xffff,0xffff,0xbfdf,0xbfdf,0xbfdf,0xffff,0xffff};
+ assert(input_update_init(&s,repeated,8)); assert(input_update_accept(&s,1,0xffff));
+ assert(input_update_poll(&s,2,0,&v)&&v==0xffff);
+ assert(input_update_accept(&s,2,0xffff)&&s.accepted==2); /* late opening */
+ assert(!input_update_accept(&s,2,0xffff)); /* no duplicate commit */
+ assert(input_update_init(&s,repeated,8)); assert(input_update_accept(&s,1,0xffff));
+ assert(input_update_poll(&s,2,1,&v)&&v==0xffff);
+ assert(input_update_poll(&s,3,1,&v)&&v==0xffff&&s.accepted==1); /* no acceptance */
+ assert(input_update_accept(&s,3,0xffff));
+ assert(input_update_poll(&s,4,1,&v)&&v==0xbfdf);
+ assert(input_update_accept(&s,4,0xffff));
+ assert(input_update_poll(&s,5,0,&v)&&v==0xbfdf);
+ assert(input_update_accept(&s,5,0xbfdf)); /* held late opening */
+ assert(input_update_poll(&s,6,1,&v)&&v==0xbfdf);
+ assert(input_update_poll(&s,7,0,&v)&&v==0xbfdf&&s.accepted==4);
+ assert(input_update_accept(&s,7,0xbfdf));
+ assert(input_update_poll(&s,8,0,&v)&&v==0xbfdf);
+ assert(!input_update_accept(&s,8,0xbfdf)); /* release must not be delayed */
+ /* Source-context policy consumes the actual auxiliary refresh once. The
+ * next ordinary press must not be delayed by a second neutral update. */
+ const uint16_t cw[]={0xffff,0xffff,0xffdf,0xffff,0xffdf,0x5fdf,0xffff,0xffff};
+ const uint8_t cx[]={0,0,0,1,0,0,0,0};
+ assert(input_update_init(&s,cw,8)); assert(input_update_set_contexts(&s,cx));
+ assert(input_update_context_accept(&s,1,cw[0],0));
+ for (unsigned i=1;i<8;i++) {
+ assert(input_update_poll(&s,i+1,1,&v));
+ assert(v==(i+1<8 ? cw[i+1] : 0xffff));
+ assert(input_update_context_accept(&s,i+1,cw[i],cx[i]));
+ }
+ assert(s.accepted==8);
+ assert(input_update_init(&s,cw,8)); assert(input_update_set_contexts(&s,cx));
+ assert(!input_update_context_accept(&s,1,cw[0],1)); /* extra auxiliary */
+ assert(input_update_init(&s,cw,8)); assert(input_update_set_contexts(&s,cx));
+ assert(input_update_context_accept(&s,1,cw[0],0));
+ assert(input_update_poll(&s,2,1,&v)); assert(input_update_context_accept(&s,2,cw[1],0));
+ assert(input_update_poll(&s,3,1,&v)); assert(input_update_context_accept(&s,3,cw[2],0));
+ assert(input_update_poll(&s,4,1,&v));
+ assert(!input_update_context_accept(&s,4,cw[3],0)); /* ordinary cannot substitute */
+ assert(input_update_init(&s,cw,8));
+ const uint8_t bad[]={0,0,0,2,0,0,0,0};
+ assert(!input_update_set_contexts(&s,bad));
+ const uint8_t alternate[]={0,0,0,1,0,1,0,0};
+ assert(input_update_init(&s,cw,8)); assert(input_update_set_contexts(&s,cx));
+ s.compatible_contexts=1;s.protected_raw_mask=8;
+ assert(input_update_context_accept(&s,1,cw[0],0));
+ for (unsigned i=1;i<8;i++) {
+ assert(input_update_poll(&s,i+1,1,&v));
+ assert(input_update_context_accept(&s,i+1,cw[i],alternate[i]));
+ }
+ assert(s.accepted==8); /* Attack through auxiliary, protected raw bit unchanged. */
+ const uint16_t start_words[]={0xffff,0xffff,0xfff7,0xffff};
+ const uint8_t normal[]={0,0,0,0};
+ assert(input_update_init(&s,start_words,4)); assert(input_update_set_contexts(&s,normal));
+ s.compatible_contexts=1;s.protected_raw_mask=8;
+ assert(input_update_context_accept(&s,1,start_words[0],0));
+ assert(input_update_poll(&s,2,1,&v));assert(input_update_context_accept(&s,2,start_words[1],0));
+ assert(input_update_poll(&s,3,1,&v));
+ assert(!input_update_context_accept(&s,3,start_words[2],1));
+ assert(s.context_failed && s.accepted==2); /* A lost Start press cannot be accepted. */
+ assert(input_update_init(&s,start_words,4)); assert(input_update_set_contexts(&s,normal));
+ s.compatible_contexts=1;s.protected_raw_mask=8;
+ assert(input_update_context_accept(&s,1,start_words[0],0));
+ assert(input_update_poll(&s,2,1,&v));assert(input_update_context_accept(&s,2,start_words[1],0));
+ assert(input_update_poll(&s,3,1,&v));assert(input_update_context_accept(&s,3,start_words[2],0));
+ assert(input_update_poll(&s,4,1,&v));
+ assert(!input_update_context_accept(&s,4,start_words[3],1)); /* Lost release/retained edge. */
+ for(int fault=0;fault<10;fault++) {
+ assert(input_update_init(&s,start_words,4));assert(input_update_set_contexts(&s,normal));
+ s.compatible_contexts=1;s.protected_raw_mask=8;
+ assert(input_update_context_accept(&s,1,0xffff,0));
+ assert(input_update_poll_mode(&s,2,2,&v)&&v==0xffff);
+ if(fault==1) {assert(!input_update_context_accept(&s,2,0xffff,1));continue;}
+ assert(input_update_context_accept(&s,2,0xffff,0)&&s.accepted==2);
+ if(fault==2) {assert(!input_update_poll_mode(&s,3,1,&v));continue;}
+ assert(input_update_poll_mode(&s,3,3,&v)&&v==0xfff7);
+ if(fault==3) {assert(!input_update_context_accept(&s,3,0xffff,0));continue;}
+ if(fault==4) {assert(!input_update_poll_mode(&s,4,1,&v));continue;}
+ if(fault==5) {assert(!input_update_neutral_refresh(&s,3,0xfff7,0,0,0));continue;}
+ if(fault==6) {assert(!input_update_neutral_refresh(&s,3,0xffff,1,0,0));continue;}
+ if(fault==7) {assert(!input_update_neutral_refresh(&s,3,0xffff,0,0,1));continue;}
+ if(fault==8) {assert(!input_update_neutral_refresh(&s,3,0xffff,0,1,0));continue;}
+ if(fault==9) {s.native_raw_press=8;assert(!input_update_neutral_refresh(&s,3,0xffff,0,0,0));continue;}
+ assert(input_update_neutral_refresh(&s,3,0xffff,0,0,0)&&s.accepted==2&&s.neutral_refreshes==1);
+ assert(input_update_poll_mode(&s,4,1,&v)&&v==0xffff);
+ assert(input_update_context_accept(&s,4,0xfff7,0));
+ assert(input_update_poll_mode(&s,5,1,&v)&&v==0xffff);
+ assert(input_update_context_accept(&s,5,0xffff,0)&&s.accepted==4);
+ assert(!input_update_neutral_refresh(&s,5,0xffff,0,0,0)); /* extra refresh */
+ }
+ /* An ordinary neutral release is accepted after the hold poll. Its
+ * predecessor can still be held at that poll; no extra source event
+ * may be consumed by the following separately checked refresh. */
+ for (unsigned previous=0;previous<2;previous++) {
+ const uint16_t release_words[]={0xffff,0xffff,previous ? 0xfff7 : 0x5fdf,0xffff,0xfff7,0xffff};
+ const uint8_t release_contexts[]={0,0,0,0,0,0};
+ assert(input_update_init(&s,release_words,6));
+ assert(input_update_set_contexts(&s,release_contexts));
+ s.compatible_contexts=1;s.protected_raw_mask=8;
+ assert(input_update_context_accept(&s,1,release_words[0],0));
+ assert(input_update_poll_mode(&s,2,1,&v));
+ assert(input_update_context_accept(&s,2,release_words[1],0));
+ assert(input_update_poll_mode(&s,3,1,&v)&&v==0xffff);
+ assert(input_update_context_accept(&s,3,release_words[2],0));
+ assert(s.accepted==3 && s.source_raw_hold==release_words[2] && s.native_raw_hold==release_words[2]);
+ InputUpdateClock bad=s;
+ bad.native_raw_hold^=1;
+ assert(!input_update_poll_mode(&bad,4,2,&v) && bad.accepted==3);
+ bad=s;bad.native_raw_press^=1;
+ assert(!input_update_poll_mode(&bad,4,2,&v) && bad.accepted==3);
+ assert(input_update_poll_mode(&s,4,2,&v)&&v==0xffff);
+ assert(input_update_context_accept(&s,4,0xffff,0)&&s.accepted==4);
+ assert(s.source_raw_hold==0xffff && s.native_raw_hold==0xffff && !s.source_raw_press && !s.native_raw_press);
+ assert(input_update_poll_mode(&s,5,3,&v)&&v==0xfff7);
+ InputUpdateClock extra=s;
+ assert(!input_update_context_accept(&extra,5,0xffff,0)); /* duplicate source acceptance */
+ extra=s;assert(!input_update_neutral_refresh(&extra,5,release_words[2],0,0,0));
+ assert(input_update_neutral_refresh(&s,5,0xffff,0,0,0));
+ assert(s.accepted==4 && s.neutral_refreshes==1);
+ assert(input_update_poll_mode(&s,6,1,&v)&&v==0xffff);
+ assert(input_update_context_accept(&s,6,0xfff7,0));
+ assert(s.source_raw_press==8 && s.native_raw_press==8); /* one fresh Start */
+ assert(input_update_poll_mode(&s,7,1,&v)&&v==0xffff);
+ assert(input_update_context_accept(&s,7,0xffff,0));
+ assert(s.accepted==6 && !s.source_raw_press && !s.native_raw_press);
+ assert(!input_update_context_accept(&s,8,0xffff,0));
+ }
+ /* A source-normal attack can be accepted through the declared compatible
+ * auxiliary context. The following normal release reconciles its raw
+ * fields before a separately verified zero-effect refresh. */
+ const uint16_t aux_release[]={0xffff,0xffff,0xffdf,0xffff,0xffdf,0x5fdf,0xffff,0xfff7,0xffff};
+ const uint8_t aux_source[]={0,0,0,0,0,0,0,0,0};
+ const uint8_t aux_native[]={0,0,0,1,0,1,0,0,0};
+ assert(input_update_init(&s,aux_release,9));assert(input_update_set_contexts(&s,aux_source));
+ s.compatible_contexts=1;s.protected_raw_mask=8;
+ assert(input_update_context_accept(&s,1,aux_release[0],0));
+ for(unsigned i=1;i<6;i++) {
+ assert(input_update_poll_mode(&s,i+1,1,&v));
+ assert(input_update_context_accept(&s,i+1,aux_release[i],aux_native[i]));
+ }
+ assert(s.accepted==6 && s.source_raw_hold==0x5fdf && s.source_raw_press==0xa000);
+ assert(s.native_raw_hold==0xffdf && s.native_raw_press==0);
+ InputUpdateClock protected_bad=s;protected_bad.native_raw_hold^=8;
+ assert(!input_update_poll_mode(&protected_bad,7,2,&v));
+ protected_bad=s;protected_bad.native_raw_press^=8;
+ assert(!input_update_poll_mode(&protected_bad,7,2,&v));
+ assert(input_update_poll_mode(&s,7,2,&v)&&v==0xffff);
+ InputUpdateClock wrong=s;
+ assert(!input_update_context_accept(&wrong,7,0xffff,1)); /* release cannot be auxiliary */
+ wrong=s;assert(!input_update_context_accept(&wrong,7,0xffdf,0)); /* changed release */
+ assert(input_update_context_accept(&s,7,0xffff,0)&&s.accepted==7);
+ assert(s.source_raw_hold==0xffff && s.native_raw_hold==0xffff && !s.source_raw_press && !s.native_raw_press);
+ assert(input_update_poll_mode(&s,8,3,&v)&&v==0xfff7);
+ assert(input_update_neutral_refresh(&s,8,0xffff,0,0,0)&&s.accepted==7&&s.neutral_refreshes==1);
+ assert(input_update_poll_mode(&s,9,1,&v)&&v==0xffff);
+ assert(input_update_context_accept(&s,9,0xfff7,0)&&s.source_raw_press==8&&s.native_raw_press==8);
+ assert(input_update_poll_mode(&s,10,1,&v)&&v==0xffff);
+ assert(input_update_context_accept(&s,10,0xffff,0)&&s.accepted==9);
+ puts("input update clock: priming, neutrals, held duration, stalls, reopening, draining and fail-stops PASS");
+}
diff --git a/runtime/tests/test_irq_cache_block.c b/runtime/tests/test_irq_cache_block.c
new file mode 100644
index 000000000..2b9d5ad86
--- /dev/null
+++ b/runtime/tests/test_irq_cache_block.c
@@ -0,0 +1,78 @@
+#include "cpu_state.h"
+#include "psx_bios_backend.h"
+#include "psx_icache.h"
+#include "psx_cyc.h"
+#include "source_gpu_runtime.h"
+#include "gpu.h"
+#include "gpu_render.h"
+#include "timers.h"
+#include "dma.h"
+#include "interrupts.h"
+#include "dirty_ram_interp.h"
+#include
+#include
+#include
+extern void psx_irq_set_cause_ptr(uint32_t*);
+extern void memory_init(const char*);
+extern uint8_t *memory_get_ram_ptr(void);
+extern void memory_set_sr_ptr(const uint32_t*);
+extern uint32_t psx_read_word(uint32_t);
+extern uint16_t psx_read_half(uint32_t);
+extern uint8_t psx_read_byte(uint32_t);
+extern void psx_write_word(uint32_t,uint32_t);
+extern void psx_write_half(uint32_t,uint16_t);
+extern void psx_write_byte(uint32_t,uint8_t);
+extern void psx_rfe_escape_check(CPUState*);
+extern CPUState *debug_cpu_ptr;
+static unsigned events,compiled,sliced;
+static CPUState *active;
+static void trace(uint32_t pc) {
+ if(pc==0x80000500 || pc==0x80000080)
+ printf("event %08X %llu %08X %08X %u %u %u %08X\n",pc,(unsigned long long)psx_get_cycle_count(),active->cop0[14],active->cop0[12],active->gpr[24],active->gpr[25],psx_read_word(0x1000),active->cop0[13]);
+ assert(++events<1000);
+}
+static void run(CPUState *cpu,uint32_t pc,uint32_t stop) {
+ for(unsigned i=0;i<100;i++) {
+ if(pc==stop || pc==0xbfc00200u){cpu->pc=pc;return;}
+ cpu->pc=pc;psx_check_interrupts_at(cpu,pc);
+ if(pc==0x80000540 && !psx_get_in_exception()) {
+ if(psx_slice_block(cpu,pc,5,0))sliced++;
+ else {
+ compiled++;
+ for(unsigned j=0;j<3;j++) {
+ if(!j)psx_icache_fetch(cpu,pc);else psx_cpu_step_boundary(cpu,pc+4*j);
+ psx_cyc_step(cpu,1u<<24);cpu->gpr[24]++;
+ }
+ psx_cpu_step_boundary(cpu,pc+12);psx_cyc_step(cpu,1u<<24);
+ int taken=(psx_read_word(pc+12)>>26)==4?cpu->gpr[24]==0:cpu->gpr[24]!=0;
+ psx_icache_fetch(cpu,pc+16);psx_cyc_step(cpu,0);
+ cpu->pc=taken?0x80000554:0x80000554; /* ordinary BNE with one delay slot */
+ }
+ } else if((pc==0x80000500 || pc==0x80000554) && !psx_get_in_exception()) {
+ /* Setup/tail are precision controls, leaving only the middle
+ * pure-ALU block eligible for the compiled fast path under test. */
+ assert(psx_slice_block(cpu,pc,4,1));
+ } else assert(dirty_ram_dispatch(cpu,pc,stop));
+ psx_cyc_batch_flush();psx_rfe_escape_check(cpu);pc=cpu->pc;assert(pc);
+ }
+ assert(!"authored dispatch bound");
+}
+static void dispatch(CPUState*c,uint32_t pc){run(c,pc,c->cop0[14]);}
+static const PsxBiosImageInfo info={.image_id="AUTHORED-CACHE-IRQ",.image_sha256="authored"};
+static const PsxBiosBackend backend={&info,dispatch,run,0,0};
+int main(int argc,char **argv) {
+ assert(argc==6);memory_init(argv[1]);
+ FILE*f=fopen(argv[2],"rb");assert(f);assert(fread(memory_get_ram_ptr(),1,0x2000,f)==0x2000);fclose(f);
+ timers_init();dma_init();interrupts_init();gr_init((uint16_t*)gpu_get_vram());gpu_init();
+ CPUState c={0};active=&c;c.read_word=psx_read_word;c.read_half=psx_read_half;c.read_byte=psx_read_byte;c.write_word=psx_write_word;c.write_half=psx_write_half;c.write_byte=psx_write_byte;
+ c.gpr[9]=atoi(argv[3]);c.gpr[11]=0x1f801120;c.gpr[12]=atoi(argv[4])?0x18:8;c.gpr[16]=0x1000;c.gpr[31]=0xbfc00200;c.cop0[12]=0x401;c.read_fudge=32;c.ld_which_t=32;
+ debug_cpu_ptr=&c;memory_set_sr_ptr(&c.cop0[12]);psx_irq_set_cause_ptr(&c.cop0[13]);psx_write_word(0x1f801074,atoi(argv[5])?0:0x40);
+ c.gpr[25]=0x80000500; /* declared synthetic ROM's entry-address register */
+ dirty_ram_mark_executable_range(0,0x2000);psx_bios_activate(&backend);psx_icache_reset();g_psx_icache_active=1;psx_write_word(0xfffe0130,0x800);
+ source_gpu_runtime_init();g_psx_precise_slice=1;
+ g_input_instruction_histogram_active=1;g_input_instruction_histogram_callback=trace;
+ run(&c,0x80000500,0xbfc00200);psx_cyc_batch_flush();
+ printf("final %llu %u %u %u %u %08X %08X\n",(unsigned long long)psx_get_cycle_count(),c.gpr[24],psx_read_word(0x1000),compiled,sliced,c.cop0[14],psx_read_word(0x1004));
+ fprintf(stderr,"timer count=%u mode=%X target=%u mask=%X stat=%X next=%u\n",timers_read(0x1f801120),timers_read(0x1f801124),timers_read(0x1f801128),psx_read_word(0x1f801074),psx_read_word(0x1f801070),timers_cycles_to_irq(~0u));
+ assert(c.gpr[24]==3 && psx_read_word(0x1000)==9);
+}
diff --git a/runtime/tests/test_irq_prefetch.c b/runtime/tests/test_irq_prefetch.c
new file mode 100644
index 000000000..5c9092b93
--- /dev/null
+++ b/runtime/tests/test_irq_prefetch.c
@@ -0,0 +1,63 @@
+/* Production IRQ poll, real authored guest handler and return, private object
+ * linkage. Source oracle uses independent original-core guest JR/RFE setup.
+ * Direct poll starts at the same committed target boundary with no pending load.
+ */
+#include
+#include
+#include
+#include
+#include "cpu_state.h"
+#include "psx_bios_backend.h"
+#include "psx_icache.h"
+#include "psx_cycles.h"
+#include "interrupts.h"
+extern unsigned char* memory_get_ram_ptr(void);
+extern void dirty_ram_mark_executable_range(uint32_t,uint32_t);
+extern int dirty_ram_dispatch(CPUState*,uint32_t,uint32_t);
+extern void memory_set_sr_ptr(const uint32_t*);
+extern uint32_t psx_read_word(uint32_t);
+extern uint16_t psx_read_half(uint32_t);
+extern uint8_t psx_read_byte(uint32_t);
+extern void psx_write_word(uint32_t,uint32_t);
+extern void psx_write_half(uint32_t,uint16_t);
+extern void psx_write_byte(uint32_t,uint8_t);
+extern void psx_rfe_escape_check(CPUState*);
+extern CPUState *debug_cpu_ptr;
+extern uint32_t i_stat,i_mask;
+static CPUState *active;static unsigned count,stores;
+typedef struct {uint32_t pc,sr,cause,epc,r8,word,tag;uint64_t cycle;} Event;
+static Event events[128];
+void __wrap_input_instruction_histogram_sample(uint32_t pc) {
+ if(count<128)events[count++]=(Event){pc,active->cop0[12],active->cop0[13],active->cop0[14],active->gpr[8],psx_read_word(0x1000),g_psx_icache_tv[(0x508&0xffc)>>2],psx_get_cycle_count()};
+}
+static void write_word(uint32_t a,uint32_t v){if((a&0x1fffffff)==0x1000)stores++;psx_write_word(a,v);}
+static void dispatch_call(CPUState*c,uint32_t pc,uint32_t stop){
+ for(unsigned i=0;i<100;i++){
+ if(pc==stop||pc==0xbfc00200u){c->pc=pc;return;}
+ c->pc=pc;
+ psx_check_interrupts_at(c,pc);
+ if(!dirty_ram_dispatch(c,pc,stop)){fprintf(stderr,"unhandled %08X\n",pc);exit(4);}
+ psx_rfe_escape_check(c);pc=c->pc;
+ if(!pc){fprintf(stderr,"null return\n");exit(5);}
+ }
+ fprintf(stderr,"dispatch bound\n");exit(6);
+}
+static void dispatch(CPUState*c,uint32_t pc){dispatch_call(c,pc,c->cop0[14]);}
+static const PsxBiosImageInfo info={.image_id="AUTHORED-IRQ",.image_sha256="authored"};
+static const PsxBiosBackend backend={&info,dispatch,dispatch_call,0,0};
+int main(int argc,char**argv){
+ if(argc!=6)return 2;uint32_t alias=strtoul(argv[1],0,16);int warm=atoi(argv[2]),irq=atoi(argv[3]),setup=atoi(argv[5]);
+ FILE*f=fopen(argv[4],"rb");if(!f)return 3;fread(memory_get_ram_ptr(),1,0x2000,f);fclose(f);dirty_ram_mark_executable_range(0,0x2000);
+ CPUState c={0};active=&c;c.read_word=psx_read_word;c.read_half=psx_read_half;c.read_byte=psx_read_byte;c.write_word=write_word;c.write_half=psx_write_half;c.write_byte=psx_write_byte;
+ c.read_fudge=32;c.ld_which_t=32;c.gpr[8]=10;c.gpr[16]=0x1000;c.gpr[29]=0x1ff000;c.cop0[12]=0x101;c.cop0[13]=irq?0x100:0;c.pc=alias|0x508;
+ if(irq==2)c.cop0[12]=1; /* pending IP0, IM0 masked */
+ if(irq==3)c.cop0[12]=0x100; /* pending/enabled IP0, IEc clear */
+ c.gpr[19]=0x1004;c.gpr[27]=alias|0x508;
+ if(setup){c.cop0[12]=0x600000;c.cop0[13]=0;}
+ psx_bios_activate(&backend);debug_cpu_ptr=&c;memory_set_sr_ptr(&c.cop0[12]);psx_icache_reset();g_psx_icache_active=1;
+ if(warm && alias<0xa0000000u)for(unsigned i=0;i<4;i++)g_psx_icache_tv[0x500/4+i]=(alias|0x500)+4*i;
+ g_input_instruction_histogram_active=1;g_input_instruction_histogram_callback=__wrap_input_instruction_histogram_sample;psx_next_service_cycle=1000000;
+ dispatch_call(&c,setup?0x80000600u:alias|0x508,0xbfc00200);psx_cyc_batch_flush();
+ printf("{\"cycles\":%llu,\"r8\":%u,\"memory\":%u,\"stores\":%u,\"epc\":%u,\"sr\":%u,\"cause\":%u,\"pc\":%u,\"events\":[",(unsigned long long)psx_cycle_count,c.gpr[8],psx_read_word(0x1000),stores,c.cop0[14],c.cop0[12],c.cop0[13],c.pc);
+ for(unsigned i=0;ipc,(unsigned long long)e->cycle,e->sr,e->cause,e->epc,e->r8,e->word,e->tag);}puts("]}");return 0;
+}
diff --git a/runtime/tests/test_irq_recognition.c b/runtime/tests/test_irq_recognition.c
new file mode 100644
index 000000000..20324f097
--- /dev/null
+++ b/runtime/tests/test_irq_recognition.c
@@ -0,0 +1,89 @@
+#include
+#include
+#include
+#include
+#include "cpu_state.h"
+#include "psx_bios_backend.h"
+#include "psx_icache.h"
+#include "psx_cycles.h"
+#include "interrupts.h"
+#include "timers.h"
+extern const PsxBiosBackend Test_psx_bios_backend;
+extern void Test_psx_dispatch_call(CPUState*,uint32_t,uint32_t);
+extern unsigned char* memory_get_ram_ptr(void);
+extern void dirty_ram_mark_executable_range(uint32_t,uint32_t);
+extern void memory_set_sr_ptr(const uint32_t*);
+extern void psx_irq_set_cause_ptr(uint32_t*);
+extern uint32_t psx_read_word(uint32_t);
+extern uint16_t psx_read_half(uint32_t);
+extern uint8_t psx_read_byte(uint32_t);
+extern void psx_write_word(uint32_t,uint32_t);
+extern void psx_write_half(uint32_t,uint16_t);
+extern void psx_write_byte(uint32_t,uint8_t);
+extern CPUState *debug_cpu_ptr;
+extern uint32_t i_stat,i_mask;
+extern uint64_t g_dispatch_static_hits;
+extern uint64_t g_irq_deliver_count,g_slice_irq_taken;
+#ifndef AUTHORED_LOAD_RETURN_ONLY
+extern void Test_func_1FC00034(CPUState*);
+#endif
+static CPUState *active;static unsigned count,stores;
+typedef struct {uint32_t pc,sr,cause,epc,t8,t9,word,v0,ra,sp;uint64_t cycle;} Event;
+static Event events[128];
+void __wrap_input_instruction_histogram_sample(uint32_t pc) {
+ if(count<128)events[count++]=(Event){pc,active->cop0[12],active->cop0[13],active->cop0[14],active->gpr[24],active->gpr[25],psx_read_word(0x1000),active->gpr[2],active->gpr[31],active->gpr[29],psx_get_cycle_count()};
+}
+static void write_word(uint32_t a,uint32_t v){if((a&0x1fffffff)==0x1000)stores++;psx_write_word(a,v);}
+int main(int argc,char**argv) {
+ if(argc!=5)return 2;unsigned target=atoi(argv[2]),irq=atoi(argv[3]),masked=atoi(argv[4]);
+ unsigned char*ram=memory_get_ram_ptr();FILE*f=fopen(argv[1],"rb");if(!f)return 3;
+ fread(ram+0x500,1,0x3c,f);fclose(f);
+ uint32_t handler[]={0x401a7000,0x3c1b1f80,0xa7601070,0x03400008,0x42000010};
+ memcpy(ram+0x80,handler,sizeof(handler));psx_write_word(0x1000,10);
+ dirty_ram_mark_executable_range(0x80,0x20);
+ CPUState c={0};active=&c;c.read_word=psx_read_word;c.read_half=psx_read_half;c.read_byte=psx_read_byte;c.write_word=write_word;c.write_half=psx_write_half;c.write_byte=psx_write_byte;
+ c.read_fudge=32;c.ld_which_t=32;c.gpr[9]=target;c.gpr[11]=0x1f801120;c.gpr[12]=irq?0x18:8;c.gpr[16]=0x1000;c.gpr[29]=0x1ff000;c.gpr[31]=0xbfc00200;c.cop0[12]=0x401;
+ if(getenv("AUTHORED_LOAD_RETURN")){
+ c.gpr[31]=0x80000524;c.gpr[19]=0xbfc00200;c.cop0[12]=0;
+ psx_write_word(0x1feffc,0xbfc00200);
+ }
+ psx_bios_activate(&Test_psx_bios_backend);debug_cpu_ptr=&c;memory_set_sr_ptr(&c.cop0[12]);psx_irq_set_cause_ptr(&c.cop0[13]);psx_icache_reset();g_psx_icache_active=1;timers_init();i_mask=masked?0:0x40;i_stat=0;
+ g_input_instruction_histogram_active=1;g_input_instruction_histogram_callback=__wrap_input_instruction_histogram_sample;psx_next_service_cycle=0;
+ const char*slice=getenv("AUTHORED_TEST_SLICE");
+ if(slice && slice[0]=='1') {
+ g_psx_precise_slice=1;
+ if(!psx_slice_block_impl(&c,0x80000500,14,1))return 6;
+ Test_psx_dispatch_call(&c,c.pc,0xbfc00200);
+ } else {
+ if(slice && slice[0]=='2')g_psx_precise_slice=1;
+ Test_psx_dispatch_call(&c,0x80000500,0xbfc00200);
+ }
+ psx_cyc_batch_flush();
+#ifndef AUTHORED_LOAD_RETURN_ONLY
+ if(getenv("AUTHORED_COOLDOWN_PROBE")) {
+ uint64_t before=psx_get_cycle_count(),delivered=g_irq_deliver_count,taken=g_slice_irq_taken;
+ unsigned s1=c.gpr[17];
+ int initial_cooldown=0;psx_get_freeze_diag(NULL,NULL,NULL,&initial_cooldown,NULL,NULL);
+ i_mask|=8;psx_irq_raise(IRQ_DMA,0);c.pc=0;g_psx_precise_slice=1;
+ Test_func_1FC00034(&c);
+ psx_cyc_batch_flush();
+ fprintf(stderr,"cooldown-probe before=%llu after=%llu actual=%llu reported=%llu pc=%08X slot=%u\n",(unsigned long long)before,(unsigned long long)psx_get_cycle_count(),(unsigned long long)(g_irq_deliver_count-delivered),(unsigned long long)(g_slice_irq_taken-taken),c.pc,c.gpr[17]-s1);
+ if(initial_cooldown>0){
+ for(unsigned j=0;j<3;j++){c.pc=0;Test_func_1FC00034(&c);psx_cyc_batch_flush();}
+ int remaining=0;psx_get_freeze_diag(NULL,NULL,NULL,&remaining,NULL,NULL);
+ fprintf(stderr,"cooldown-repeat cycles=%llu slots=%u actual=%llu reported=%llu remaining=%d\n",(unsigned long long)(psx_get_cycle_count()-before),c.gpr[17]-s1,(unsigned long long)(g_irq_deliver_count-delivered),(unsigned long long)(g_slice_irq_taken-taken),remaining);
+ if(remaining>0){
+ psx_advance_cycles((uint32_t)remaining-1u);psx_cyc_batch_flush();
+ psx_check_interrupts_at(&c,0x80000534);
+ uint64_t below=g_irq_deliver_count-delivered;
+ psx_advance_cycles(1);psx_cyc_batch_flush();
+ psx_check_interrupts_at(&c,0x80000534);
+ fprintf(stderr,"cooldown-expiry below=%llu at=%llu pending=%u epc=%08X\n",(unsigned long long)below,(unsigned long long)(g_irq_deliver_count-delivered),i_stat&i_mask,c.cop0[14]);
+ }
+ }
+ }
+#endif
+ fprintf(stderr,"istat=%08X mask=%08X count=%u mode=%X target=%u next=%u\n",i_stat,i_mask,timers_read(0x1f801120),timers_read(0x1f801124),timers_read(0x1f801128),timers_cycles_to_irq(~0u));
+ printf("{\"cycles\":%llu,\"memory\":%u,\"stores\":%u,\"s0\":%u,\"s1\":%u,\"s2\":%u,\"ra\":%u,\"sp\":%u,\"t9\":%u,\"epc\":%u,\"sr\":%u,\"pc\":%u,\"static_hits\":%llu,\"events\":[",(unsigned long long)psx_cycle_count,psx_read_word(0x1000),stores,c.gpr[16],c.gpr[17],c.gpr[18],c.gpr[31],c.gpr[29],c.gpr[25],c.cop0[14],c.cop0[12],c.pc,(unsigned long long)g_dispatch_static_hits);
+ for(unsigned i=0;ipc,(unsigned long long)e->cycle,e->sr,e->cause,e->epc,e->t8,e->t9,e->word,e->v0,e->ra,e->sp);}puts("]}");return 0;
+}
diff --git a/runtime/tests/test_irq_vector_entry.c b/runtime/tests/test_irq_vector_entry.c
new file mode 100644
index 000000000..d4fd3118b
--- /dev/null
+++ b/runtime/tests/test_irq_vector_entry.c
@@ -0,0 +1,39 @@
+/* The verifier extracts the actual production entry-selection block. This
+ * checks architectural dispatch, not handler/RFE execution or cycle accuracy. */
+#include "cpu_state.h"
+#include
+#include
+#include
+#ifndef PSX_TEST_IRQ_VECTOR_INCLUDE
+#error "Run through verify_irq_vector.py to bind the production source block"
+#endif
+static unsigned reads;
+static uint32_t vector_words[2];
+static uint32_t read_vector(uint32_t addr) {
+ reads++;
+ return vector_words[(addr>>2)&1u];
+}
+static uint32_t production_entry(CPUState *cpu, uint32_t sr) {
+#include PSX_TEST_IRQ_VECTOR_INCLUDE
+ return target_pc;
+}
+int main(void) {
+ unsigned failures=0,cases=0;
+ for(unsigned kind=0;kind<3;kind++) for(unsigned bev=0;bev<2;bev++) {
+ CPUState cpu={0},before;
+ cpu.read_word=read_vector;
+ for(unsigned r=0;r<32;r++)cpu.gpr[r]=0x12340000u+r;
+ vector_words[0]=kind==2?0x08012345u:0x3c1a8001u;
+ vector_words[1]=kind==0?0x275a8000u:kind==1?0x375a8000u:0x00000000u;
+ before=cpu;reads=0;
+ uint32_t target=production_entry(&cpu,bev?0x00400401u:0x401u);
+ uint32_t expected=bev?0xbfc00180u:0x80000080u;
+ cases++;
+ if(target!=expected || reads || memcmp(&cpu,&before,sizeof(cpu))) {
+ fprintf(stderr,"vector kind%u BEV%u: dispatch=%08X expected=%08X data_reads=%u k0=%08X\n",kind,bev,target,expected,reads,cpu.gpr[26]);failures++;
+ }
+ }
+ if(failures)return 1;
+ printf("PASS %u architectural IRQ entry cases; vector instruction execution stays with guest dispatcher\n",cases);
+ return 0;
+}
diff --git a/runtime/tests/test_launcher_vulkan_option.py b/runtime/tests/test_launcher_vulkan_option.py
index ad460ece0..06d51d377 100644
--- a/runtime/tests/test_launcher_vulkan_option.py
+++ b/runtime/tests/test_launcher_vulkan_option.py
@@ -2,10 +2,10 @@
from pathlib import Path
root = Path(__file__).resolve().parents[2]
-main = (root / "runtime" / "src" / "main.cpp").read_text()
-config_h = (root / "recompiler" / "src" / "config_loader.h").read_text()
-config_cpp = (root / "recompiler" / "src" / "config_loader.cpp").read_text()
-runtime_cmake = (root / "runtime" / "runtime.cmake").read_text()
+main = (root / "runtime" / "src" / "main.cpp").read_text(encoding="utf-8")
+config_h = (root / "recompiler" / "src" / "config_loader.h").read_text(encoding="utf-8")
+config_cpp = (root / "recompiler" / "src" / "config_loader.cpp").read_text(encoding="utf-8")
+runtime_cmake = (root / "runtime" / "runtime.cmake").read_text(encoding="utf-8")
assert "runtime/" + "launcher" not in runtime_cmake
assert "Rml" + "Ui" not in runtime_cmake
diff --git a/runtime/tests/test_memory_dma_read_wait.c b/runtime/tests/test_memory_dma_read_wait.c
new file mode 100644
index 000000000..79f19f547
--- /dev/null
+++ b/runtime/tests/test_memory_dma_read_wait.c
@@ -0,0 +1,41 @@
+/* Exercise the actual production data-load charge, without a BIOS or game. */
+#include "cpu_state.h"
+#include "psx_cycles.h"
+#include
+#include
+#include
+#include
+#include "dma.h"
+#include
+uint64_t psx_cycle_count,psx_next_service_cycle;
+int psx_in_device_service,g_event_step_conservative;
+uint32_t g_psx_cyc_batch,g_psx_cyc_batch_limit;
+int g_ls_replay_active;
+static uint32_t penalty,queries,complete_on_charge;
+uint32_t dma_cpu_read_penalty(void) {queries++;return penalty;}
+void psx_devices_service_to_now(void) {if(complete_on_charge)penalty=0;}
+void psx_advance_cycles_slow(uint32_t n) {psx_cycle_count+=n;if(complete_on_charge)penalty=0;}
+#include PSX_TEST_MEMORY_READ_INCLUDE
+int main(void) {
+ CPUState cpu;
+ for(uint32_t p=0;p<=200;p+=5) {
+ penalty=p;memset(&cpu,0,sizeof(cpu));cpu.read_fudge=0x20;
+ psx_cycle_count=queries=0;
+ psx_cyc_readmem(&cpu,0x10000,4,2,3);
+ assert(psx_cycle_count==7+p && cpu.ld_absorb==5+p && queries==1);
+ memset(&cpu,0,sizeof(cpu));psx_cycle_count=queries=0;
+ psx_cyc_readmem(&cpu,0x1f801814,4,2,3);
+ assert(psx_cycle_count==3+p && cpu.ld_absorb==3+p && queries==1);
+ memset(&cpu,0,sizeof(cpu));cpu.read_fudge=0x20;
+ psx_cycle_count=queries=0;
+ psx_cyc_readmem(&cpu,0x1f800004,4,2,3);
+ assert(psx_cycle_count==0 && cpu.ld_absorb==0 && queries==0);
+ }
+ /* Completion while this load dispatches events must not erase the penalty
+ * sampled when the load started. The next load observes the cleared state. */
+ memset(&cpu,0,sizeof(cpu));cpu.read_fudge=0x20;
+ penalty=15;complete_on_charge=1;psx_cycle_count=queries=0;psx_next_service_cycle=11;
+ psx_cyc_readmem(&cpu,0x10000,4,2,3);
+ assert(psx_cycle_count==22 && cpu.ld_absorb==20 && penalty==0 && queries==1);
+ puts("PASS production load wait: RAM, MMIO, absorbed cycles, prior-load rule and scratchpad exclusion");return 0;
+}
diff --git a/runtime/tests/test_mmio_read_sample_order.c b/runtime/tests/test_mmio_read_sample_order.c
new file mode 100644
index 000000000..e436716e7
--- /dev/null
+++ b/runtime/tests/test_mmio_read_sample_order.c
@@ -0,0 +1,113 @@
+/* Exact production load helpers against an independent original-core oracle.
+ * Fetch/base/dependency setup is outside this fixture; the supplied start is
+ * the ReadMemory entry. No BIOS or retail instructions/data are used. */
+#ifndef PSX_TEST_REAL_STEP
+#define PSX_CYC_H /* Component mode excludes instruction step. */
+#endif
+#include "cpu_state.h"
+#include
+#include
+#include
+#include
+#include
+uint64_t psx_cycle_count;
+static uint64_t sample_cycle;
+static uint64_t g_psx_cycle_fast_limit;
+#ifdef PSX_TEST_DEADLINE
+int g_event_step_conservative=0;
+#else
+int g_event_step_conservative=1;
+#endif
+int g_ls_replay_active;
+static unsigned phase,reads;
+static int effect_test,device_flag,effect_order;
+static uint64_t effect_deadline;
+static void advance_clock(uint32_t n){
+ psx_cycle_count+=n;
+ if(effect_test && effect_deadline && psx_cycle_count>=effect_deadline){
+ effect_order=effect_order*10+2;device_flag=1;effect_deadline=0;
+ }
+}
+#ifdef PSX_TEST_REAL_STEP
+uint64_t psx_next_service_cycle;
+int psx_in_device_service,g_psx_cyc_bb_defer;
+uint32_t g_psx_cyc_batch,g_psx_cyc_batch_limit,*g_psx_cyc_local_acc;
+int g_psx_load_delay=1,g_ls_mode,g_ram_read_watch_active;
+volatile int g_ds_recording;
+uint32_t g_dma_cpu_read_wait;
+uint8_t *g_psx_ram;
+void psx_advance_cycles_slow(uint32_t n){advance_clock(n);}
+void psx_devices_service_to_now(void){advance_clock(0);}
+void debug_server_trace_ram_read_watch(uint32_t a,uint32_t b){(void)a;(void)b;}
+int psx_load_delay_enabled(void){return 1;}
+#else
+static void psx_advance_cycles(uint32_t n){advance_clock(n);}
+static void psx_cyc_base(CPUState*c){(void)c;}
+static void psx_cyc_deps(CPUState*c,uint32_t mask){(void)c;(void)mask;}
+static void psx_cyc_lds(CPUState*c){(void)c;}
+static int psx_load_delay_enabled(void){return 1;}
+#endif
+static uint32_t dma_cpu_read_penalty(void){return 0;}
+static uint32_t bus_read(uint32_t addr){
+ assert((addr&0x1fffffff)==0x1f801120);sample_cycle=psx_cycle_count;reads++;
+ if(effect_test){effect_order=effect_order*10+1;device_flag=0;}
+ return (uint32_t)(sample_cycle+phase)/8;
+}
+uint32_t psx_read_word(uint32_t a){return bus_read(a);}
+uint16_t psx_read_half(uint32_t a){return (uint16_t)bus_read(a);}
+uint8_t psx_read_byte(uint32_t a){return (uint8_t)bus_read(a);}
+#include PSX_TEST_READ_ORDER_INCLUDE
+#ifdef PSX_TEST_GENERATED_READ_INCLUDE
+#include PSX_TEST_GENERATED_READ_INCLUDE
+#endif
+static uint32_t perform_load(CPUState *c,unsigned width,unsigned lwc,uint32_t addr){
+#ifdef PSX_TEST_GENERATED_READ_INCLUDE
+ return generated_read(c,width,lwc,addr);
+#elif defined(PSX_TEST_REAL_STEP)
+ return lwc?psx_cyc_lwc2_read(c,addr):width==1?psx_cyc_load_byte(c,addr,3,0):width==2?psx_cyc_load_half(c,addr,3,0):psx_cyc_load_word(c,addr,3,0);
+#else
+ return lwc?psx_cyc_lwc2_read(c,addr):width==1?psx_cyc_load_byte(c,addr,3,0):width==2?psx_cyc_load_half_slow(c,addr,3,0):psx_cyc_load_word_slow(c,addr,3,0);
+#endif
+}
+int main(void){
+ unsigned width,lwc,fudge,p,start,value,sample,end,absorb,count=0,failures=0,value_failures=0;
+ while(scanf("%u %u %u %u %u %u %u %u %u",&width,&lwc,&fudge,&p,&start,&value,&sample,&end,&absorb)==9){
+ for(unsigned alias=0;alias<3;alias++){
+ CPUState c;memset(&c,0,sizeof(c));c.read_fudge=fudge;c.ld_which_t=0x20;
+ phase=p;psx_cycle_count=start;sample_cycle=0;reads=0;
+#ifdef PSX_TEST_REAL_STEP
+ c.ld_which_t=(uint8_t)fudge;
+ if(!lwc)psx_cycle_count--; /* The source oracle begins after the base step. */
+#ifdef PSX_TEST_DEADLINE
+ psx_cycle_count-=5;g_psx_cyc_batch=5;psx_next_service_cycle=start+1;
+#endif
+#endif
+ uint32_t addr=0x1f801120u|(alias==1?0x80000000u:alias==2?0xa0000000u:0);
+ uint32_t v=perform_load(&c,width,lwc,addr);
+ if(v!=value||sample_cycle!=sample||psx_cycle_count!=end||c.ld_absorb!=absorb||reads!=1){
+ if(!failures || (v!=value && !value_failures))fprintf(stderr,"case%u alias%u width%u lwc%u fudge%u phase%u start%u: value%u/%u sample%llu/%u end%llu/%u absorb%u/%u reads%u\n",count,alias,width,lwc,fudge,p,start,v,value,(unsigned long long)sample_cycle,sample,(unsigned long long)psx_cycle_count,end,c.ld_absorb,absorb,reads);
+ failures++;value_failures+=(v!=value);
+ }
+ }
+ count++;
+ }
+ assert(count==512);printf("512 source vectors x3 aliases: failures=%u value_failures=%u\n",failures,value_failures);
+ for(unsigned kind=0;kind<4;kind++){
+ CPUState c;memset(&c,0,sizeof(c));c.ld_which_t=0;
+ unsigned lwc=kind==3,width=kind==0?1:kind==1?2:4;
+ phase=0;reads=0;psx_cycle_count=1000;
+#ifdef PSX_TEST_REAL_STEP
+ if(!lwc)psx_cycle_count--;
+#endif
+ effect_test=1;device_flag=1;effect_order=0;effect_deadline=1002;
+#ifdef PSX_TEST_DEADLINE
+ psx_cycle_count-=5;g_psx_cyc_batch=5;psx_next_service_cycle=effect_deadline;
+#endif
+ (void)perform_load(&c,width,lwc,0xbf801120);
+ if(sample_cycle!=1001 || psx_cycle_count!=(lwc?1002u:1003u) || effect_order!=12 || device_flag!=1 || reads!=1){
+ fprintf(stderr,"side-effect case %u: sample=%llu end=%llu order=%d flag=%d reads=%u\n",kind,(unsigned long long)sample_cycle,(unsigned long long)psx_cycle_count,effect_order,device_flag,reads);failures++;
+ }
+ effect_test=0;
+ }
+ puts("4 side-effect order cases checked: read/clear before completion event/set");return failures?1:0;
+}
diff --git a/runtime/tests/test_muldiv_deferred.c b/runtime/tests/test_muldiv_deferred.c
new file mode 100644
index 000000000..fcf0beeb9
--- /dev/null
+++ b/runtime/tests/test_muldiv_deferred.c
@@ -0,0 +1,43 @@
+#include "psx_cyc.h"
+#include
+#include
+uint64_t psx_cycle_count,psx_next_service_cycle=UINT64_MAX;
+uint32_t g_psx_cyc_batch,g_psx_cyc_batch_limit;
+uint32_t *g_psx_cyc_local_acc;
+int g_psx_cyc_bb_defer,psx_in_device_service,g_event_step_conservative,g_ls_replay_active;
+void psx_devices_service_to_now(void){assert(0);}
+void psx_advance_cycles_slow(uint32_t n){psx_cycle_count+=n;}
+#include PSX_TEST_MULDIV_INCLUDE
+static uint32_t local;
+static void reset(uint64_t clock,unsigned batch,unsigned local_value) {
+ psx_cycle_count=clock;g_psx_cyc_batch=batch;local=local_value;
+ g_psx_cyc_local_acc=local_value?&local:NULL;g_psx_cyc_bb_defer=1;
+}
+int main(int argc,char **argv) {
+ assert(argc==2);CPUState cpu={0};
+ /* Authored sequence mirrors the measured shift/MULT/store/MFLO timing.
+ * The preceding load commits on shift; MULT consumes its give-back.
+ * Source timestamps give 8 cycles before the next instruction fetch. */
+ cpu.ld_which_t=3;cpu.ld_absorb=5;reset(1000,0,0);
+ psx_cyc_step(&cpu,1u<<2);psx_cyc_step(&cpu,(1u<<2)|(1u<<3));
+ psx_muldiv_set(&cpu,7);
+ psx_cyc_step(&cpu,(1u<<17)|(1u<<4));psx_cyc_step(&cpu,1u<<6);
+ psx_muldiv_stall(&cpu);psx_cyc_batch_flush();
+ if(psx_cycle_count!=1008){printf("FAIL measured synthetic span: expected1008 actual%llu\n",(unsigned long long)psx_cycle_count);return 1;}
+ for(unsigned pending=0;pending<64;pending++)for(unsigned mode=0;mode<3;mode++) {
+ reset(1000,mode==1?0:pending,mode==0?0:pending);psx_muldiv_set(&cpu,37);
+ assert(cpu.muldiv_ts_done==1037+(mode==2?2:1)*pending);
+ }
+ FILE *f=fopen(argv[1],"r");assert(f);unsigned latency,elapsed,give,want_give;unsigned long long want_clock,want_deadline;unsigned cases=0;
+ while(fscanf(f,"%u %u %u %llu %llu %u",&latency,&elapsed,&give,&want_clock,&want_deadline,&want_give)==6) {
+ for(unsigned pending=0;pending<=elapsed;pending++)for(unsigned mode=0;mode<2;mode++) {
+ memset(&cpu,0,sizeof(cpu));cpu.read_absorb_which=3;cpu.read_absorb[3]=give;cpu.muldiv_ts_done=1000+latency;
+ reset(1000+elapsed-pending,mode?0:pending,mode?pending:0);
+ psx_muldiv_stall(&cpu);psx_cyc_batch_flush();
+ assert(psx_cycle_count==want_clock && cpu.muldiv_ts_done==want_deadline && cpu.read_absorb[3]==want_give);
+ assert(g_psx_cyc_batch==0 && local==0);cases++;
+ }
+ }
+ assert(feof(f));fclose(f);assert(cases==25944);
+ puts("PASS multiply deadline, measured load/MULT/read span, and 25944 exact-source stall/deferred cases");
+}
diff --git a/runtime/tests/test_overlay_step_boundary.py b/runtime/tests/test_overlay_step_boundary.py
new file mode 100644
index 000000000..2ee1cb638
--- /dev/null
+++ b/runtime/tests/test_overlay_step_boundary.py
@@ -0,0 +1,100 @@
+"""Compile/load the real overlay shim and check observer batch ownership."""
+from pathlib import Path
+import ctypes
+import os
+import shutil
+import subprocess
+import tempfile
+
+ROOT = Path(__file__).resolve().parents[2]
+GCC = shutil.which('gcc')
+assert GCC, 'overlay boundary test requires GCC on PATH'
+SOURCE = r'''
+#include
+#include "cpu_state.h"
+#include "psx_icache.h"
+#include "overlay_dispatch_preamble.c.inc"
+static int installed, replay, calls, advances;
+static uint64_t clock_value;
+static uint32_t last_address;
+static CPUState *last_cpu;
+static int enabled(int include_replay) {
+ return installed && (include_replay || !replay);
+}
+static void advance(uint32_t cycles) { clock_value += cycles; advances++; }
+static void boundary(CPUState *cpu, uint32_t address) {
+ assert(enabled(0));
+ assert(s_pending_cycles == 0);
+ last_cpu = cpu; last_address = address; calls++;
+}
+#ifdef _WIN32
+__declspec(dllexport)
+#else
+__attribute__((visibility("default")))
+#endif
+int test_boundary(void) {
+ CPUState cpu = {0};
+ OverlayCallbacks cbs = {0};
+ cbs.advance_cycles = advance;
+ cbs.cpu_step_boundary_enabled = enabled;
+ cbs.cpu_step_boundary = boundary;
+ overlay_init(&cbs);
+ assert(overlay_abi() == PSX_OVERLAY_ABI_TAG);
+ for (int state = 0; state < 4; state++) {
+ installed = state & 1; replay = (state >> 1) & 1;
+ calls = advances = 0; clock_value = 0;
+ assert(!!g_psx_cpu_step_boundary_callback == !!installed);
+ psx_advance_cycles(7);
+ psx_cpu_step_boundary(&cpu, 0x80010004);
+ int active = installed && !replay;
+ assert(calls == active && advances == active);
+ assert(clock_value == (active ? 7 : 0));
+ if (active) assert(last_cpu == &cpu && last_address == 0x80010004);
+ overlay_flush_cycles();
+ assert(clock_value == 7 && advances == 1);
+ }
+ /* Live installation changes must be queried afresh for cached followers. */
+ installed = replay = calls = advances = 0; clock_value = 0;
+ psx_advance_cycles(3);
+ psx_cpu_step_boundary(&cpu, 0x80010008);
+ installed = 1;
+ psx_advance_cycles(5);
+ psx_cpu_step_boundary(&cpu, 0x8001000C);
+ assert(clock_value == 8 && calls == 1 && advances == 1);
+ assert(last_address == 0x8001000C);
+ installed = 0;
+ psx_advance_cycles(2);
+ psx_cpu_step_boundary(&cpu, 0x80010010);
+ assert(clock_value == 8 && calls == 1);
+ overlay_flush_cycles();
+ assert(clock_value == 10 && advances == 2);
+ /* A host with no observer callbacks retains ordinary batch behavior. */
+ cbs.cpu_step_boundary_enabled = 0; cbs.cpu_step_boundary = 0;
+ overlay_init(&cbs);
+ assert(!g_psx_cpu_step_boundary_callback);
+ psx_advance_cycles(9);
+ psx_cpu_step_boundary(&cpu, 0x80010014);
+ assert(clock_value == 10 && calls == 1);
+ overlay_flush_cycles();
+ assert(clock_value == 19 && advances == 3);
+ return 6;
+}
+'''
+
+for opt in ('0', '2'):
+ with tempfile.TemporaryDirectory() as td:
+ p = Path(td)
+ (p/'boundary.c').write_text(SOURCE, encoding='utf-8')
+ dll = p/('boundary.dll' if os.name == 'nt' else 'boundary.so')
+ command = [GCC, '-shared', '-fPIC', '-O'+opt,
+ '-DPSX_OVERLAY_DLL_BUILD', '-DPSX_ENABLE_BLOCK_CYCLES=1',
+ '-DPSX_NO_DEBUG_TOOLS', '-I'+str(ROOT/'runtime/include'),
+ str(p/'boundary.c'), '-o', str(dll), '-lm']
+ subprocess.run(command, check=True, capture_output=True)
+ lib = ctypes.CDLL(str(dll))
+ assert lib.test_boundary() == 6
+ if os.name == 'nt':
+ import _ctypes
+ _ctypes.FreeLibrary(lib._handle)
+ del lib
+print('12 overlay boundary cases passed at O0/O2')
diff --git a/runtime/tests/test_psx_cycle_event_boundaries.c b/runtime/tests/test_psx_cycle_event_boundaries.c
index 83b6e8854..bf522b019 100644
--- a/runtime/tests/test_psx_cycle_event_boundaries.c
+++ b/runtime/tests/test_psx_cycle_event_boundaries.c
@@ -48,6 +48,8 @@ uint32_t cdrom_cycles_to_irq(uint32_t mask) {
return s_cd_ready ? UINT32_MAX : s_cd_cycles_remaining;
}
uint32_t dma_cycles_to_internal_event(void) { return UINT32_MAX; }
+void source_gpu_runtime_advance(void) {}
+uint32_t source_gpu_runtime_cycles_to_event(void) { return UINT32_MAX; }
uint32_t dma_cycles_to_deliverable_irq(uint32_t mask) {
(void)mask;
return UINT32_MAX;
diff --git a/runtime/tests/test_release_zip.py b/runtime/tests/test_release_zip.py
index 2431963af..09e095ea0 100644
--- a/runtime/tests/test_release_zip.py
+++ b/runtime/tests/test_release_zip.py
@@ -17,7 +17,7 @@
(stage / "mods" / "package").mkdir(parents=True)
(stage / "Tomba Recompiled.exe").write_bytes(b"exe")
(stage / "mods" / "package" / "manifest.toml").write_text(
- 'id = "test"\n', encoding="utf-8"
+ 'id = "test"\n', encoding="utf-8", newline="\n"
)
output = base / "release.zip"
subprocess.run(
diff --git a/runtime/tests/test_sio_ack_timing.c b/runtime/tests/test_sio_ack_timing.c
new file mode 100644
index 000000000..a05ccd3db
--- /dev/null
+++ b/runtime/tests/test_sio_ack_timing.c
@@ -0,0 +1,94 @@
+/* Production SIO controller with an explicit synthetic guest clock.
+ * No BIOS, retail data or disk-backed card. */
+#include "../src/sio.c"
+#include
+#include
+uint32_t i_stat, i_mask, g_debug_current_func_addr, g_debug_last_store_pc;
+static uint64_t clock_now;
+static int test_unrelated_card_words;
+uint64_t psx_get_cycle_count(void) { return clock_now; }
+int psx_get_in_exception(void) { return 0; }
+uint8_t psx_read_byte(uint32_t a) { (void)a;return 0; }
+uint32_t psx_read_word(uint32_t a) {
+ if(test_unrelated_card_words && (a==0x800A6C10u || a==0x800B4E30u)) return 1;
+ return 0;
+}
+void psx_write_word(uint32_t a,uint32_t v) { (void)a;(void)v; }
+uint32_t memory_get_sr(void) { return 0; }
+void psx_irq_raise(uint32_t bit,uint32_t detail) { (void)detail;i_stat|=1u<0);
+
+ /* Source pulse is independent of reads and of IRQ enable. */
+ for(int irq=0;irq<2;irq++) {
+ setup(1,irq);sio_write(0x1F801040,1);
+ advance(1088+63);assert(!(stat()&0x80));advance(1);
+ assert(stat()&0x80);assert(!!(i_stat&0x80)==irq);
+ for(int i=0;i<100;i++)assert(stat()&0x80);
+ assert(clock_now==1152);advance(31);assert(stat()&0x80);
+ advance(1);assert(!(stat()&0x80));assert(!g_sio_timing_active);
+ assert(!!(stat()&0x200)==irq); /* pulse expiry does not clear IRQ latch */
+ assert(i_stat&(1u<<9)); /* never consume unrelated SPU IRQ */
+ assert(sio_snapshot_bytes()==0 && !sio_snapshot_read(NULL,0));
+ }
+ /* CTRL ACK clears only IRQ; source intentionally does not reassert it. */
+ setup(1,1);sio_write(0x1F801040,1);jump(1152);
+ sio_write(0x1F80104A,0x1013);assert(!(stat()&0x200));assert(stat()&0x80);
+ jump(31);assert(stat()&0x80);jump(1);assert(!(stat()&0x80));
+ /* A complete coarse interval crosses shift, ACK-on and ACK-off exactly. */
+ setup(1,1);sio_write(0x1F801040,1);jump(1184);
+ assert((stat()&0x282)==0x202 && !g_sio_timing_active);
+ /* Cancellation before ACK, while ACK active, and RESET. */
+ setup(1,1);sio_write(0x1F801040,1);jump(1100);sio_write(0x1F80104A,0);jump(1000);
+ assert(!(stat()&0x280) && !(i_stat&0x80));
+ setup(1,1);sio_write(0x1F801040,1);jump(1152);sio_write(0x1F80104A,0);
+ assert(!(stat()&0x80));
+ setup(1,1);sio_write(0x1F801040,1);jump(1152);sio_write(0x1F80104A,0x40);
+ assert(!(stat()&0x280) && !g_sio_timing_active);
+ /* Digital poll bytes and no ACK after the final response. */
+ setup(1,1);
+ const unsigned tx[]={1,0x42,0,0,0},rx[]={0xFF,0x41,0x5A,0xFF,0xFF};
+ for(int i=0;i<5;i++) {
+ sio_write(0x1F801040,tx[i]);jump(1088);
+ assert(sio_read(0x1F801040)==rx[i]);jump(64);
+ assert(!!(stat()&0x80)==(i<4));jump(32);
+ }
+ /* Absent selected slot never fabricates ACK. */
+ setup(1,1);sio_write(0x1F80104A,0x3003);sio_write(0x1F801040,1);jump(2000);
+ assert(!(stat()&0x280) && !(i_stat&0x80));
+ puts("SIO source digital ACK: exact deadlines, read-independent pulse, IRQ clear/mask, default, packet, cancellation and cold-boot guard PASS");
+ return 0;
+}
diff --git a/runtime/tests/test_sio_card_repair.c b/runtime/tests/test_sio_card_repair.c
new file mode 100644
index 000000000..f5f425c26
--- /dev/null
+++ b/runtime/tests/test_sio_card_repair.c
@@ -0,0 +1,26 @@
+/* Isolate the pre-existing globally enabled title repair using production SIO.
+ * Synthetic unrelated RAM happens to match the repair's fixed-address probes.
+ * No BIOS, disc, guest execution or real memory card is required. */
+#define main unused_ack_fixture_main
+#include "test_sio_ack_timing.c"
+#undef main
+int main(void) {
+ for(int enabled=-1;enabled<2;enabled++) {
+#ifdef _WIN32
+ _putenv_s("PSX_APE_CARD_UNSTICK",enabled<0?"":enabled?"1":"0");
+#else
+ if(enabled<0) unsetenv("PSX_APE_CARD_UNSTICK");
+ else setenv("PSX_APE_CARD_UNSTICK",enabled?"1":"0",1);
+#endif
+ s_ape_unstick_env=-1;s_ape_unstick_pending=0;s_ape_unstick_cool_cyc=0;
+ setup(1,0);i_mask=0x0D;i_stat=1;test_unrelated_card_words=1;
+ sio_write(0x1F801040,0x81);jump(1088);
+ assert(memcard_is_present(0)==0 && !(sio_peek_stat()&0x80));
+ sio_write(0x1F80104A,0x40); /* abort the absent-card probe */
+ printf("repair=%d, no card, CTRL reset: I_MASK=%02X I_STAT=%02X\n",enabled,i_mask,i_stat);
+ assert(i_mask==(enabled>0?0x8D:0x0D));
+ assert(i_stat==(enabled>0?0x81:0x01));
+ }
+ puts("Legacy card repair: unrelated RAM can inject IRQ7/mask; existing off switch prevents it PASS");
+ return 0;
+}
diff --git a/runtime/tests/test_source_cpu_block_bound.c b/runtime/tests/test_source_cpu_block_bound.c
new file mode 100644
index 000000000..0e08444da
--- /dev/null
+++ b/runtime/tests/test_source_cpu_block_bound.c
@@ -0,0 +1,36 @@
+#include "psx_icache.h"
+#include "source_cpu_block_bound.h"
+#include
+#include
+uint64_t psx_cycle_count,psx_next_service_cycle=1000000;
+uint32_t g_psx_cyc_batch,g_psx_cyc_batch_limit;
+int g_psx_cyc_bb_defer,psx_in_device_service,g_event_step_conservative;
+int g_ls_replay_active;
+uint32_t *g_psx_cyc_local_acc;
+uint64_t psx_get_cycle_count(void){return psx_cycle_count+g_psx_cyc_batch+(g_psx_cyc_local_acc?*g_psx_cyc_local_acc:0);}
+static uint8_t ram[0x200000];uint8_t *g_psx_ram=ram;
+void psx_devices_service_to_now(void){assert(0);}
+void psx_advance_cycles_slow(uint32_t n){psx_cycle_count+=n;}
+uint32_t psx_read_word(uint32_t addr){assert(addr==0xfffe0130);return 0x800;}
+uint32_t psx_gte_cmd_latency(uint32_t cmd){(void)cmd;assert(0);return 0;}
+int main(void) {
+ /* Independently execute a five-instruction ALU/branch-slot timing path.
+ * Entry at offset8 crosses a cold cache line. Seven clocks to a future
+ * IRQ is beyond the old count5 but lies inside the real block17. */
+ const uint32_t code[]={0x00a63823,0x000212c3,0x3046001f,0x14c00002,0};
+ memcpy(ram+0x508,code,sizeof(code));
+ for(unsigned warm=0;warm<2;warm++) {
+ CPUState cpu={0};cpu.ld_which_t=32;cpu.read_fudge=32;
+ psx_cycle_count=0;psx_icache_reset();g_psx_icache_active=1;
+ if(warm)for(unsigned i=0;i<8;i++)g_psx_icache_tv[(0x500>>2)+i]=0x80000500+4*i;
+ uint64_t before=psx_get_cycle_count();
+ uint32_t bound=source_cpu_block_bound(&cpu,0x80000508,5,7);
+ assert(psx_get_cycle_count()==before);
+ for(unsigned i=0;i<5;i++){psx_icache_fetch_interp(&cpu,0x80000508+4*i);psx_cyc_step(&cpu,0);}
+ psx_cyc_batch_flush();
+ assert(psx_get_cycle_count()==(warm?5u:17u));
+ assert(bound>=psx_get_cycle_count());
+ assert(warm || (5u<7u && 7u<=bound && 7u
+#include
+#include
+
+static void setup(SourceGPUCommandProjection *s) {
+ source_gpu_command_cold(s);
+ assert(source_gpu_command_write(s,0xe3000000));
+ assert(source_gpu_command_write(s,0xe407ffff));
+ assert(source_gpu_command_update(s,128));
+}
+static void quad(SourceGPUCommandProjection *s,unsigned n) {
+ assert(source_gpu_command_write(s,0x280000ff));
+ assert(source_gpu_command_write(s,0));
+ assert(source_gpu_command_write(s,n));
+ assert(source_gpu_command_write(s,n<<16));
+ assert(source_gpu_command_write(s,n|(n<<16)));
+}
+static void report(const SourceGPUCommandProjection *s,const char *label) {
+ printf("%s %d %u %u %d %llu %u %u\n",label,s->budget,s->phase,
+ s->count,source_gpu_command_ready(s),(unsigned long long)s->last_update,
+ s->command,s->count?s->queue[0]:0);
+}
+int main(int argc,char **argv) {
+ SourceGPUCommandProjection s;
+ if(argc==3) {
+ unsigned n=(unsigned)atoi(argv[1]),chain=(unsigned)atoi(argv[2]);
+ setup(&s); report(&s,"before_start");
+ assert(source_gpu_command_update(&s,146)); quad(&s,n);
+ if(chain && source_gpu_command_ready(&s))
+ for(unsigned i=0;i<6;++i) assert(source_gpu_command_write(&s,0));
+ report(&s,"after_start"); report(&s,"immediate_state");
+ assert(source_gpu_command_update(&s,256));
+ assert(source_gpu_command_update(&s,384));
+ if(chain && n==32) {
+ assert(source_gpu_command_ready(&s));
+ for(unsigned i=0;i<6;++i) assert(source_gpu_command_write(&s,0));
+ }
+ report(&s,"before_release");
+ for(unsigned cycle=512;cycle<=768;cycle+=128) assert(source_gpu_command_update(&s,cycle));
+ report(&s,"final_state");
+ /* Empty queue: final explicit service reproduces the retained scalar
+ * state, without claiming the intervening raster deadline sequence. */
+ assert(source_gpu_command_update(&s,13056)); report(&s,"settled_state");
+ assert(s.first_triangles==1 && s.second_triangles==1); return 0;
+ }
+ setup(&s); assert(source_gpu_command_update(&s,146)); quad(&s,32);
+ assert(s.budget==-356 && s.phase==2 && s.count==1);
+ assert(source_gpu_command_update(&s,146)); /* Same-time call cannot drain. */
+ assert(s.second_triangles==0);
+ assert(source_gpu_command_update(&s,323)); assert(s.budget==-2 && s.phase==2);
+ assert(source_gpu_command_update(&s,324)); /* Exact zero-credit boundary. */
+ assert(s.budget==-542 && s.phase==0 && source_gpu_command_ready(&s)==1);
+ assert(source_gpu_command_write(&s,0)); assert(s.budget==-542);
+ quad(&s,1); /* Negative budget queues a complete next quad. */
+ assert(s.count==5 && s.phase==0 && source_gpu_command_ready(&s)==0);
+ assert(source_gpu_command_update(&s,595));
+ assert(s.first_triangles==2 && s.second_triangles==1 && s.count==1 && s.budget==-85);
+ assert(source_gpu_command_update(&s,595)); assert(s.second_triangles==1);
+ assert(source_gpu_command_update(&s,638)); assert(s.second_triangles==2 && s.budget==-45);
+ assert(!source_gpu_command_update(&s,637)); assert(s.error==SOURCE_GPU_COMMAND_REVERSE_TIME);
+ setup(&s); assert(!source_gpu_command_write(&s,0x2a000000));
+ assert(s.error==SOURCE_GPU_COMMAND_UNSUPPORTED);
+ setup(&s); assert(source_gpu_command_update(&s,146)); quad(&s,32);
+ for(unsigned i=1;i<16;++i) assert(source_gpu_command_write(&s,0));
+ assert(!source_gpu_command_write(&s,0)); assert(s.error==SOURCE_GPU_COMMAND_OVERFLOW);
+ setup(&s); assert(source_gpu_command_write(&s,0x28000000));
+ assert(source_gpu_command_write(&s,0)); assert(source_gpu_command_write(&s,0x10001));
+ assert(source_gpu_command_write(&s,0x10000)); /* General flat first triangle. */
+ setup(&s); assert(source_gpu_command_write(&s,0x28000000));
+ assert(source_gpu_command_write(&s,0)); assert(source_gpu_command_write(&s,1));
+ assert(source_gpu_command_write(&s,0x10000));
+ assert(source_gpu_command_write(&s,0x10002)); /* Independent fourth vertex. */
+ setup(&s); assert(source_gpu_command_write(&s,0xe4000000));
+ assert(source_gpu_command_write(&s,0x28000000));
+ assert(source_gpu_command_write(&s,0)); assert(source_gpu_command_write(&s,1));
+ assert(source_gpu_command_write(&s,0x10000)); /* Inclusive clip admits one pixel. */
+ assert(source_gpu_command_write(&s,0x10001)); assert(s.budget==125);
+ setup(&s); assert(source_gpu_command_write(&s,0xe4080000)); /* Clip Y512 is not admitted. */
+ assert(source_gpu_command_write(&s,0x28000000));
+ assert(source_gpu_command_write(&s,0)); assert(source_gpu_command_write(&s,1));
+ assert(!source_gpu_command_write(&s,0x10000));
+ setup(&s); assert(source_gpu_command_write(&s,0xe5000001)); /* Offset X1. */
+ assert(source_gpu_command_write(&s,0x28000000));
+ assert(source_gpu_command_write(&s,0x000003ff)); /* Offset makes X1024: still unqualified. */
+ assert(source_gpu_command_write(&s,0x000003fe));
+ assert(!source_gpu_command_write(&s,0x000103ff));
+ const uint32_t ordinary[]={0x01000000,0xe1000200,0xe2007fff,0xe6000001};
+ for(unsigned c=0;c
+#include
+#include
+extern CPUState *debug_cpu_ptr;
+extern void memory_init(const char *);
+extern uint8_t *memory_get_ram_ptr(void);
+extern void memory_set_sr_ptr(const uint32_t *);
+extern void psx_write_word(uint32_t,uint32_t);
+extern uint32_t psx_read_word(uint32_t);
+extern void psx_write_half(uint32_t,uint16_t);
+extern uint16_t psx_read_half(uint32_t);
+extern void psx_write_byte(uint32_t,uint8_t);
+extern uint8_t psx_read_byte(uint32_t);
+static uint32_t labels[128],label_count,seen,observed;
+static CPUState cpu;
+extern uint32_t __real_gpu_read_gpustat(void);
+uint32_t __wrap_gpu_read_gpustat(void) {
+ SourceGPUCommandProjection s;source_gpu_runtime_copy(0,&s);
+ uint32_t value=__real_gpu_read_gpustat();
+ printf("gpustat %llu %08x %d %u %u %llu\n",(unsigned long long)psx_get_cycle_count(),value,s.budget,s.phase,s.count,(unsigned long long)s.last_update);
+ return value;
+}
+static void trace(uint32_t pc) {
+ assert(++observed<200000);
+ if(observed<20)printf("trace %08x %llu\n",pc,(unsigned long long)psx_get_cycle_count());
+ for(unsigned i=0;i
+#include
+#include
+extern void memory_init(const char *);
+extern uint8_t *memory_get_ram_ptr(void);
+static void to(uint64_t time) {assert(time>=psx_get_cycle_count());psx_advance_cycles((uint32_t)(time-psx_get_cycle_count()));psx_devices_service_to_now();}
+static void report(uint64_t sample) {
+ SourceGPUServiceClock clock;SourceGPUCommandProjection s;source_gpu_runtime_copy(&clock,&s);
+ printf("state %llu %u %u %u %u %llu %u %u %u %u\n",(unsigned long long)sample,(uint32_t)s.budget,s.phase,s.count,(unsigned)source_gpu_command_ready(&s),(unsigned long long)s.last_update,s.command,s.count?s.queue[0]:0,dma_read(0x1f8010a8),dma_read(0x1f8010f4));
+}
+int main(int argc,char **argv) {
+ assert(argc==5);memory_init(argv[1]);timers_init();dma_init();interrupts_init();
+ gr_init((uint16_t*)gpu_get_vram());gpu_init();source_gpu_runtime_init();
+ FILE *ram=fopen(argv[2],"rb");assert(ram);assert(fread(memory_get_ram_ptr(),1,0x3000,ram)==0x3000);fclose(ram);
+ gpu_write_gp0(0xe3000000);gpu_write_gp0(0xe407ffff);
+ to(98);dma_write(0x1f8010f0,0x800);to(108);dma_write(0x1f8010f4,0x840000);
+ to(120);dma_write(0x1f8010a0,0x1000);to(121);dma_write(0x1f8010a4,0);
+ to(146);dma_write(0x1f8010a8,0x01000401);report(146);
+ FILE *times=fopen(argv[3],"r");assert(times);unsigned long long time;
+ while(fscanf(times,"%llu",&time)==1){assert(time>=146);to(time);report(time);}fclose(times);
+ assert((dma_read(0x1f8010a8)&0x1000000u)==0);
+ if(atoi(argv[4])) {
+ unsigned n=atoi(argv[4]);(void)n;
+ /* Renderer visibility is not source-qualified by this timing test. */
+ assert(gpu_vram_peek(0,0)!=0);
+ }
+ return 0;
+}
diff --git a/runtime/tests/test_source_gpu_environment.c b/runtime/tests/test_source_gpu_environment.c
new file mode 100644
index 000000000..96dd901ac
--- /dev/null
+++ b/runtime/tests/test_source_gpu_environment.c
@@ -0,0 +1,39 @@
+/* Source-authored, explicitly timed ordinary setup; production GPU/DMA service. */
+#include "source_gpu_runtime.h"
+#include "gpu.h"
+#include "gpu_render.h"
+#include "dma.h"
+#include "timers.h"
+#include "interrupts.h"
+#include "psx_cycles.h"
+#include
+#include
+extern void memory_init(const char *);
+extern uint8_t *memory_get_ram_ptr(void);
+static void to(uint64_t t){assert(t>=psx_get_cycle_count());psx_advance_cycles((uint32_t)(t-psx_get_cycle_count()));psx_devices_service_to_now();}
+int main(int argc,char **argv){
+ assert(argc==4);memory_init(argv[1]);timers_init();dma_init();interrupts_init();gr_init((uint16_t*)gpu_get_vram());gpu_init();source_gpu_runtime_init();
+ FILE *f=fopen(argv[2],"rb");assert(f);assert(fread(memory_get_ram_ptr(),1,0x3000,f)==0x3000);fclose(f);
+ f=fopen(argv[3],"r");assert(f);unsigned long long t;unsigned addr,word;char op;int result=0;
+ while(fscanf(f," %c %llu %x %x",&op,&t,&addr,&word)==4){
+ to(t);
+ if(op=='W'){
+ if(addr==0x1f801810)gpu_write_gp0(word);
+ else if(addr==0x1f801814)gpu_write_gp1(word);
+ else dma_write(addr,word);
+ }else if(op=='R'){
+ uint32_t actual=gpu_read_gpustat();
+ printf("register %llu %08x %08x %08x\n",t,addr,word,actual);
+ if((actual&addr)!=(word&addr))result=3;
+ }else if(op=='V'){
+ char path[64];snprintf(path,sizeof(path),"vram-%u.bin",addr);
+ FILE *dump=fopen(path,"wb");assert(dump);
+ assert(fwrite(gpu_get_vram(),1,1024u*512u*2u,dump)==1024u*512u*2u);
+ assert(fclose(dump)==0);
+ }else{
+ SourceGPUCommandProjection s;source_gpu_runtime_copy(0,&s);
+ printf("state %llu %u %u %u %u %llu %u %u\n",t,(uint32_t)s.budget,s.phase,s.count,(unsigned)source_gpu_command_ready(&s),(unsigned long long)s.last_update,s.command,s.count?s.queue[0]:0);
+ }
+ }
+ fclose(f);return result;
+}
diff --git a/runtime/tests/test_source_gpu_frame_request.c b/runtime/tests/test_source_gpu_frame_request.c
new file mode 100644
index 000000000..5e7bd8f1c
--- /dev/null
+++ b/runtime/tests/test_source_gpu_frame_request.c
@@ -0,0 +1,28 @@
+/* Fixed-range source frame request stream, independent of hardware VBlank.
+ * CPU origin is a retained authored uncached-loop pre-fetch; later steps cost
+ * five clocks, as independently qualified against actual CPU execution. */
+#include "source_gpu_service_clock.h"
+#include
+#include
+#include
+static SourceGPUServiceClock *active;
+static void service(void *context,uint64_t cycle,unsigned kind) {
+ (void)context;
+ if(kind==SOURCE_GPU_EVENT_FRAME_END)
+ printf("frame %llu %llu %u\n",(unsigned long long)cycle,
+ (unsigned long long)active->frame_request_cycle,active->raster.rises);
+}
+int main(int argc,char **argv) {
+ assert(argc==4);SourceGPUServiceClock clock;source_gpu_service_cold(&clock);active=&clock;
+ assert(input_route_raster_gp1(&clock.raster,0x07000010u|((unsigned)atoi(argv[1])<<10)));
+ uint64_t cycle=strtoull(argv[2],0,10),limit=strtoull(argv[3],0,10);
+ assert(source_gpu_service_to(&clock,cycle,service,0));assert(!clock.frame_pending);
+ while(cycle<=limit && clock.frame_returns<2) {
+ assert(source_gpu_service_cpu_boundary(&clock,cycle,service,0));
+ unsigned before=clock.frame_returns;
+ assert(source_gpu_service_cpu_boundary(&clock,cycle,service,0));
+ assert(clock.frame_returns==before); /* same timestamp cannot consume twice */
+ cycle+=5;
+ }
+ assert(clock.frame_returns==2);return 0;
+}
diff --git a/runtime/tests/test_source_gpu_pending_frame.c b/runtime/tests/test_source_gpu_pending_frame.c
new file mode 100644
index 000000000..3c5692189
--- /dev/null
+++ b/runtime/tests/test_source_gpu_pending_frame.c
@@ -0,0 +1,37 @@
+/* Source-observed late quad, with an automatic CPU-step frame return. */
+#include "source_gpu_service_clock.h"
+#include "source_gpu_command_projection.h"
+#include
+#include