Integrate qualified TAS timing and execution profile

Rebased onto upstream after #362: the Ape Escape card-repair hunks are
dropped with the removed block, and the pace walk keeps upstream's uncapped
deadline consumption plus the source ACK-pulse edge.

(cherry picked from commit 8235a7700f)
This commit is contained in:
Pegasus
2026-09-09 10:36:36 +03:00
committed by Alexandros Mandravillis
parent 5968162fe9
commit 3bd62b3099
121 changed files with 9599 additions and 279 deletions
+24 -2
View File
@@ -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).
@@ -368,6 +374,17 @@ if(BUILD_TESTING)
target_include_directories(host_path_test PRIVATE include)
add_test(NAME host_path_test COMMAND host_path_test)
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
@@ -713,6 +730,7 @@ if(BUILD_TESTING)
pal_cadence_host_refresh
sdl3_main_single_include
spu_sample_scheduler_default
overlay_step_boundary
vk_build_default
vk_color_self_barrier
vk_command_buffer_batching
@@ -797,6 +815,10 @@ if(BUILD_TESTING AND Python3_Interpreter_FOUND AND NOT MSVC)
"${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_gpu_upload_history.py"
--cc ${CMAKE_C_COMPILER})
endif()
if(BUILD_TESTING)
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../tools/tasreplays/tests"
"${CMAKE_CURRENT_BINARY_DIR}/tas-models")
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
+11 -5
View File
@@ -1477,7 +1477,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
@@ -1812,14 +1813,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";
+9
View File
@@ -209,6 +209,12 @@ std::set<uint32_t> ControlFlowAnalyzer::find_block_boundaries(const Function& fu
uint32_t instr = *instr_opt;
// A synchronous syscall can transfer to the guest exception vector.
// Its return is the following instruction (there is no delay slot),
// which must be independently re-enterable by the flat dispatcher.
if ((instr & 0xFC00003Fu) == 0x0000000Cu && addr + 4 < func.end_addr)
boundaries.insert(addr + 4);
if (is_control_flow(instr)) {
ControlFlowInstr cf = analyze_instruction(addr, instr);
@@ -514,6 +520,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);
+91 -8
View File
@@ -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<ContinuationLabel> local_continuations;
@@ -769,8 +787,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 —
@@ -778,9 +797,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);
};
auto emit_patch_range_guard = [&](uint32_t addr) {
@@ -910,6 +934,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
@@ -940,6 +1000,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) {
@@ -1915,6 +1979,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();
@@ -1923,16 +1988,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<uint32_t, uint32_t> 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";
@@ -2079,6 +2149,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";
@@ -2111,6 +2183,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";
@@ -2222,6 +2295,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"
+180 -2
View File
@@ -96,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);
}
@@ -202,6 +202,32 @@ 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.full;
}
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.full.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.full.find(needle, first + 1) == std::string::npos,
"a fetch is not emitted twice for one instruction");
}
}
} // namespace
void patch_range_guards(BiosConfig config) {
@@ -260,7 +286,7 @@ void patch_range_delay_boundaries(BiosConfig config) {
}
}
int main() {
int main(int argc, char** argv) {
BiosConfig config{};
config.config_path = "test://full-function-emitter";
config.load_address = kBase;
@@ -277,6 +303,137 @@ 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<uint32_t> 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<uint8_t> 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<uint32_t> 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<uint8_t> 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<uint32_t> words = {
0xA5690008u, 0xA56C0004u, 0x95680000u, 0x8E020000u, 0x8E180000u,
0x00000000u, 0x2719FFFFu, 0x14400005u, 0xAE190000u,
0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u,
0x03E00008u, 0x26310001u};
std::vector<uint8_t> 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<uint32_t> 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<uint8_t> 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<uint32_t> words(0x130u / 4u, 0u);
const auto put = [&](uint32_t at, std::initializer_list<uint32_t> 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<uint8_t> 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();
@@ -284,6 +441,27 @@ int main() {
complementary_lwl_lwr_stays_native();
patch_range_guards(config);
patch_range_delay_boundaries(config);
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.full.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);
@@ -1159,6 +1159,76 @@ void cfg_fallthrough_reachability_test() {
check_tail(true, "independent alias entry");
}
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 <assert.h>\n#include <stdint.h>\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() {
@@ -1175,6 +1245,8 @@ int main() {
jump_table_producer_codegen_test();
cfg_codegen_load_delay_test();
cfg_fallthrough_reachability_test();
uncached_fetch_codegen_test();
syscall_codegen_transfer_test();
} catch (const std::exception& e) {
fmt::print(stderr, "FAIL unexpected exception: {}\n", e.what());
++failures;
+26 -5
View File
@@ -1516,8 +1516,10 @@ m.publish_shard_pair(sys.argv[2], sys.argv[3], sys.argv[4])
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
@@ -2221,8 +2223,10 @@ def check_candidate_capacity_publication():
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'
@@ -3000,6 +3004,8 @@ def check_interior_fragment_contract():
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()
# 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"
@@ -3951,4 +3957,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())
+85
View File
@@ -81,6 +81,80 @@ if(BUILD_TESTING)
else()
message(STATUS "gl_readback_region_test: not registered (requires MinGW, SDL3 headers/static library in PSX_GL_READBACK_SDL_ROOT and gcc.exe/g++.exe in PSX_GL_READBACK_COMPILER_BIN)")
endif()
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
$<TARGET_FILE:input_route_observer_test>)
add_executable(launcher_device_roundtrip_test
tests/test_launcher_device_roundtrip.cpp)
target_include_directories(launcher_device_roundtrip_test PRIVATE include)
@@ -654,6 +728,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)
@@ -666,6 +747,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(spu_gaussian_test tests/test_spu_gaussian.c)
target_include_directories(spu_gaussian_test PRIVATE include)
add_test(NAME spu_gaussian_test COMMAND spu_gaussian_test)
+33
View File
@@ -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"
)
+7
View File
@@ -141,6 +141,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);
@@ -224,6 +226,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;
@@ -288,6 +292,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 {
+70
View File
@@ -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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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->cursor<tape->count) {
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
+9
View File
@@ -84,6 +84,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);
@@ -130,6 +134,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);
@@ -203,6 +208,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. */
@@ -234,6 +241,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);
+13 -1
View File
@@ -18,12 +18,24 @@
#define PSXRECOMP_DMA_H
#include <stdint.h>
#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);
+4
View File
@@ -23,6 +23,7 @@
#define PSXRECOMP_EVENT_RING_H
#include <stdint.h>
#include <stdio.h>
#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. */
+6
View File
@@ -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 */
@@ -55,6 +59,8 @@ void gpu_texture_correction_stats(uint64_t *attempts, uint64_t *armed,
* packed RGB888 lives in the CPU mirror; treating A0 rects as 1555 FBO uploads
* 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);
+8
View File
@@ -12,6 +12,7 @@
* (a requested backend that fails to initialize falls back to software). */
#include <stdint.h>
#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,
+6
View File
@@ -2,6 +2,7 @@
#define PSX_GPU_SW_RENDERER_H
#include <stdint.h>
#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).
@@ -0,0 +1,13 @@
#ifndef PSX_INPUT_INSTRUCTION_HISTOGRAM_H
#define PSX_INPUT_INSTRUCTION_HISTOGRAM_H
#include <stdint.h>
#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
@@ -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;i<s_instruction_site_count;i++)if(s_instruction_sites[i]==pc)return 0;
s_instruction_sites[s_instruction_site_count++]=pc;
if(!*p)return 1;
if(*p++!=',')return 0;
}
}
static int input_instruction_histogram_configure(void) {
const char *p=getenv("PSX_INPUT_HISTOGRAM_RANGE");
const char *sites=getenv("PSX_INPUT_HISTOGRAM_SITES");
if(!p)return sites==NULL;
unsigned first,last;char extra;
if(sscanf(p,"%u,%u%c",&first,&last,&extra)!=2 || last<first ||
last-first>7 || 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<s_instruction_site_count;i++)if(s_instruction_sites[i]==pc) {
if(s_instruction_site_retained==8192)s_instruction_site_dropped++;
else {
unsigned n=s_instruction_site_retained++;
s_instruction_site_rows[n]=(InputInstructionSiteRow){pc,s_instruction_events,psx_cycle_count,input_instruction_effective_cycle(),g_psx_icache_tv[(pc & 0xFFCu)>>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;i<s_instruction_site_retained;i++) {
InputInstructionSiteRow *row=&s_instruction_site_rows[i];
fprintf(f,"%08X\t%llu\t%llu\t%llu\t%08X\t%d\n",row->pc,(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;i<s_instruction_site_retained;i++) {
InputInstructionSiteRow *site=&s_instruction_site_rows[i];
InputInstructionContextRow *row=&s_instruction_context_rows[i];
fprintf(f,"%08X\t%llu\t%d\t%d\t%08X\t%08X\t%08X\t%08X\t%llu\t%llu\n",site->pc,(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;
}
}
+43
View File
@@ -0,0 +1,43 @@
#ifndef PSX_INPUT_ROUTE_FIELD_CLOCK_H
#define PSX_INPUT_ROUTE_FIELD_CLOCK_H
#include <stdint.h>
/* 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
+52
View File
@@ -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 <stdint.h>
#include <stdio.h>
#include <string.h>
#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
+11
View File
@@ -0,0 +1,11 @@
#ifndef PSX_INPUT_ROUTE_OBSERVER_H
#define PSX_INPUT_ROUTE_OBSERVER_H
#include <stdint.h>
#include <stdio.h>
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
+108
View File
@@ -0,0 +1,108 @@
#ifndef PSX_INPUT_ROUTE_RASTER_CLOCK_H
#define PSX_INPUT_ROUTE_RASTER_CLOCK_H
#include <stdint.h>
#include <string.h>
/* 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=ticks<s->remaining ? (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
+134
View File
@@ -0,0 +1,134 @@
#ifndef PSX_INPUT_UPDATE_CLOCK_H
#define PSX_INPUT_UPDATE_CLOCK_H
#include <stdint.h>
/* 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;i<s->count;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
+223
View File
@@ -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;i<count;i++) for(uint32_t j=0;j<steps[i].frames;j++) words[n++]=steps[i].buttons;
if (!input_update_init(&s_update,words,total)) {free(words);return 0;}
const char *contexts_path=getenv("PSX_INPUT_UPDATE_CONTEXTS");
if (contexts_path) {
FILE *f=fopen(contexts_path,"rb"); uint8_t header[12];
if (!f) return 0;
if (fread(header,1,12,f)!=12 || memcmp(header,"PSXCTX1\0",8)) {fclose(f);return 0;}
uint32_t size=(uint32_t)header[8]|((uint32_t)header[9]<<8)|
((uint32_t)header[10]<<16)|((uint32_t)header[11]<<24);
uint8_t *contexts=malloc(total);
if (!contexts || size!=total || fread(contexts,1,total,f)!=total ||
fgetc(f)!=EOF || ferror(f) || !input_update_set_contexts(&s_update,contexts)) {
free(contexts);fclose(f);return 0;
}
if (fclose(f)) return 0;
}
const char *protected_mask=getenv("PSX_INPUT_UPDATE_PROTECTED_RAW_MASK");
if (protected_mask) {
char *end; unsigned long v=strtoul(protected_mask,&end,0);
if (!s_update.contexts || end==protected_mask || *end || v==0 || v>0xffff) 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<s_update_decision_count;i++) {
uint32_t mode=s_update_decision_open[i];
if(mode>=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;i<s_update_decision_count;i++) if(s_update_decision_frames[i]==s_frame_count) {
open=(int)s_update_decision_open[i];override=1;break;
}
if(s_refresh_wait_pressed)update_fail("neutral refresh missing pressed store");
if (!input_update_poll_mode(&s_update,s_frame_count,open,&word)) {
if (s_update.predicted) {
fprintf(s_update_log,"{\"kind\":\"decision_failure\",\"decision_frame\":%llu,\"observed_open\":0,\"accepted\":%u}\n",
(unsigned long long)s_update.last_poll,s_update.accepted); fflush(s_update_log);
}
update_fail("poll/acceptance phase mismatch");
}
fprintf(s_update_log,"{\"kind\":\"poll\",\"frame\":%llu,\"accepted\":%u,\"gate\":%u,\"word\":%u}\n",
(unsigned long long)s_frame_count,s_update.accepted,gate,word);
if(override)fprintf(s_update_log,"{\"kind\":\"measured_decision\",\"frame\":%llu,\"open\":%d}\n",(unsigned long long)s_frame_count,open);
if (fflush(s_update_log)) update_fail("log flush");
return word;
}
static void update_accept_write(uint32_t phys,uint8_t width,uint32_t pc,uint32_t ra,uint32_t old_val,uint32_t new_val) {
if(s_update_enabled && s_refresh_wait_pressed && phys==s_refresh_guard[0]) {
if(width!=2 || pc!=s_refresh_guard[1] || ra!=s_update_config[2] || old_val || new_val ||
update_peek16(s_update_config[0]) || update_peek16(s_refresh_guard[2]) || update_peek16(s_refresh_guard[3]))
update_fail("neutral refresh changed pressed/raw state");
s_refresh_wait_pressed=0;
fprintf(s_update_log,"{\"kind\":\"neutral_refresh\",\"frame\":%llu,\"accepted\":%u,\"word\":65535,\"held_before\":0,\"held_after\":0,\"pressed_before\":0,\"pressed_after\":0,\"raw_held\":0,\"raw_pressed\":0}\n",
(unsigned long long)s_frame_count,s_update.accepted);fflush(s_update_log);return;
}
if (!s_update_enabled || s_update.accepted==s_update.count || phys!=s_update_config[0] ||
pc!=s_update_config[1] || ra!=s_update_config[2] || width!=2) return;
uint16_t header=update_peek16(s_update_config[5]);
if (header!=s_update_config[6]) {
if (s_update.primed) update_fail("accepted packet header changed");
return;
}
uint16_t word=update_peek16(s_update_config[4]);
uint32_t selector=debug_cpu_ptr ? debug_cpu_ptr->gpr[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");
}
+15
View File
@@ -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);
+6 -2
View File
@@ -105,8 +105,8 @@
* 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. */
/* v23: native overlay forwarding for signed screen bounds and mod entry hooks. */
#define PSX_OVERLAY_ABI_VERSION 23
/* v24: instruction-boundary callbacks appended after v23 mod callbacks. */
#define PSX_OVERLAY_ABI_VERSION 24
/* Process-lifetime overlay candidate capacity. Every accepted manifest F
* record consumes one slot, even when another DLL carries an identical
@@ -340,6 +340,10 @@ typedef struct {
const PGXPHooks *pgxp;
int32_t (*ws_screen_x_bound)(int32_t vanilla);
void (*mod_function_entry)(CPUState *cpu, uint32_t address);
/* 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
@@ -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))
PSX_OVERLAY_EXPORT void overlay_flush_cycles(void) {
uint32_t cycles = s_pending_cycles;
s_pending_cycles = 0;
@@ -114,6 +119,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);
+10 -2
View File
@@ -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);
+32 -2
View File
@@ -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
+4
View File
@@ -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. */
+48
View File
@@ -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(bound<deadline)return (uint32_t)bound;
uint32_t phys=pc&0x1fffffffu;
if(count>0x80000u || phys>=0x200000u || (uint64_t)phys+4ull*count>0x200000u)return UINT32_MAX;
bound=(uint64_t)count*8u+stalls;
for(uint32_t i=0;i<count;i++) {
uint32_t word;memcpy(&word,g_psx_ram+phys+4u*i,4);
uint32_t op=word>>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<count;i++) {
uint32_t word;memcpy(&word,g_psx_ram+phys+4u*i,4);
uint32_t op=word>>26,rs=(word>>21)&31u,rt=(word>>16)&31u;
if(op==0x12u && (rs==0u || rs==2u) && rt!=0u)return 1;
}
return 0;
}
#endif
@@ -0,0 +1,59 @@
#ifndef PSX_SOURCE_CPU_BOUNDARY_PROBE_H
#define PSX_SOURCE_CPU_BOUNDARY_PROBE_H
#include <stdio.h>
#include <stdlib.h>
/* 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 || cycle<low)return;
if(cycle>high) {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
@@ -0,0 +1,290 @@
#ifndef PSX_SOURCE_GPU_COMMAND_PROJECTION_H
#define PSX_SOURCE_GPU_COMMAND_PROJECTION_H
#include <stdint.h>
#include <string.h>
#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->count<source_gpu_command_feedback_length(s->queue[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<h;y++) {
unsigned row=(y+(words[1]>>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(x<s->clip_x0)x=s->clip_x0;if(y<s->clip_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;row<bottom;row++) {
if(interlace && ((unsigned)row&1u)==s->skip_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->count<stride)return 1;
for(unsigned i=0;i<stride;i++)s->polygon_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;i<stride;i++)source_gpu_command_pop(s);
s->budget-=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->count<n)return 1;
int cost=source_gpu_command_block_cost(s,s->queue);
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;i<n;i++)s->dispatch.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->count<n)return 1;
int cost=source_gpu_command_polygon_cost(s,s->queue,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;i<n;i++)s->polygon_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(cycle<s->last_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
@@ -0,0 +1,76 @@
#ifndef PSX_SOURCE_GPU_POLYGON_PROJECTION_H
#define PSX_SOURCE_GPU_POLYGON_PROJECTION_H
#include <stdint.h>
/* 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]<input_x[0]?2:0;
for(int i=0;i<3;i++) {
v[i].x=input_x[i];v[i].y=input_y[i];v[i].original=i;
/* Coordinate wrap beyond this range is not yet a qualified draw. */
if(v[i].x < -1024 || v[i].x > 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:y<clip_top)break;
if(down?y<clip_top:y>clip_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(left<clip_left){width-=clip_left-left;left=clip_left;}
if(left+width>clip_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
+28
View File
@@ -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
@@ -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_deadline<s->dma_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(now<s->cycle) 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
+17
View File
@@ -0,0 +1,17 @@
#ifndef PSX_SOURCE_GPU_TEXTURE_H
#define PSX_SOURCE_GPU_TEXTURE_H
#include <stdint.h>
/* 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
+66
View File
@@ -0,0 +1,66 @@
#ifndef PSX_SOURCE_RAM_PAGE_PROBE_H
#define PSX_SOURCE_RAM_PAGE_PROBE_H
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 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
+73
View File
@@ -0,0 +1,73 @@
#ifndef PSX_TIMER1_SOURCE_CLOCK_H
#define PSX_TIMER1_SOURCE_CLOCK_H
#include <stdint.h>
#include <string.h>
/* 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((before<s->target && 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
+78
View File
@@ -0,0 +1,78 @@
#ifndef PSX_TIMER2_SOURCE_CLOCK_H
#define PSX_TIMER2_SOURCE_CLOCK_H
#include <stdint.h>
#include <string.h>
/* 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((before<s->target && 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->counter<s->target)?s->target:65536u;
uint32_t clocks=target-s->counter;
if(s->mode&0x200)clocks=clocks*8-s->divider;
return clocks<1024?clocks:1024;
}
#endif
+6
View File
@@ -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]);
+2
View File
@@ -325,6 +325,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
@@ -333,6 +334,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/kernel_patch_ranges.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
+377 -58
View File
@@ -660,6 +660,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. */
@@ -758,8 +768,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;
@@ -788,6 +801,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 =
@@ -824,6 +846,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) {
@@ -888,6 +913,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
@@ -916,11 +976,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);
@@ -1608,7 +1675,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;
@@ -1626,14 +1768,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. */
@@ -1674,6 +1822,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) {
@@ -1856,7 +2005,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 (xa_data_end_pending) {
@@ -1873,6 +2022,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;
@@ -1987,6 +2137,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_count<s_source_command_due || !source_clock_receive_ready())) return;
if(s_source_clock && s_source_command_phase<1) {
if(s_source_args_remaining>0) {
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;
@@ -2005,6 +2165,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;
@@ -2043,6 +2223,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))
@@ -2101,6 +2290,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()) {
@@ -2109,8 +2301,11 @@ 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;
else {
if (cdrom_lid_acknowledge_closed_shell(&s_lid))
stat_reg &= (uint8_t)~CDSTAT_SHELL;
}
set_irq(CDIRQ_ACK);
break;
@@ -2138,8 +2333,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;
@@ -2182,6 +2377,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);
@@ -2196,13 +2392,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++;
@@ -2210,26 +2410,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;
@@ -2405,6 +2612,7 @@ static void exec_command(uint8_t cmd) {
}
}
}
s_source_seek_paused = 0;
response_push(stat_reg);
set_irq(CDIRQ_ACK);
break;
@@ -2417,18 +2625,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();
s_source_seek_paused = 0; /* source STANDBY after plain seek */
read_min = seek_min;
read_sec = seek_sec;
read_sect = seek_sect;
stat_reg |= CDSTAT_SEEK;
response_push(stat_reg);
set_irq(CDIRQ_ACK);
{
int lat = seek_complete_delay_cycles();
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;
@@ -2453,8 +2667,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;
@@ -2464,24 +2678,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);
@@ -2504,6 +2717,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_count<s_source_reset_due)return;
s_source_reset_due=0;
/* Original SetAIP then ClearAIP presents only if receive is open at this
* instant; drive state completes even if the old ACK owns the FIFO. */
if(source_clock_receive_ready()) {
response_clear();response_push(stat_reg);
set_irq(CDIRQ_COMPLETE);fire_cdrom_irq();
}
cd_muted=0;spu_cd_audio_reset();xa_reset_decode();
mode_reg=0x20;
read_min=seek_min=0;read_sec=seek_sec=2;read_sect=seek_sect=0;
s_setloc_lba=0;setloc_seek_far=0;
s_source_seek_paused=1;
}
static void process_pending(uint32_t cycles) {
uint8_t done_cmd;
(void)cycles;
@@ -2515,6 +2744,7 @@ static void process_pending(uint32_t cycles) {
* under/over-counted mid-slice acks and forked Pause→Seek→ReadN). */
if (psx_cycle_count < pending.due_cyc) return;
if (irq_flag != 0) return;
if (!source_clock_receive_ready()) return;
done_cmd = pending.cmd;
pending.pending = 0;
@@ -2538,6 +2768,7 @@ static void process_pending(uint32_t cycles) {
case 0x07: /* MotorOn complete — motor now spinning */
stat_reg |= CDSTAT_MOTOR;
s_source_seek_paused = 0;
response_push(stat_reg);
set_irq(CDIRQ_COMPLETE);
fire_cdrom_irq();
@@ -2565,10 +2796,11 @@ static void process_pending(uint32_t cycles) {
case 0x15: /* SeekL complete */
case 0x16: /* SeekP complete */
stat_reg &= ~CDSTAT_SEEK;
stat_reg |= CDSTAT_READ; /* PSX-CD-003: GT1 waits for READ after seek */
/* A plain seek finishes paused. Only a subsequent Read starts READ;
* the implicit ReadN/S seek is a separate transition. */
stat_reg &= ~(CDSTAT_SEEK | CDSTAT_READ | CDSTAT_PLAY);
setloc_seek_far = 0;
setloc_pending = 0;
setloc_pending = 0;
response_push(stat_reg);
set_irq(CDIRQ_COMPLETE);
fire_cdrom_irq();
@@ -2709,15 +2941,17 @@ static void process_read_stream(uint32_t cycles) {
}
if (read_delay <= 0) {
/* The target is now eligible. READ replaces SEEK before the first
* data-ready response; the existing read timer carries both delays. */
stat_reg = (stat_reg & (uint8_t)~CDSTAT_SEEK) | CDSTAT_READ;
uint64_t timing_seq = cd_timing_begin_sector(
msf_to_lba(read_min, read_sec, read_sect));
if (irq_flag == 0) {
if (source_clock_receive_ready()) {
if (rb_available()) {
trace_cdrom('O', 0, (uint32_t)RB_.pos, 0);
}
if (deliver_read_sector()) {
if (deliver_read_sector(timing_seq)) {
cd_timing_flag(timing_seq, CDT_DATA);
cd_timing_arm_irq(timing_seq);
}
} else if (dma_cdrom_transfer_active()) {
/* Active multi-sector CD DMA with the data-ready INT still
@@ -2746,6 +2980,7 @@ static void process_read_stream(uint32_t cycles) {
pending_dataready_slot = s_ring_write;
s_cd_timing_pending_seq = timing_seq;
s_int1_pended++;
if(s_source_clock && irq_flag==0)pending_present_due=s_source_ready_due;
}
}
s_accel_block_accum = 0; /* the pipeline advanced; the next hold
@@ -2773,12 +3008,29 @@ static void present_pending_dataready(void) {
response_push(pending_dataready_stat);
s_ring_read = pending_dataready_slot; /* the slot this INT1 announced */
set_irq(CDIRQ_DATA_READY);
fire_cdrom_irq();
cd_timing_arm_irq(timing_seq);
fire_cdrom_irq();
s_dataready_fires++;
}
void cdrom_init(const char* cue_path) {
s_source_firmware_model=source_boot_model("PSX_CD_FIRMWARE_MODEL");
s_source_cold_status_model=source_boot_model("PSX_CD_COLD_STATUS_MODEL");
s_source_toc_seek_model=source_boot_model("PSX_CD_TOC_SEEK_MODEL");
s_source_explicit_seek_model=source_boot_model("PSX_CD_EXPLICIT_SEEK_MODEL");
cd_tape_free(&s_source_clock_tape);
s_source_clock=0;s_source_command_due=0;s_source_ready_due=0;s_source_clock_calls=0;s_source_reset_due=0;
s_source_command_phase=-1;s_source_args_remaining=0;
const char *clock_tape=getenv("PSX_CD_SOURCE_CLOCK_TAPE");
if(clock_tape && *clock_tape) {
const char *pipeline=getenv("PSX_CD_READ_START_MODEL");
if(!s_source_explicit_seek_model || !s_source_toc_seek_model ||
!pipeline || strcmp(pipeline,"octoshock-2.2.2-pipeline") ||
!cd_tape_load(&s_source_clock_tape,clock_tape)) {
fprintf(stderr,"[CDROM] Source clock needs a valid tape, source seek models and source pipeline\n");exit(2);
}
s_source_clock=1;
}
memset(param_fifo, 0, sizeof(param_fifo));
memset(response_fifo, 0, sizeof(response_fifo));
memset(s_sector_ring, 0, sizeof(s_sector_ring));
@@ -2834,8 +3086,10 @@ void cdrom_init(const char* cue_path) {
read_cmd = 0;
read_delay = 0;
seek_min = seek_sec = seek_sect = 0;
s_source_read_start_lba = 0;
s_setloc_lba = -1;
setloc_seek_far = 0;
setloc_pending = 0;
s_warm_routes_count = 0;
s_warm_route_configured = 0;
s_warm_route_enabled = 0;
@@ -2865,6 +3119,8 @@ void cdrom_init(const char* cue_path) {
}
stat_reg = has_disc() ? CDSTAT_MOTOR : CDSTAT_SHELL;
if(s_source_cold_status_model) stat_reg |= CDSTAT_SHELL;
s_source_seek_paused = has_disc() ? 1 : 0;
cdrom_debug_clear_trace();
cdrom_debug_clear_command_history();
trace_cdrom('N', 0, has_disc() ? 1u : 0u, 0);
@@ -2943,6 +3199,9 @@ void cdrom_write(uint32_t addr, uint32_t value) {
case 0x1F801802:
if (index_reg == 0) {
if(s_source_clock && queued_cmd.pending) {
fprintf(stderr,"[CDROM] Source clock post-command argument writes are not qualified\n");exit(2);
}
if (param_count < PARAM_FIFO_SIZE) {
param_fifo[param_count++] = val;
}
@@ -2982,6 +3241,7 @@ void cdrom_write(uint32_t addr, uint32_t value) {
if (had_active_irq && (irq_flag & 0x1F) == 0) {
cdrom_intc_request_latched = 0;
present_lid_open_irq_if_ready();
if(s_source_clock)s_source_ready_due=psx_cycle_count+2000u;
}
if (val & 0x40) {
param_count = 0;
@@ -2993,7 +3253,7 @@ void cdrom_write(uint32_t addr, uint32_t value) {
* second-response already past due_cyc; a queued command waits
* behind. */
if (pending_dataready && pending_present_due == 0)
pending_present_due = psx_cycle_count + CDROM_PEND_PRESENT_DELAY;
pending_present_due = psx_cycle_count + (s_source_clock?2000u:CDROM_PEND_PRESENT_DELAY);
process_pending(0);
try_execute_queued_command();
}
@@ -3013,6 +3273,10 @@ void cdrom_write(uint32_t addr, uint32_t value) {
uint32_t cdrom_cycles_to_irq(uint32_t i_mask) {
if (!(i_mask & (1u << 2))) return 0xFFFFFFFFu; /* IRQ_CDROM masked */
uint32_t best = 0xFFFFFFFFu;
if(s_source_clock && queued_cmd.pending && irq_flag==0) {
uint64_t due=s_source_command_due>s_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();
@@ -3022,6 +3286,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<best)best=d;
}
if (pending.pending) {
if (psx_cycle_count < pending.due_cyc) {
uint32_t d = (uint32_t)cycles_until_due(pending.due_cyc);
@@ -3049,19 +3317,25 @@ void cdrom_advance(uint32_t cycles) {
* Menu CD asymmetry is contained by no-invent during media + core POST. */
process_lid_state();
refresh_cdrom_irq_line();
process_source_reset();
process_pending(cycles);
try_execute_queued_command();
if(!s_source_clock)try_execute_queued_command();
/* Release a scheduled data-ready once its delay has elapsed and the guest
* has genuinely finished with the previous INT. */
if (pending_present_due != 0 && psx_cycle_count >= 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);
deliver_xa_data_end();
process_lid_state();
/* 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();
}
@@ -3069,13 +3343,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
@@ -3093,6 +3369,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() &&
@@ -3363,6 +3642,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
@@ -3427,6 +3718,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
@@ -3455,6 +3759,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;
+123 -6
View File
@@ -283,7 +283,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) ---- */
@@ -323,16 +324,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;
@@ -9574,6 +9613,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;
@@ -12835,6 +12875,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;
@@ -13936,6 +14022,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.
@@ -14662,10 +14755,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) {
@@ -14677,6 +14788,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)
+303 -51
View File
@@ -39,6 +39,8 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#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;
@@ -1379,16 +1414,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
@@ -1401,15 +1469,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).
@@ -1419,8 +1500,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(result<INT32_MIN || result>INT32_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<<rd_field(insn))&0xfbe8u))return rt_field(insn);
else if(op==0x12u && (cpu->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
@@ -1451,39 +1600,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<<rd_field(insn))&0xfbe8u);
const int is_ld = (((in_bios_rom || in_bios_kernel_ram) &&
((ld_op >= 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;
@@ -1513,7 +1666,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
@@ -1521,8 +1675,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
@@ -1636,7 +1798,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
@@ -1659,7 +1821,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
@@ -1799,17 +1961,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_result<INT32_MIN || signed_result>INT32_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_result<INT32_MIN || signed_result>INT32_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
@@ -1864,7 +2032,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;
@@ -1874,7 +2042,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
@@ -1929,28 +2097,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;
@@ -1974,15 +2142,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_result<INT32_MIN || signed_result>INT32_MAX)
return interp_arithmetic_overflow(cpu,pc,insn);
if (psx_ws_angle_site(pc, insn, &widened))
cpu->gpr[rt] = widened;
else
@@ -2497,6 +2668,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
@@ -2504,6 +2678,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;
@@ -2523,6 +2701,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))
@@ -2565,10 +2747,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;
@@ -2580,6 +2770,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;
@@ -2591,7 +2782,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;
@@ -2600,10 +2793,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); }
@@ -2629,11 +2836,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;
@@ -2644,6 +2851,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;
@@ -2672,7 +2880,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;
@@ -2720,6 +2934,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. */
@@ -2737,12 +2955,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++;
@@ -3041,7 +3284,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
@@ -3218,6 +3462,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) &&
+583 -10
View File
@@ -13,6 +13,7 @@
*/
#include "dma.h"
#include "source_gpu_runtime.h"
#include "cdrom.h"
#include "crash_trace.h"
#include "dirty_ram_interp.h"
@@ -25,6 +26,7 @@
#include "spu.h"
#include "audio_trace.h"
#include "event_ring.h"
#include "psx_cycles.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -68,6 +70,47 @@ static DMAGpuOtStats gpu_ot_stats;
static uint64_t gpu_ot_start_cycle;
static uint32_t gpu_ot_polls_this_walk;
/* 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
@@ -339,6 +382,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();
}
@@ -380,7 +424,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);
}
@@ -444,6 +491,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) {
@@ -754,6 +806,185 @@ static void start_async_gpu_linked_list(void) {
gpu_ot_polls_this_walk = 0;
}
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()<gpu_upload_source.remaining) {
fprintf(stderr,"[dma-model] source GPU upload changed during transfer\n");exit(2);
}
if(!gpu_upload_source.in_block) {
/* Request feedback is sampled at block boundaries, including
* when the packet header is still queued behind a draw. */
if(queued && to_gpu && gpu_dma_source_ll_ready()!=1)break;
gpu_upload_source.in_block=gpu_upload_source.block_size;
gpu_upload_source.address=channels[2].madr&0xfffffcu;
channels[2].bcr-=1u<<16;
gpu_upload_source.budget-=7;
}
if(gpu_upload_source.address&0x800000u) {
fprintf(stderr,"[dma-model] unsupported source GPU request payload address\n");exit(2);
}
uint32_t at=gpu_upload_source.address&0x1ffffcu;
if(to_gpu) {
uint32_t value=psx_read_word(at);
gpu_set_gp0_source(at);gpu_write_gp0(value);
} else psx_write_word(at,gpu_read_gpuread());
gpu_upload_source.address=(gpu_upload_source.address+((channels[2].chcr&2u)?-4u:4u))&0xffffffu;
gpu_upload_source.in_block--;gpu_upload_source.remaining--;
gpu_upload_source.budget--;
if(!gpu_upload_source.in_block)channels[2].madr=gpu_upload_source.address;
if(!gpu_upload_source.remaining)complete_transfer(2);
}
if(gpu_upload_source.budget>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,7 +1367,11 @@ static void try_execute(int ch) {
start_async_mdec_transfer(1);
break;
case 2:
if ((channels[2].chcr & 1u) != 0u &&
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 if ((channels[2].chcr & 1u) != 0u &&
((channels[2].chcr >> 9) & 3u) == 2u) {
start_async_gpu_linked_list();
} else {
@@ -971,8 +1383,9 @@ static void try_execute(int ch) {
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();
@@ -1010,9 +1423,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(d<best)best=d;
}
return best;
}
uint32_t dma_cycles_to_irq(uint32_t i_mask) {
if (!(i_mask & (1u << 3))) return 0xFFFFFFFFu; /* IRQ_DMA masked */
uint32_t best = 0xFFFFFFFFu;
/* Source GPU transfers complete during periodic DMA service, without a
* delayed_complete entry. A compiled block must stop before that service
* can raise its completion IRQ. This is a conservative bound, not a new
* transfer duration: readiness can postpone completion beyond the event. */
uint32_t best = source_gpu_dma_irq_bound();
if(spu_source.remaining) {
uint32_t d=spu_source.next_cycle>psx_cycle_count?
(uint32_t)(spu_source.next_cycle-psx_cycle_count):1u;
if(d<best)best=d;
}
const DMAAsyncChannel *async_ch[3] = { &mdec_async[0], &mdec_async[1], &cdrom_async };
for (int i = 0; i < 3; i++) {
const DMAAsyncChannel *a = async_ch[i];
@@ -1035,6 +1470,29 @@ uint32_t dma_cycles_to_irq(uint32_t i_mask) {
uint32_t dma_cycles_to_internal_event(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) {
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(d<best)best=d;
}
if(cd_source_model && cdrom_async.active) {
uint32_t d=cd_source.next_cycle>psx_cycle_count?
(uint32_t)(cd_source.next_cycle-psx_cycle_count):1u;
if(d<best)best=d;
}
if(spu_source.remaining) {
uint32_t d=spu_source.next_cycle>psx_cycle_count?
(uint32_t)(spu_source.next_cycle-psx_cycle_count):1u;
if(d<best)best=d;
}
/* Async MDEC channels move one word whenever their cycle accumulator
* reaches the channel cost. Completion-only scheduling batches many RAM
@@ -1059,7 +1517,7 @@ uint32_t dma_cycles_to_internal_event(void) {
/* CD-ROM DMA writes guest RAM incrementally. Expose each word on time;
* waiting only for the channel-complete IRQ can delay hundreds of writes. */
if (cdrom_async.active && cdrom_async.remaining_words != 0 &&
if (!cd_source_model && cdrom_async.active && cdrom_async.remaining_words != 0 &&
((channels[3].chcr >> 24) & 1u) && channel_enabled(3)) {
if ((channels[3].chcr & 1u) != 0) {
return 1u; /* unsupported RAM->CD direction cancels next tick */
@@ -1088,7 +1546,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<best)best=d;
}
const int async_num[3] = { 0, 1, 3 };
const DMAAsyncChannel *async_ch[3] = {
&mdec_async[0], &mdec_async[1], &cdrom_async
@@ -1116,7 +1579,12 @@ uint32_t dma_cycles_to_deliverable_irq(uint32_t i_mask) {
void dma_advance(uint32_t cycles) {
if (cycles == 0) return;
g_dma_exec_depth++; /* async to-RAM DMA writes below run through psx_write_word */
advance_source_otc();
advance_source_cdrom();
advance_source_gpu();
advance_source_gpu_ll();
advance_mdec_channel(0, cycles);
advance_source_spu();
advance_mdec_channel(1, cycles);
if (gpu_linked_list.active && ((channels[2].chcr >> 24) & 1u) &&
channel_enabled(2)) {
@@ -1127,7 +1595,7 @@ void dma_advance(uint32_t cycles) {
dma_gpu_ll_advance(&gpu_linked_list, cycles, &gpu_ll_ops, NULL);
}
DMAAsyncChannel *a = &cdrom_async;
if (dma_cdrom_transfer_active()) {
if (!cd_source_model && dma_cdrom_transfer_active()) {
uint32_t chcr = channels[3].chcr;
uint32_t direction = chcr & 1u;
uint32_t step = (chcr >> 1) & 1u;
@@ -1188,12 +1656,39 @@ 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(&gpu_linked_list, 0, sizeof(gpu_linked_list));
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();
@@ -1244,6 +1739,55 @@ bad:
}
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);
@@ -1269,6 +1813,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;
@@ -1423,6 +1970,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++) {
@@ -1441,6 +2004,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;
int gpu_ll_was_active = gpu_linked_list.active != 0;
if (len != DMA_SNAP_WIRE_BYTES) return 0;
+9 -3
View File
@@ -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;
}
+161 -7
View File
@@ -14,6 +14,8 @@
#include "gpu_gl_renderer.h"
#include "mod_texture_banks.h"
#include "display_scanout.h"
#include "source_gpu_runtime.h"
#include "interrupts.h"
#include "pgxp.h"
#include "mod_memory.h"
#include "gpu_primitive_reject.h"
@@ -41,6 +43,8 @@
#include <stdlib.h>
#include <string.h>
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);
@@ -2438,12 +2442,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
@@ -2474,6 +2483,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;
@@ -2821,6 +2838,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;
@@ -2888,6 +2906,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) ---- */
@@ -2983,6 +3003,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;
}
@@ -3003,12 +3032,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;
}
}
@@ -3021,7 +3055,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;
}
@@ -3375,6 +3410,27 @@ int gpu_display_is_depth24(void) {
return (int)(display_depth & 1u);
}
/* Video standard as the hardware defines it: GP1(08h) bit 3 (GPUSTAT.20).
* 0 = NTSC (60 Hz VBlank, 263 lines), 1 = PAL (50 Hz VBlank, 314 lines).
* The EU BIOS shell and PAL titles set it themselves; a console powers up
* NTSC-timed until the first GP1(08h). Consumers: VBlank/HBlank periods in
* interrupts.c/timers.c and the wall-clock pacer in main.cpp (T32, MGS PAL). */
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;
@@ -4765,6 +4821,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) {
@@ -4783,7 +4843,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
@@ -5612,7 +5672,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) {
@@ -5691,6 +5750,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 &&
@@ -5782,6 +5842,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). */
@@ -5809,7 +5870,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;
@@ -5829,6 +5978,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;
@@ -5954,10 +6104,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;
+11
View File
@@ -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(); }
+182 -1
View File
@@ -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 <string.h>
#include <stdlib.h>
#include <math.h>
@@ -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<count;i++)
source_texture_cache.palette[i]=g_vram[((clut>>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;x<end;x++) {
uint16_t pixel=0,texel=0;
if(c->texture) {
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]<x[0]?2:0;
SourceTriangleColors c={0};c.target=rt_native();c.core_x=x[core];c.core_y=y[core];
c.dither=(shaded || texture)&&dither;c.texture=texture;
if(texture) {
c.mode=(texture->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;row<height;row++) {
unsigned y=(y0+row)&511u;
if(block->interlace && (y&1u)==block->skip_field)continue;
for(unsigned col=0;col<width;col++)g_vram[y*1024u+((x0+col)&1023u)]=color;
if(width)gpu_vram_dirty_mark_row(y);
}
return 1;
}
if(opcode==0x80) {
unsigned sx=words[1]&1023u,sy=(words[1]>>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;row<height;row++) {
unsigned source_row=((sy+row)&511u)*1024u,dest_y=(dy+row)&511u;
for(unsigned first=0;first<width;first+=128) {
unsigned count=width-first;if(count>128)count=128;
for(unsigned i=0;i<count;i++)row_chunk[i]=g_vram[source_row+((sx+first+i)&1023u)];
for(unsigned i=0;i<count;i++) {
unsigned address=dest_y*1024u+((dx+first+i)&1023u);
if(g_mask_check_bit && (g_vram[address]&0x8000u))continue;
g_vram[address]=row_chunk[i]|(g_mask_set_bit?0x8000u:0);
}
}
gpu_vram_dirty_mark_row(dest_y);
}
return 1;
}
if(opcode!=0x60 && opcode!=0x62 && opcode!=0x64 && opcode!=0x65)return 0;
int textured=!!(opcode&4);unsigned dimensions=words[textured?3:2];
int left=block->x,top=block->y,right=left+(int)(dimensions&1023u),bottom=top+(int)((dimensions>>16)&511u);
SourceGPUTexture texture={0};SourceTriangleColors c={0};
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(left<block->clip_left)left=block->clip_left;if(top<block->clip_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;row<bottom;row++) {
if(block->interlace && ((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) {
+376
View File
@@ -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 <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <io.h>
#else
#include <unistd.h>
#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(&sectors);
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 = &sectors[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(&registers);
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 = &registers[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<h; ++y) for (unsigned x=0; x<w; ++x) {
uint8_t *p = rgb + ((size_t)y*w+x)*3;
gpu_display_pixel_rgb(&di, x, y, p, p+1, p+2);
}
snprintf(name, sizeof(name), "frame-%06u.png", (unsigned)completed);
FILE *png = open_output(name);
int ok = png_write_rgb(png, rgb, w, h);
free(rgb);
if (fclose(png) || !ok) fail("screenshot write");
if (fprintf(log_file,
"{\"frame\":%u,\"runtime_frame\":%llu,\"boundary\":\"before_next_input\","
"\"ram_bytes\":%u,\"ram_sha256\":\"%s\",\"applied_words_sha256\":\"%s\","
"\"supplied_words_sha256\":\"%s\",\"sio_samples\":%u,"
"\"input_frames\":%u,\"neutral_tail_ticks\":%u,"
"\"width\":%u,\"height\":%u,\"display_disabled\":%s}\n",
(unsigned)completed, (unsigned long long)runtime_frame,
(unsigned)PSX_MAIN_RAM_BYTES, ram_hash, applied_hash, supplied_hash,
(unsigned)delivered_inputs,
(unsigned)(completed < total_inputs ? completed : total_inputs),
(unsigned)(completed > 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);
}
}
+243 -46
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -178,11 +184,76 @@ void psx_irq_raise(uint32_t bit, uint32_t detail)
/* Dispatch counter for vblank scheduling. */
#define VBLANK_INTERVAL 50000 /* legacy: dispatch-count fallback (unused for VBlank gating now) */
#define VBLANK_DEFER_STALE_CYCLES (vblank_cycles * 10ull)
uint32_t vblank_cycles = 564480u; /* 33.8688 MHz / 60 Hz — real PSX NTSC VBlank period */
uint32_t vblank_cycles = 564480u;
/* VBlank period in guest cycles follows the GPU video standard (GP1(08h)
* bit 3), as on hardware: NTSC 33.8688 MHz / 60 Hz = 564,480; PAL 33.8688 MHz
* / 50 Hz = 677,376. A PAL title ticked at the NTSC period sees 60 VBlanks per
* guest second (1.2x logic speed). Before this the constant was NTSC-only
* (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 vblank_cycles;
}
#define VBLANK_CYCLES vblank_period_cycles()
#define VBLANK_DEFER_STALE_CYCLES ((uint64_t)VBLANK_CYCLES * 10ull)
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;
@@ -192,6 +263,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;
@@ -402,11 +474,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;
@@ -427,13 +499,6 @@ uint32_t psx_spu_sample_event_cycles_to_next(void) {
g_spu_sample_pump_null_rejects++;
return UINT32_MAX;
}
/* Gate on SPUCNT alone — this runs on every deadline recompute, and the
* full SpuGlobalState snapshot here dominated the emu thread under an
* MMIO-polling guest loop (Capcom FMV, gdb-sampled 2026-09-01). */
if ((spu_ctrl_read() & 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.
@@ -461,8 +526,7 @@ void psx_spu_sample_event_service(void) {
if ((psx_get_cycle_count() % 768u) != 0)
g_spu_sample_deferred_mismatches++;
}
if ((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();
}
@@ -472,7 +536,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,
@@ -493,16 +563,27 @@ static void fire_vblank_edge(void) {
void interrupts_service_scheduled_events(void) {
note_sio_progress_cycle();
if (in_exception) return;
while (cycles_since_vblank >= vblank_cycles) {
/* 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();
}
}
uint32_t interrupts_cycles_to_vblank(void) {
if (cycles_since_vblank >= vblank_cycles) return 0;
return vblank_cycles - cycles_since_vblank;
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;
}
uint32_t interrupts_get_cycles_since_vblank(void) {
@@ -510,11 +591,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();
}
@@ -831,6 +931,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;
@@ -1023,8 +1156,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;
@@ -1065,6 +1197,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; }
@@ -1072,8 +1209,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;
}
@@ -1089,6 +1225,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;
@@ -1465,7 +1607,7 @@ irq_deliver_eval:
* 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);
@@ -1498,6 +1640,7 @@ irq_deliver_eval:
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
@@ -1507,6 +1650,42 @@ irq_deliver_eval:
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++;
@@ -1555,6 +1734,17 @@ irq_deliver_eval:
* 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. */
@@ -1700,25 +1890,11 @@ irq_deliver_eval:
* (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
@@ -2009,7 +2185,13 @@ irq_deliver_eval:
* 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 {
@@ -2171,6 +2353,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);
+27
View File
@@ -19,6 +19,7 @@
#include "psx_bios_known_images.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"
@@ -6786,6 +6787,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 —
@@ -14839,6 +14843,15 @@ session_reboot:
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
@@ -14986,8 +14999,22 @@ session_reboot:
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 */
+142 -46
View File
@@ -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 "pst_wire.h"
@@ -25,6 +26,7 @@
#include "dirty_ram_interp.h"
#include "guest_tty.h"
#include "psx_cycles.h"
#include "psx_icache.h"
#include "starvation_ring.h"
#include <stdint.h>
#include <stdio.h>
@@ -154,9 +156,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);
}
@@ -1152,6 +1169,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 */
@@ -1167,8 +1192,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);
@@ -1290,14 +1315,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 */
@@ -1394,6 +1422,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];
@@ -1406,7 +1439,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) {
@@ -1677,6 +1710,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 (docs/ENHANCEMENTS.md G1). */
@@ -1685,25 +1723,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:
@@ -1832,7 +1861,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;
@@ -1879,7 +1910,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; }
@@ -1951,7 +1985,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;
@@ -1996,7 +2032,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
@@ -2059,8 +2097,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;
@@ -2068,16 +2106,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
@@ -2092,6 +2143,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). */
@@ -2105,23 +2164,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
}
@@ -2153,33 +2213,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
@@ -2192,7 +2283,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
@@ -2214,7 +2307,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; }
+4
View File
@@ -2354,6 +2354,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);
@@ -2366,6 +2368,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;
+9
View File
@@ -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
+27
View File
@@ -11,6 +11,19 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
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);
}
+66 -8
View File
@@ -502,6 +502,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;
@@ -775,6 +780,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;
@@ -1256,7 +1276,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);
@@ -2149,7 +2169,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) {
@@ -2178,6 +2203,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;
@@ -2209,7 +2236,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;
}
}
@@ -2270,6 +2297,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;
@@ -2446,7 +2475,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);
@@ -2486,7 +2519,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;
}
}
@@ -2506,13 +2540,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;
}
@@ -2531,7 +2569,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;
}
}
@@ -2570,6 +2609,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) {
@@ -2577,16 +2622,24 @@ 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();
} else if (next_event == 1) {
sio_pending_ack = 0;
sio_fire_ack_irq();
} else if (next_event == 2) {
sio_stat &= ~SIO_STAT_ACK;
} 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) {
@@ -2915,6 +2968,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);
@@ -2924,6 +2980,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);
}
@@ -2931,6 +2988,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));
+128
View File
@@ -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 <stdio.h>
#include <stdlib.h>
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(cycle<draw_raster.cycle || cycle-draw_raster.cycle>UINT32_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;
}
+241 -14
View File
@@ -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 int16_t voice_next_sample(int idx) {
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];
if (!v->active) return 0;
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) {
/* 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) {
@@ -1726,9 +1916,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) {
@@ -1758,6 +1952,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) {
@@ -1784,6 +1991,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; }
@@ -1814,7 +2040,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);
@@ -1823,6 +2050,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;
}
+121
View File
@@ -15,6 +15,10 @@
*/
#include "timers.h"
#include "timer1_source_clock.h"
#include "timer2_source_clock.h"
#include <stdlib.h>
#include <stdio.h>
#include "event_ring.h"
#include <string.h>
@@ -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(cycle<source_timer1_cycle || cycle-source_timer1_cycle>UINT32_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);
@@ -238,6 +328,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(next<best)best=next;
continue; /* source periodic updates are independent of I_MASK */
}
if (!(i_mask & (1u << timer_irq[t]))) continue; /* IRQ masked: not deliverable */
const Timer* tm = &timers[t];
int want_target = (tm->mode & MODE_IRQ_TARGET) != 0;
@@ -273,6 +369,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: {
@@ -311,6 +410,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:
+34
View File
@@ -16,6 +16,7 @@
#include "psx_fiber.h" /* cross-platform fibers (Win32 fibers / POSIX ucontext) */
#include "psx_scheduler.h" /* deterministic TCB scheduler carve-out (scaffolding) */
#include "parity_trace.h" /* general two-process control-flow parity ring */
#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) {
@@ -920,6 +921,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:
@@ -938,6 +954,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) */
+40
View File
@@ -0,0 +1,40 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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<nf;i++)printf("%s%u",i?",":"",fetches[i]);puts("]}");return 0;
}
+50
View File
@@ -0,0 +1,50 @@
#include <stdio.h>
#include <string.h>
#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;i<nf;i++)printf("%s%u",i?",":"",fetches[i]);puts("]}");return 0;
}
+98
View File
@@ -0,0 +1,98 @@
/* Controller-level regression. Synthetic sectors only; no BIOS/game data.
* Include the production unit to seed the laser position without reading an
* entire synthetic disc. All commands and advancement use its real MMIO API.
*/
#include "../src/cdrom.c"
uint64_t psx_cycle_count, s_frame_count;
uint32_t i_stat, g_debug_current_func_addr, g_debug_last_store_pc;
uint32_t debug_guest_ra(void) { return 0; }
static int reads, failures, irq_raises;
void psx_irq_raise(uint32_t a, uint32_t b) { (void)b; i_stat |= 1u << a; irq_raises++; }
void event_ring_record(uint16_t a, uint8_t b) { (void)a; (void)b; }
void event_ring_record_aux(uint16_t a, uint8_t b, uint32_t c) { (void)a; (void)b; (void)c; }
void audio_trace_event(uint16_t a, uint32_t b, uint32_t c) { (void)a; (void)b; (void)c; }
uint32_t interrupts_get_cycles_since_vblank(void) { return 0; }
int dma_cdrom_transfer_active(void) { return 0; }
void spu_cd_audio_push(const int16_t *a, int b) { (void)a; (void)b; }
void spu_cd_audio_reset(void) {}
int psx_netplay_active(void) { return 0; }
int psx_netplay_cd_bisect_active(void) { return 0; }
int psx_netplay_is_resimulating(void) { return 0; }
uint32_t psx_netplay_sim_tick(void) { return 0; }
void *iso_open(const char *p) { (void)p; return (void *)1; }
void iso_close(void *p) { (void)p; }
int iso_read_sector(void *p, uint32_t lba, uint8_t *b, int n) {
(void)p; memset(b, (int)(lba & 255u), (size_t)n); reads++; return 1;
}
int iso_read_raw_sector(void *p, uint32_t lba, uint8_t *b, int n) {
(void)p; (void)lba; (void)b; (void)n; return 0;
}
int iso_read_subq(void *p, uint32_t lba, uint8_t *b, int n, int *v) {
(void)p; (void)lba; (void)b; (void)n; *v=0; return 0;
}
int iso_has_subq_replacements(void *p) { (void)p; return 0; }
uint32_t iso_sector_count(void *p) { (void)p; return 333000; }
int iso_track_count(void *p) { (void)p; return 1; }
uint32_t iso_track_start_lba(void *p, int t) { (void)p; (void)t; return 0; }
uint32_t iso_track_pregap_lba(void *p, int t) { (void)p; (void)t; return 0; }
int iso_track_is_audio(void *p, int t) { (void)p; (void)t; return 0; }
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr,"FAIL: %s\n",m); failures++; } } while(0)
static void ack(void) {
cdrom_write(0x1f801800,1); cdrom_write(0x1f801803,0x1f);
cdrom_write(0x1f801800,0);
}
static void command(uint8_t c) { cdrom_write(0x1f801800,0); cdrom_write(0x1f801801,c); }
static void advance(int n) { psx_cycle_count += (uint32_t)n; cdrom_advance((uint32_t)n); }
static void set_model(const char *key,const char *value) {
#ifdef _WIN32
_putenv_s(key,value);
#else
setenv(key,value,1);
#endif
}
static void test_version(int source) {
static const uint8_t expected[2][4]={{0x94,0x09,0x19,0xc0},{0x97,0x01,0x10,0xc2}};
set_model("PSX_CD_FIRMWARE_MODEL",source?"octoshock-2.2.2":"");
cdrom_init("synthetic");
cdrom_write(0x1f801800,0);cdrom_write(0x1f801802,0x20);command(0x19);
CHECK(irq_flag==CDIRQ_ACK&&response_count==4,"version owns one four-byte ACK");
for(int i=0;i<4;i++) CHECK(cdrom_read(0x1f801801)==expected[source][i],"documented controller version byte");
ack();
}
static void test_status(int source,int present) {
set_model("PSX_CD_COLD_STATUS_MODEL",source?"octoshock-2.2.2":"");
iso_handle=NULL;cdrom_init(present?"synthetic":NULL);
command(1);
CHECK(cdrom_read(0x1f801801)==(present?(source?0x12:0x02):0x10),"first GetStat reports cold latch before clearing");
CHECK(irq_flag==CDIRQ_ACK,"GetStat ACK");ack();command(1);
CHECK(cdrom_read(0x1f801801)==(present?0x02:0x10),"second GetStat clears only closed present-disc latch");
ack();command(0x1a);
CHECK((cdrom_read(0x1f801801)&0x10)==(present?0:0x10),"GetID sees resulting tray state");
}
int main(void) {
for(int firmware=0;firmware<2;firmware++) for(int cold=0;cold<2;cold++) {
set_model("PSX_CD_COLD_STATUS_MODEL",cold?"octoshock-2.2.2":"");test_version(firmware);
for(int disc=0;disc<2;disc++) test_status(cold,disc);
}
set_model("PSX_CD_FIRMWARE_MODEL","");set_model("PSX_CD_COLD_STATUS_MODEL","");
test_version(0);test_status(0,1);
for(int source=0;source<2;source++) for(int fast=0;fast<2;fast++) {
set_model("PSX_CD_TOC_SEEK_MODEL",source?"octoshock-2.2.2":"");
cdrom_init("synthetic");psx_cycle_count=0;mode_reg=fast?0x80:0;
command(0x1e);
int expected=30000000+(source?(20000+(fast?1237952:2475904)):0);
CHECK(pending.due_cyc==(uint64_t)expected,"TOC includes exact declared cold paused seek lower bound");
CHECK(irq_flag==CDIRQ_ACK&&response_fifo[response_read]==2,"TOC first response remains pre-command ACK");
ack();advance(expected-1);CHECK(irq_flag==0,"TOC cannot complete one cycle early");
advance(1);CHECK(irq_flag==CDIRQ_COMPLETE,"TOC completes at declared deadline");
}
CHECK(source_seek_lower_bound(123,0,0,0,0)==33888800,"stopped motor restarts at zero");
CHECK(source_seek_lower_bound(0,2250,1,1,0)==10395840,"long distance uses settle instead of paused term");
CHECK(source_seek_lower_bound(2250,0,1,1,0)==10395840,"reverse travel uses same distance");
CHECK(source_seek_lower_bound(0,4,1,0,0)==20000,"standby is not paused");
set_model("PSX_CD_TOC_SEEK_MODEL","");
if(failures){fprintf(stderr,"FAILED (%d)\n",failures);return 1;}
puts("CD boot model: ALL PASS");return 0;
}
+116
View File
@@ -0,0 +1,116 @@
/* Controller-level regression. Synthetic sectors only; no BIOS/game data.
* Include the production unit to seed the laser position without reading an
* entire synthetic disc. All commands and advancement use its real MMIO API.
*/
#include "../src/cdrom.c"
uint64_t psx_cycle_count, s_frame_count;
uint32_t i_stat, g_debug_current_func_addr, g_debug_last_store_pc;
uint32_t debug_guest_ra(void) { return 0; }
static int reads, failures, irq_raises;
void psx_irq_raise(uint32_t a, uint32_t b) { (void)b; i_stat |= 1u << a; irq_raises++; }
void event_ring_record(uint16_t a, uint8_t b) { (void)a; (void)b; }
void event_ring_record_aux(uint16_t a, uint8_t b, uint32_t c) { (void)a; (void)b; (void)c; }
void audio_trace_event(uint16_t a, uint32_t b, uint32_t c) { (void)a; (void)b; (void)c; }
uint32_t interrupts_get_cycles_since_vblank(void) { return 0; }
int dma_cdrom_transfer_active(void) { return 0; }
void spu_cd_audio_push(const int16_t *a, int b) { (void)a; (void)b; }
void spu_cd_audio_reset(void) {}
int psx_netplay_active(void) { return 0; }
int psx_netplay_cd_bisect_active(void) { return 0; }
int psx_netplay_is_resimulating(void) { return 0; }
uint32_t psx_netplay_sim_tick(void) { return 0; }
void *iso_open(const char *p) { (void)p; return (void *)1; }
void iso_close(void *p) { (void)p; }
int iso_read_sector(void *p, uint32_t lba, uint8_t *b, int n) {
(void)p; memset(b, (int)(lba & 255u), (size_t)n); reads++; return 1;
}
int iso_read_raw_sector(void *p, uint32_t lba, uint8_t *b, int n) {
(void)p; (void)lba; (void)b; (void)n; return 0;
}
int iso_read_subq(void *p, uint32_t lba, uint8_t *b, int n, int *v) {
(void)p; (void)lba; (void)b; (void)n; *v=0; return 0;
}
int iso_has_subq_replacements(void *p) { (void)p; return 0; }
uint32_t iso_sector_count(void *p) { (void)p; return 333000; }
int iso_track_count(void *p) { (void)p; return 1; }
uint32_t iso_track_start_lba(void *p, int t) { (void)p; (void)t; return 0; }
uint32_t iso_track_pregap_lba(void *p, int t) { (void)p; (void)t; return 0; }
int iso_track_is_audio(void *p, int t) { (void)p; (void)t; return 0; }
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr,"FAIL: %s\n",m); failures++; } } while(0)
static void ack(void) {
cdrom_write(0x1f801800,1); cdrom_write(0x1f801803,0x1f);
cdrom_write(0x1f801800,0);
}
static void command(uint8_t c) { cdrom_write(0x1f801800,0); cdrom_write(0x1f801801,c); }
static void advance(int n) { psx_cycle_count += (uint32_t)n; cdrom_advance((uint32_t)n); }
static void target(int lba) {
int n=lba+150, m=n/4500, s=n/75%60, f=n%75;
cdrom_write(0x1f801800,0);
cdrom_write(0x1f801802,bin_to_bcd(m));
cdrom_write(0x1f801802,bin_to_bcd(s));
cdrom_write(0x1f801802,bin_to_bcd(f));
command(2); ack();
}
static void setup(int origin) {
psx_cycle_count=0; cdrom_init("synthetic"); reads=0;
last_sector_lba=origin;
int n=origin+150; read_min=n/4500; read_sec=n/75%60; read_sect=n%75;
mode_reg=0x80; setloc_pending=0;
}
/* PSX-SPX plain seek stops future reads and finishes paused. Octoshock
* Command_SeekL/P also preserves the already admitted DMA buffer. Do not
* conflate that guest-owned payload with a queued, unannounced INT1. */
static void exercise_explicit(uint8_t read, uint8_t seek, int delivered) {
setup(100); target(100); command(read); ack();
if(delivered) { advance(read_delay); ack(); }
target(1000);
/* Authored pending producer state, independent of guest response FIFO. */
pending_dataready=1; pending_dataready_stat=CDSTAT_READ|CDSTAT_MOTOR;
pending_present_due=psx_cycle_count+500; s_cd_timing_pending_seq=123;
RB_.size=2048;RB_.pos=4;RB_.data[4]=0x73;request_reg=CDROM_REQUEST_BFRD;
int before_reads=reads; uint8_t before_status=stat_reg;
int delay=seek_complete_delay_cycles();
command(seek);
CHECK(irq_flag==CDIRQ_ACK&&response_count==1&&response_fifo[response_read]==before_status,
"plain seek ACK reports pre-transition drive state");
CHECK(reading==0&&read_delay==0,"plain seek cancels pending or active read producer");
CHECK(!pending_dataready&&pending_present_due==0,"plain seek cancels unannounced old INT1");
CHECK((stat_reg&(CDSTAT_SEEK|CDSTAT_READ|CDSTAT_PLAY))==CDSTAT_SEEK,
"plain seek owns only SEEK status");
CHECK(pending.due_cyc==psx_cycle_count+(uint64_t)delay,"explicit timing is unchanged");
CHECK(RB_.size==2048&&RB_.pos==4&&request_reg==CDROM_REQUEST_BFRD,
"admitted guest FIFO ownership survives command");
CHECK(cdrom_read(0x1f801802)==0x73&&RB_.pos==5,"guest can finish an admitted byte");
ack(); advance(delay-1);
CHECK(irq_flag==0&&reads==before_reads,"no old sector or completion before deadline");
advance(1);
CHECK(irq_flag==CDIRQ_COMPLETE,"plain seek emits completion at existing deadline");
CHECK(response_fifo[response_read]==CDSTAT_MOTOR&&stat_reg==CDSTAT_MOTOR,
"plain seek completes paused with no READ/PLAY/SEEK bit");
CHECK(reads==before_reads&&!reading,"plain seek never resumes old stream");
CHECK(msf_to_lba(read_min,read_sec,read_sect)==1000,"seek retargets future stream position");
ack();advance(2000000);
CHECK(irq_flag==0&&reads==before_reads,"paused seek has no late INT1");
command(read);
CHECK(response_fifo[response_read]==CDSTAT_MOTOR,"subsequent read ACK sees paused state");
CHECK(read_delay==initial_read_delay_cycles(),"completed seek has no extra implicit seek");
ack();advance(read_delay);
CHECK(last_sector_lba==1000&&reads==before_reads+1,"explicit read resumes at seek target");
printf("read=%02x seek=%02x delivered=%d checked\n",read,seek,delivered);
}
int main(void) {
for(int read=0;read<2;read++) for(int seek=0;seek<2;seek++)
for(int delivered=0;delivered<2;delivered++)
exercise_explicit(read?0x1b:6,seek?0x16:0x15,delivered);
for(int seek=0;seek<2;seek++) {
setup(100);target(1000);cdda_playing=1;stat_reg=CDSTAT_MOTOR|CDSTAT_PLAY;
command(seek?0x16:0x15);
CHECK(response_fifo[response_read]==(CDSTAT_MOTOR|CDSTAT_PLAY),"audio seek ACK keeps prior PLAY");
CHECK(!cdda_playing&&!(stat_reg&CDSTAT_PLAY),"plain seek cancels audio producer");
}
if(failures){fprintf(stderr,"FAILED (%d)\n",failures);return 1;}
puts("CD explicit seek: ALL PASS");return 0;
}
+153
View File
@@ -0,0 +1,153 @@
/* Controller-level regression. Synthetic sectors only; no BIOS/game data.
* Include the production unit to seed the laser position without reading an
* entire synthetic disc. All commands and advancement use its real MMIO API.
*/
#include "../src/cdrom.c"
uint64_t psx_cycle_count, s_frame_count;
uint32_t i_stat, g_debug_current_func_addr, g_debug_last_store_pc;
uint32_t debug_guest_ra(void) { return 0; }
static int reads, failures, irq_raises;
void psx_irq_raise(uint32_t a, uint32_t b) { (void)b; i_stat |= 1u << a; irq_raises++; }
void event_ring_record(uint16_t a, uint8_t b) { (void)a; (void)b; }
void event_ring_record_aux(uint16_t a, uint8_t b, uint32_t c) { (void)a; (void)b; (void)c; }
void audio_trace_event(uint16_t a, uint32_t b, uint32_t c) { (void)a; (void)b; (void)c; }
uint32_t interrupts_get_cycles_since_vblank(void) { return 0; }
int dma_cdrom_transfer_active(void) { return 0; }
void spu_cd_audio_push(const int16_t *a, int b) { (void)a; (void)b; }
void spu_cd_audio_reset(void) {}
int psx_netplay_active(void) { return 0; }
int psx_netplay_cd_bisect_active(void) { return 0; }
int psx_netplay_is_resimulating(void) { return 0; }
uint32_t psx_netplay_sim_tick(void) { return 0; }
void *iso_open(const char *p) { (void)p; return (void *)1; }
void iso_close(void *p) { (void)p; }
int iso_read_sector(void *p, uint32_t lba, uint8_t *b, int n) {
(void)p; memset(b, (int)(lba & 255u), (size_t)n); reads++; return 1;
}
int iso_read_raw_sector(void *p, uint32_t lba, uint8_t *b, int n) {
(void)p; (void)lba; (void)b; (void)n; return 0;
}
int iso_read_subq(void *p, uint32_t lba, uint8_t *b, int n, int *v) {
(void)p; (void)lba; (void)b; (void)n; *v=0; return 0;
}
int iso_has_subq_replacements(void *p) { (void)p; return 0; }
uint32_t iso_sector_count(void *p) { (void)p; return 333000; }
int iso_track_count(void *p) { (void)p; return 1; }
uint32_t iso_track_start_lba(void *p, int t) { (void)p; (void)t; return 0; }
uint32_t iso_track_pregap_lba(void *p, int t) { (void)p; (void)t; return 0; }
int iso_track_is_audio(void *p, int t) { (void)p; (void)t; return 0; }
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr,"FAIL: %s\n",m); failures++; } } while(0)
static void ack(void) {
cdrom_write(0x1f801800,1); cdrom_write(0x1f801803,0x1f);
cdrom_write(0x1f801800,0);
}
static void command(uint8_t c) { cdrom_write(0x1f801800,0); cdrom_write(0x1f801801,c); }
static void advance(int n) { psx_cycle_count += (uint32_t)n; cdrom_advance((uint32_t)n); }
static void target(int lba) {
int n=lba+150, m=n/4500, s=n/75%60, f=n%75;
cdrom_write(0x1f801800,0);
cdrom_write(0x1f801802,bin_to_bcd(m));
cdrom_write(0x1f801802,bin_to_bcd(s));
cdrom_write(0x1f801802,bin_to_bcd(f));
command(2); ack();
}
static void setup(int origin) {
psx_cycle_count=0; cdrom_init("synthetic"); reads=0;
last_sector_lba=origin;
int n=origin+150; read_min=n/4500; read_sec=n/75%60; read_sect=n%75;
mode_reg=0x80; setloc_pending=0;
}
static int exercise(uint8_t cmd, int origin, int dest, int observe) {
setup(origin); target(dest); command(cmd);
int deadline=read_delay;
printf("cmd=%02x origin=%d target=%d ack=%02x status=%02x first_delay=%d\n",
cmd,origin,dest,response_fifo[response_read],stat_reg,deadline);
CHECK(response_fifo[response_read]==CDSTAT_MOTOR,"Read ACK contains pre-seek state");
CHECK((stat_reg&(CDSTAT_SEEK|CDSTAT_READ|CDSTAT_PLAY))==CDSTAT_SEEK,
"pending implicit seek has only SEEK state");
if(observe) {
ack(); advance(deadline-1); CHECK(reads==0,"no sector before combined seek/read deadline");
advance(1); CHECK(reads==1,"one target sector at deadline");
CHECK(last_sector_lba==dest,"first delivered sector is requested target");
CHECK((stat_reg&(CDSTAT_SEEK|CDSTAT_READ|CDSTAT_PLAY))==CDSTAT_READ,
"first sector changes SEEK to READ");
ack(); advance(225791); CHECK(reads==1,"steady 2x cadence unchanged before deadline");
advance(1); CHECK(reads==2,"steady 2x cadence delivers next sector");
CHECK(last_sector_lba==dest+1,"stream advances consecutively");
}
return deadline;
}
int main(void) {
#ifdef _WIN32
#define SET_MODEL(v) _putenv_s("PSX_CD_READ_START_MODEL", (v))
#else
#define SET_MODEL(v) setenv("PSX_CD_READ_START_MODEL", (v), 1)
#endif
SET_MODEL("");
int near_delay=exercise(0x06,103948,103949,1);
int far_delay=exercise(0x06,103948,251926,1);
int reverse=exercise(0x1b,251926,103948,1);
CHECK(far_delay>near_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;
}
+45
View File
@@ -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;i<sizeof(vectors)/sizeof(vectors[0]);i++) {
uint32_t before=tape.cursor;
CHECK(cd_tape_bounded(&tape,vectors[i][0],&value));
CHECK(value==vectors[i][1] && tape.cursor-before==vectors[i][2]);
}
uint32_t cursor=tape.cursor;
CHECK(cd_tape_load(&tape,argv[1]) && tape.cursor==0);
CHECK(cd_tape_restore(&tape,tape.sha256,tape.count,cursor));
printf("source vectors 22, raw cursor %u\n",cursor);
}
cd_tape_free(&tape);
printf("random tape fixture: %s\n",failures?"FAIL":"PASS");
return failures?1:0;
}
+111
View File
@@ -0,0 +1,111 @@
/* Reuse the authored controller stubs and default timing regression. */
#define main baseline_explicit_timing_main
#include "test_cdrom_source_explicit_timing.c"
#undef main
static const char *tape_path;
static void clock_boot(void) {
set_model("PSX_CD_EXPLICIT_SEEK_MODEL","octoshock-2.2.2");
set_model("PSX_CD_TOC_SEEK_MODEL","octoshock-2.2.2");
set_model("PSX_CD_READ_START_MODEL","octoshock-2.2.2-pipeline");
set_model("PSX_CD_SOURCE_CLOCK_TAPE",tape_path);
psx_cycle_count=0;cdrom_init("synthetic");i_stat=0;reads=irq_raises=0;
}
static void receive_command(void) {
for(int guard=0;queued_cmd.pending && guard<20;guard++) {
CHECK(!irq_flag,"fixture command must be eligible");
uint64_t due=s_source_command_due>s_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;i<size;i++){RB_.data[i]=(uint8_t)(0x10+i);expected|=(uint32_t)RB_.data[i]<<(i*8);}
CHECK(cdrom_dma_read_padded()==expected && RB_.pos==size,"source padded DMA drains available bytes and zeros missing bytes");
CHECK(cdrom_dma_read_padded()==0 && RB_.pos==size,"empty padded DMA remains empty");
RB_.pos=0;request_reg=0;CHECK(cdrom_dma_read_padded()==0 && RB_.pos==0,"native BFRD ownership remains enforced");
request_reg=CDROM_REQUEST_BFRD;
if(size<4)CHECK(cdrom_dma_read()==0 && RB_.pos==0,"ordinary DMA partial-word behavior stays unchanged");
}
clock_boot();
cdrom_write(0x1f801802,0x20);command(0x19);
CHECK(s_source_clock_tape.cursor==3 && s_source_clock_calls==1,"source first command consumes rejection words");
CHECK(s_source_command_due==13452 && !irq_flag,"initial reception period has source jitter");
advance(13451);CHECK(!irq_flag && s_source_command_phase==-1,"no early reception phase");
advance(1);CHECK(s_source_command_phase==0 && s_source_command_due==15267,"one parameter reception phase");
advance(1815);CHECK(s_source_command_phase==1 && s_source_command_due==23767,"source final reception phase");
advance(8499);CHECK(!irq_flag,"no early command response");
advance(1);CHECK(irq_flag==3 && (i_stat&4) && psx_cycle_count==23767,"ACK flag and CPU line at actual command execution");
clock_boot();command(1);receive_command();
CHECK(psx_cycle_count==21952,"zero arguments omit parameter receive phase");
command(1);advance(30000);
CHECK(queued_cmd.pending && s_source_command_phase==-1,"old IRQ blocks the first reception stage");
ack();uint64_t cleared=psx_cycle_count;
advance(1999);CHECK(s_source_command_phase==-1 && !irq_flag,"post-ACK receive interval remains closed");
advance(1);CHECK(s_source_command_phase==1 && !irq_flag,"receive resumes after full 2000 clocks");
advance(8499);CHECK(!irq_flag,"later command execution delay is not skipped by blocked time");
advance(1);CHECK(irq_flag==3 && psx_cycle_count-cleared==10500,"blocked command retains remaining source phase");
clock_boot();cdrom_write(0x1f801802,0);cdrom_write(0x1f801802,2);cdrom_write(0x1f801802,4);command(2);
command(1);receive_command();
CHECK(!setloc_pending && s_source_clock_calls==2 && s_source_clock_tape.cursor==4,"replacement keeps command-write entropy order and cancels old arguments");
CHECK(psx_cycle_count==20918,"replacement reception uses its own source jitter");
clock_boot();cdrom_write(0x1f801802,0x20);command(0x19);advance(13452);
unsigned length=cdrom_snapshot_bytes();uint8_t *wire=malloc(length),*before=malloc(length),*after=malloc(length);
cdrom_snapshot_write(wire);
receive_command();ack();command(1);receive_command();
CHECK(s_source_clock_tape.cursor==4,"second command advances entropy before restore");
CHECK(cdrom_snapshot_read(wire,length) && s_source_clock_tape.cursor==3 && s_source_command_phase==0,"controller snapshot restores cursor and reception stage");
CHECK(s_source_command_due-psx_cycle_count==1815,"restored stage uses remaining delay");
receive_command();ack();command(1);receive_command();
CHECK(s_source_clock_tape.cursor==4,"restored branch consumes the same next word");
unsigned offsets[]={4,36,40,44,48,52,56,60,64};
for(unsigned n=0;n<sizeof(offsets)/sizeof(offsets[0]);n++) {
cdrom_snapshot_write(before);uint8_t saved=wire[length-76+offsets[n]];
memset(wire+length-76+offsets[n],0xff,offsets[n]==4?1:4);
if(offsets[n]==48) { memset(wire+length-76+48,0,4);wire[length-76+48]=2; }
CHECK(!cdrom_snapshot_read(wire,length),"bad tape identity or clock state rejected");
cdrom_snapshot_write(after);CHECK(!memcmp(before,after,length),"invalid clock snapshot rejected before any controller mutation");
/* Restore the valid source image for the next malformed case. */
(void)saved;memcpy(wire,before,length);
}
free(wire);free(before);free(after);
for(int paused=0;paused<=1;paused++) {
clock_boot();mode_reg=0x80;s_source_seek_paused=(uint8_t)paused;
read_min=seek_min=0;read_sec=seek_sec=2;read_sect=seek_sect=4;setloc_pending=0;
command(6);receive_command();
int expected=677376+20000+4996+(paused?1237952:0);
CHECK(read_delay==expected,"fresh ReadN seeks from standby without a pending Setloc, then fills three periods");
CHECK(s_source_clock_tape.cursor==5 && s_source_clock_calls==2,"ReadN consumes command and seek entropy separately");
ack();advance(expected-1);CHECK(!irq_flag && reads==0,"no early first sector");
advance(1);CHECK(irq_flag==1 && reads==1,"first sector appears at the declared source clock deadline");
}
clock_boot();command(1);receive_command();ack();
reading=1;read_cmd=6;read_min=0;read_sec=2;read_sect=4;mode_reg=0x80;read_delay=1000;
s_cd_timing_next_due=psx_cycle_count+1000;
advance(1000);CHECK(!irq_flag && pending_dataready,"sector can arrive while receive interval is closed");
advance(999);CHECK(!irq_flag,"pended sector stays hidden until receive interval ends");
advance(1);CHECK(irq_flag==1,"pended sector is admitted when receive interval reopens");
clock_boot();CHECK(s_source_clock_tape.cursor==0 && s_source_clock_calls==0,"cold boot resets tape cursor and call count");
if(failures){fprintf(stderr,"source clock failures %d\n",failures);return 1;}
puts("CD source clock: ALL PASS");return 0;
}
@@ -0,0 +1,106 @@
/* Controller-level regression. Synthetic sectors only; no BIOS/game data.
* Include the production unit to seed the laser position without reading an
* entire synthetic disc. All commands and advancement use its real MMIO API.
*/
#include "../src/cdrom.c"
uint64_t psx_cycle_count, s_frame_count;
uint32_t i_stat, g_debug_current_func_addr, g_debug_last_store_pc;
uint32_t debug_guest_ra(void) { return 0; }
static int reads, failures, irq_raises;
void psx_irq_raise(uint32_t a, uint32_t b) { (void)b; i_stat |= 1u << a; irq_raises++; }
void event_ring_record(uint16_t a, uint8_t b) { (void)a; (void)b; }
void event_ring_record_aux(uint16_t a, uint8_t b, uint32_t c) { (void)a; (void)b; (void)c; }
void audio_trace_event(uint16_t a, uint32_t b, uint32_t c) { (void)a; (void)b; (void)c; }
uint32_t interrupts_get_cycles_since_vblank(void) { return 0; }
int dma_cdrom_transfer_active(void) { return 0; }
void spu_cd_audio_push(const int16_t *a, int b) { (void)a; (void)b; }
void spu_cd_audio_reset(void) {}
int psx_netplay_active(void) { return 0; }
int psx_netplay_cd_bisect_active(void) { return 0; }
int psx_netplay_is_resimulating(void) { return 0; }
uint32_t psx_netplay_sim_tick(void) { return 0; }
void *iso_open(const char *p) { (void)p; return (void *)1; }
void iso_close(void *p) { (void)p; }
int iso_read_sector(void *p, uint32_t lba, uint8_t *b, int n) {
(void)p; memset(b, (int)(lba & 255u), (size_t)n); reads++; return 1;
}
int iso_read_raw_sector(void *p, uint32_t lba, uint8_t *b, int n) {
(void)p; (void)lba; (void)b; (void)n; return 0;
}
int iso_read_subq(void *p, uint32_t lba, uint8_t *b, int n, int *v) {
(void)p; (void)lba; (void)b; (void)n; *v=0; return 0;
}
int iso_has_subq_replacements(void *p) { (void)p; return 0; }
uint32_t iso_sector_count(void *p) { (void)p; return 333000; }
int iso_track_count(void *p) { (void)p; return 1; }
uint32_t iso_track_start_lba(void *p, int t) { (void)p; (void)t; return 0; }
uint32_t iso_track_pregap_lba(void *p, int t) { (void)p; (void)t; return 0; }
int iso_track_is_audio(void *p, int t) { (void)p; (void)t; return 0; }
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr,"FAIL: %s\n",m); failures++; } } while(0)
static void ack(void) {
cdrom_write(0x1f801800,1); cdrom_write(0x1f801803,0x1f);
cdrom_write(0x1f801800,0);
}
static void command(uint8_t c) { cdrom_write(0x1f801800,0); cdrom_write(0x1f801801,c); }
static void advance(int n) { psx_cycle_count += (uint32_t)n; cdrom_advance((uint32_t)n); }
static void set_model(const char *key,const char *value) {
#ifdef _WIN32
_putenv_s(key,value);
#else
setenv(key,value,1);
#endif
}
static void target(int lba) {
int n=lba+150;
cdrom_write(0x1f801800,0);
cdrom_write(0x1f801802,bin_to_bcd(n/4500));
cdrom_write(0x1f801802,bin_to_bcd(n/75%60));
cdrom_write(0x1f801802,bin_to_bcd(n%75));
command(2);ack();
}
static void finish(void) { ack();advance((int)(pending.due_cyc-psx_cycle_count));ack(); }
static int run_seek(int fast,int logical,int paused) {
cdrom_init("synthetic");psx_cycle_count=0;mode_reg=fast?0x80:0;target(4);
if(!paused) { command(0x16);finish();target(5); }
command(logical?0x15:0x16);
int n=(int)pending.due_cyc-(int)psx_cycle_count;
int expected=20000+(paused?(fast?1237952:2475904):0)+(logical?(fast?225792:451584):0);
CHECK(n==expected,"source paused/standby and SeekL/P deterministic deadline");
CHECK(response_fifo[response_read]==CDSTAT_MOTOR,"seek ACK remains pre-transition motor status");
ack();advance(n-1);CHECK(irq_flag==0,"no early explicit completion");
advance(1);CHECK(irq_flag==CDIRQ_COMPLETE&&stat_reg==CDSTAT_MOTOR,"exact completion paused public status");
CHECK(s_source_seek_paused==0,"plain seek ends in source STANDBY timing state");
return n;
}
int main(void) {
set_model("PSX_CD_EXPLICIT_SEEK_MODEL","");cdrom_init("synthetic");
unsigned default_size=cdrom_snapshot_bytes();
uint8_t *default_a=malloc(default_size),*default_b=malloc(default_size);
cdrom_snapshot_write(default_a);s_source_seek_paused=0;cdrom_snapshot_write(default_b);
CHECK(!memcmp(default_a,default_b,default_size),"default wire excludes inert source-only state");
set_model("PSX_CD_EXPLICIT_SEEK_MODEL","octoshock-2.2.2");
for(int fast=0;fast<2;fast++) for(int logical=0;logical<2;logical++) for(int paused=0;paused<2;paused++)
printf("fast=%d logical=%d paused=%d delay=%d\n",fast,logical,paused,run_seek(fast,logical,paused));
cdrom_init("synthetic");psx_cycle_count=0;target(4);
unsigned size=cdrom_snapshot_bytes();CHECK(size==default_size+1,"profile adds one declared timing byte");
uint8_t *wire=malloc(size);cdrom_snapshot_write(wire);
command(0x15);finish();CHECK(!s_source_seek_paused,"seek changes timing state before restore");
CHECK(cdrom_snapshot_read(wire,size)&&s_source_seek_paused==1,"matching-profile snapshot restores paused timing");
CHECK(source_explicit_seek_cycles(0x15)==2947488,"restored first logical seek retains exact delay");
wire[size-1]=2;uint8_t old_stat=stat_reg;uint8_t old_phase=s_source_seek_paused;
CHECK(!cdrom_snapshot_read(wire,size)&&stat_reg==old_stat&&s_source_seek_paused==old_phase,"invalid phase fails before state mutation");
command(0x15);finish();command(9);finish();target(5);command(0x15);
CHECK(pending.due_cyc-psx_cycle_count==2947488,"Pause restores paused restart cost after standby");
finish();command(8);finish();target(4);command(0x16);
CHECK(pending.due_cyc-psx_cycle_count==33888800,"stopped motor uses source spin-up lower bound");
cdrom_init("synthetic");target(2250);command(0x16);
CHECK(pending.due_cyc-psx_cycle_count==10395840,"long travel uses settle term instead of paused term");
set_model("PSX_CD_EXPLICIT_SEEK_MODEL","");cdrom_init("synthetic");target(4);command(0x15);
CHECK(pending.due_cyc-psx_cycle_count==1806336,"default far explicit timing is unchanged");
free(wire);free(default_a);free(default_b);
if(failures){fprintf(stderr,"FAILED (%d)\n",failures);return 1;}
puts("CD source explicit timing: ALL PASS");return 0;
}
+93
View File
@@ -0,0 +1,93 @@
/* Source-profile 0A reset contract. Expected deadlines/state come from the
* original 2.2.2 Command_Reset/update path and passive 699..708 state capture.
* Only synthetic controller data. No title, BIOS or restored game state. */
#define main source_explicit_baseline_main
#include "test_cdrom_source_explicit_timing.c"
#undef main
static const char *tape_path;
static void clock_boot(void) {
set_model("PSX_CD_EXPLICIT_SEEK_MODEL","octoshock-2.2.2");
set_model("PSX_CD_TOC_SEEK_MODEL","octoshock-2.2.2");
set_model("PSX_CD_READ_START_MODEL","octoshock-2.2.2-pipeline");
set_model("PSX_CD_SOURCE_CLOCK_TAPE",tape_path);
psx_cycle_count=0;cdrom_init("synthetic");i_stat=0;reads=irq_raises=0;
}
static void receive_command(void) {
for(int guard=0;queued_cmd.pending && guard<20;guard++) {
CHECK(!irq_flag,"fixture command must be eligible");
uint64_t due=s_source_command_due>s_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;
}
@@ -0,0 +1,35 @@
#include "psx_cyc.h"
#include <assert.h>
#include <stdio.h>
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;
}
@@ -0,0 +1,38 @@
#include "psx_cyc.h"
#include <assert.h>
#include <stdio.h>
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");
}
+39
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <assert.h>
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;
}
+55
View File
@@ -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 <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}
+91
View File
@@ -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;}
+135
View File
@@ -0,0 +1,135 @@
/* Authored production-controller fixture. No BIOS, disc or retail code. */
#define _POSIX_C_SOURCE 200809L
#include "../src/dma.c"
#include <assert.h>
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<<bit;irqs++;}
void event_ring_record_aux(uint16_t kind,uint8_t src,uint32_t value) {(void)kind;(void)src;(void)value;}
int cdrom_dma_ready(void) {return read_words<available;}
uint32_t cdrom_dma_sector_word_count(void) {return available;}
uint32_t cdrom_dma_read(void) {assert(cdrom_dma_ready());return 0xCA000000u+read_words++;}
uint32_t cdrom_dma_read_padded(void) {return cdrom_dma_ready()?cdrom_dma_read():0;}
void cdrom_debug_snapshot(CDROMDebugState *s) {memset(s,0,sizeof(*s));s->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) {}
@@ -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 <assert.h>
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<<bit;irqs++;}
void event_ring_record_aux(uint16_t kind,uint8_t src,uint32_t value) {(void)kind;(void)src;(void)value;}
int cdrom_dma_ready(void) {return read_words<available;}
uint32_t cdrom_dma_sector_word_count(void) {return available;}
uint32_t cdrom_dma_read(void) {assert(cdrom_dma_ready());return 0xCA000000u+read_words++;}
uint32_t cdrom_dma_read_padded(void) {return cdrom_dma_ready()?cdrom_dma_read():0;}
void cdrom_debug_snapshot(CDROMDebugState *s) {memset(s,0,sizeof(*s));s->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;}
@@ -0,0 +1,140 @@
/* Authored production-controller fixture. No BIOS, disc or retail code. */
#define _POSIX_C_SOURCE 200809L
#include "../src/dma.c"
#include <assert.h>
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<<bit;irqs++;}
void event_ring_record_aux(uint16_t kind,uint8_t src,uint32_t value) {(void)kind;(void)src;(void)value;}
int cdrom_dma_ready(void) {return read_words<available;}
uint32_t cdrom_dma_sector_word_count(void) {return available;}
uint32_t cdrom_dma_read(void) {assert(cdrom_dma_ready());return 0xCA000000u+read_words++;}
uint32_t cdrom_dma_read_padded(void) {return cdrom_dma_ready()?cdrom_dma_read():0;}
void cdrom_debug_snapshot(CDROMDebugState *s) {memset(s,0,sizeof(*s));s->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) {}
+153
View File
@@ -0,0 +1,153 @@
/* Authored production-controller fixture. No BIOS, disc or retail code. */
#define _POSIX_C_SOURCE 200809L
#include "../src/dma.c"
#include <assert.h>
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<<bit;irqs++;}
void event_ring_record_aux(uint16_t kind,uint8_t src,uint32_t value) {(void)kind;(void)src;(void)value;}
int cdrom_dma_ready(void) {return read_words<available;}
uint32_t cdrom_dma_sector_word_count(void) {return available;}
uint32_t cdrom_dma_read(void) {assert(cdrom_dma_ready());return 0xCA000000u+read_words++;}
uint32_t cdrom_dma_read_padded(void) {return cdrom_dma_ready()?cdrom_dma_read():0;}
void cdrom_debug_snapshot(CDROMDebugState *s) {memset(s,0,sizeof(*s));s->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) {}
+57
View File
@@ -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 <assert.h>
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<<bit; irqs++; }
void event_ring_record_aux(uint16_t kind,uint8_t src,uint32_t value) { (void)kind; (void)src; (void)value; }
static void setup(uint32_t count, uint64_t phase) {
dma_init();
memset(ram,0xCC,sizeof(ram)); writes=irqs=i_stat=0;
psx_cycle_count=phase; psx_next_service_cycle=0;
channels[6].madr=0x100000; channels[6].bcr=count; channels[6].chcr=0x11000002;
dpcr|=8u<<24;
dicr=(1u<<23)|(1u<<22);
}
static void tick(uint32_t cycles) { psx_cycle_count+=cycles; advance_source_otc(); }
int main(void) {
setup(1024,0);
execute_ch6_otc();
assert(writes==1024 && psx_cycle_count==0 && !(channels[6].chcr&(1u<<24)));
for(uint32_t phase=0;phase<128;phase++) {
setup(1024,phase); start_source_otc();
assert(writes==64 && otc_source.remaining==960 && irqs==0);
assert(ram[0x100000/4]==0xFFFFC && ram[(0x100000-64*4)/4]==0xCCCCCCCC);
uint64_t expected=((phase+960+127)/128)*128;
tick((uint32_t)(expected-psx_cycle_count)-1);
assert(otc_source.remaining && (channels[6].chcr&(1u<<24)) && irqs==0);
tick(1); assert(writes==1024 && !otc_source.remaining && irqs==1);
assert(ram[(0x100000-1023*4)/4]==0xFFFFFF);
tick(1000); assert(writes==1024 && irqs==1);
}
for(uint32_t n=1;n<=65;n++) {
setup(n,127); start_source_otc();
assert(writes==(n<64?n:64));
assert((otc_source.remaining==0)==(n<=64));
if(n==65) { tick(1); assert(writes==65 && irqs==1); }
}
setup(0,0); start_source_otc(); tick(65536);
assert(writes==65536 && !otc_source.remaining && irqs==1);
setup(1024,43); otc_source_model=1; execute_ch6_otc();
assert(writes==1024 && irqs==1 && psx_cycle_count==1024);
assert(dma_snapshot_read(NULL,0)==0); /* source model rejects restore before reading */
puts("PASS: default, source start budget, all 128 phases, partial RAM, completion/IRQ, short/zero counts, CPU wait, restore guard");
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) {}
+128
View File
@@ -0,0 +1,128 @@
/* Bounded authored uncached-RAM loop at a retained source pre-fetch boundary.
* Include the actual interpreter to observe its instruction/slot ownership;
* link remaining production device/scheduler objects without rendering init.
* No candidate command clock is enabled by this test. */
#include "../src/dirty_ram_interp.c"
#include "gpu.h"
#include "timers.h"
#include "dma.h"
#include "psx_bios_backend.h"
#include "source_gpu_runtime.h"
#include <assert.h>
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;
}
@@ -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 <stdio.h>
#include <string.h>
#include <stdint.h>
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;
}
@@ -0,0 +1,30 @@
/* Passive observer range/ordering regression. No guest execution is involved. */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
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;
}
@@ -0,0 +1,104 @@
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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<sizeof(invalid)/sizeof(*invalid);i++)assert(!input_instruction_sites_parse(invalid[i]));
assert(!input_instruction_sites_parse("00000000,00000004,00000008,0000000C,00000010,00000014,00000018,0000001C,00000020,00000024,00000028,0000002C,00000030,00000034,00000038,0000003C,00000040"));
assert(input_instruction_sites_parse("80001000,80001004"));
s_instruction_configured=1;s_instruction_first=10;s_instruction_last=11;
input_instruction_histogram_boundary(9);
input_instruction_histogram_record(0x80001000);
assert(!g_input_instruction_histogram_active && s_instruction_events==0);
input_instruction_histogram_boundary(10);
for(unsigned i=0;i<100;i++)input_instruction_histogram_record(0x80001000);
input_instruction_histogram_record(0x80001004);
assert(s_instruction_events==101 && s_instruction_dropped==0 && psx_cycle_count==1000);
assert(s_instruction_site_retained==101 && s_instruction_site_rows[100].pc==0x80001004 && s_instruction_site_rows[100].cycle==1000 && s_instruction_site_rows[100].sequence==100);
unsigned observed_first=0;
for(unsigned i=0;i<16384;i++)if(s_instruction_rows[i].used) {
assert(s_instruction_rows[i].first_cycle==1000);
assert(s_instruction_rows[i].first_sequence==(s_instruction_rows[i].pc==0x80001000?0:100));
observed_first++;
}
assert(observed_first==2);
g_ls_replay_active=1;input_instruction_histogram_record(0x80001000);g_ls_replay_active=0;
assert(s_instruction_events==101);
assert(s_instruction_site_retained==101);
input_instruction_histogram_boundary(11);
assert(outputs==1 && s_instruction_events==0 && g_input_instruction_histogram_active);
for(unsigned i=0;i<1000003;i++)input_instruction_histogram_record(0x80001000);
assert(s_instruction_events==1000000 && s_instruction_dropped==3);
assert(s_instruction_site_retained==8192 && s_instruction_site_dropped==1000000-8192);
input_instruction_histogram_boundary(12);
assert(outputs==2 && !g_input_instruction_histogram_active);
s_instruction_first=s_instruction_last=20;input_instruction_histogram_boundary(20);
for(unsigned i=0;i<16385;i++)input_instruction_histogram_record(0x80000000+i*4);
assert(s_instruction_events==16384 && s_instruction_dropped==1);
input_instruction_histogram_boundary(21);
s_instruction_first=s_instruction_last=30;input_instruction_histogram_boundary(30);
uint32_t local=17;
g_psx_cyc_batch=31;g_psx_cyc_local_acc=&local;
g_psx_icache_tv[0]=0x80001002u;
g_precise_mode=1;in_exception=1;i_stat=8;i_mask=9;
g_slice_fired=43;g_slice_irq_taken=9;g_slice_last_block=0x80000100;g_slice_last_committed=0x80000200;
input_instruction_histogram_record(0x80001000);
assert(s_instruction_context_rows[0].precise==1 && s_instruction_context_rows[0].in_exception==1);
assert(s_instruction_context_rows[0].istat==8 && s_instruction_context_rows[0].imask==9);
assert(s_instruction_context_rows[0].slices==43 && s_instruction_context_rows[0].takes==9);
assert(s_instruction_context_rows[0].last_block==0x80000100 && s_instruction_context_rows[0].last_committed==0x80000200);
assert(g_slice_fired==43 && g_slice_irq_taken==9 && i_stat==8);
assert(s_instruction_site_rows[0].cycle==1000);
assert(s_instruction_site_rows[0].effective_cycle==1048);
assert(s_instruction_site_rows[0].cache_tag==0x80001002u && s_instruction_site_rows[0].cache_active==1);
assert(psx_cycle_count==1000 && g_psx_cyc_batch==31 && local==17 && g_psx_icache_tv[0]==0x80001002u);
g_psx_cyc_local_acc=NULL;g_psx_cyc_batch=0;g_psx_icache_active=0;
input_instruction_histogram_record(0x80001004);
assert(s_instruction_site_rows[1].effective_cycle==1000 && s_instruction_site_rows[1].cache_active==0);
for(unsigned i=0;i<16384;i++)if(s_instruction_rows[i].used && s_instruction_rows[i].pc==0x80001000) {
assert(s_instruction_rows[i].first_cycle==1000 && s_instruction_rows[i].first_effective_cycle==1048);
assert(s_instruction_rows[i].first_cache_tag==0x80001002u && s_instruction_rows[i].first_cache_active==1);
}
input_instruction_histogram_boundary(31);
s_instruction_first=s_instruction_last=40;input_instruction_histogram_boundary(40);
input_instruction_histogram_record(0x80001000);
uint64_t before=psx_cycle_count;unsigned old_outputs=outputs;
input_instruction_histogram_exit();
assert(!g_input_instruction_histogram_active && partial_outputs==6);
assert(outputs==old_outputs+1 && psx_cycle_count==before && s_instruction_events==1);
input_instruction_histogram_exit();assert(partial_outputs==6);
/* Configuration parsing is separately checked by the launcher. */
(void)input_instruction_histogram_configure;
puts("instruction histogram: counts, interval reset, event/entry caps, shadow exclusion and passive cycles PASS");
}
@@ -0,0 +1,33 @@
#include "input_route_field_clock.h"
#include <assert.h>
#include <stdio.h>
/* 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;
}
+49
View File
@@ -0,0 +1,49 @@
#include "input_route_file.h"
#include <stdlib.h>
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<n;++i) {
le32(r,i+1); r[4]=(unsigned char)(fault==10 ? (i&1) : (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;
}
+72
View File
@@ -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 <string.h>
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 <stdlib.h>
#include <string.h>
#include <stdio.h>
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 */
}
@@ -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")
@@ -0,0 +1,93 @@
#include "input_route_raster_clock.h"
#include <assert.h>
#include <stdio.h>
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;j<span;j++) input_route_raster_advance(&b,1);
assert(!memcmp(&a,&b,sizeof(a)));
}
InputRouteRasterClock before=a;
assert(!input_route_raster_gp1(&a,0x08000008));
assert(!memcmp(&a,&before,sizeof(a)));
/* GPU soft reset restores control values without resetting beam phase. */
assert(input_route_raster_gp1(&a,0));
assert(a.cycle==before.cycle && a.fraction==before.fraction && a.scanline==before.scanline && a.field==before.field);
/* Changes behind the beam take effect at the next scanline match. */
input_route_raster_reset(&a);input_route_raster_advance(&a,500000);
assert(input_route_raster_gp1(&a,0x0703F010));
uint32_t distance=input_route_raster_until_rise(&a); assert(distance>0 && 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;
}
+205
View File
@@ -0,0 +1,205 @@
#include "input_update_clock.h"
#include <assert.h>
#include <stdio.h>
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");
}
+78
View File
@@ -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 <assert.h>
#include <stdio.h>
#include <stdlib.h>
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);
}
+63
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#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;i<count;i++){Event*e=&events[i];printf("%s{\"pc\":%u,\"cycle\":%llu,\"sr\":%u,\"cause\":%u,\"epc\":%u,\"r8\":%u,\"memory\":%u,\"target_tag\":%u}",i?",":"",e->pc,(unsigned long long)e->cycle,e->sr,e->cause,e->epc,e->r8,e->word,e->tag);}puts("]}");return 0;
}
+89
View File
@@ -0,0 +1,89 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#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;i<count;i++){Event*e=&events[i];printf("%s{\"pc\":%u,\"cycle\":%llu,\"sr\":%u,\"cause\":%u,\"epc\":%u,\"t8\":%u,\"t9\":%u,\"memory\":%u,\"v0\":%u,\"ra\":%u,\"sp\":%u}",i?",":"",e->pc,(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;
}
+39
View File
@@ -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 <stdio.h>
#include <string.h>
#include <stdint.h>
#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;
}
+41
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "dma.h"
#include <assert.h>
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;
}
+113
View File
@@ -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 <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}
+43
View File
@@ -0,0 +1,43 @@
#include "psx_cyc.h"
#include <assert.h>
#include <stdio.h>
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");
}
+100
View File
@@ -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 <assert.h>
#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')

Some files were not shown because too many files have changed in this diff Show More