mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 02:56:36 -04:00
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:
committed by
Alexandros Mandravillis
parent
5968162fe9
commit
3bd62b3099
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(§ors);
|
||||
f = open_output("cd-sectors.jsonl");
|
||||
start = total > CDROM_SECTOR_HISTORY_CAP ? total - CDROM_SECTOR_HISTORY_CAP : 0;
|
||||
for (uint64_t i = start; i < total; ++i) {
|
||||
const CDROMSectorHistoryEntry *e = §ors[i % CDROM_SECTOR_HISTORY_CAP];
|
||||
if (e->seq != i) fail("CD sector history sequence mismatch");
|
||||
fprintf(f, "{\"seq\":%llu,\"frame\":%u,\"lba\":%d,\"size\":%d,\"mode\":%u,\"data_delivered\":%u}\n",
|
||||
(unsigned long long)i, e->frame, e->lba, e->size, e->mode, e->data_delivered);
|
||||
}
|
||||
if (ferror(f) || fclose(f)) fail("CD sector metadata write");
|
||||
CdBurstRecord bursts[128];
|
||||
int n = cdrom_get_bursts(bursts, 128);
|
||||
f = open_output("cd-bursts.jsonl");
|
||||
for (int i = n - 1; i >= 0; --i) fprintf(f,
|
||||
"{\"start_frame\":%u,\"end_frame\":%u,\"sectors\":%u,\"divisor\":%u}\n",
|
||||
bursts[i].start_frame, bursts[i].end_frame, bursts[i].sectors, bursts[i].divisor);
|
||||
if (ferror(f) || fclose(f)) fail("CD burst metadata write");
|
||||
const DMACDROMHistoryEntry *dma;
|
||||
total = dma_debug_get_cdrom_history(&dma);
|
||||
start = total > DMA_CDROM_HISTORY_CAP ? total - DMA_CDROM_HISTORY_CAP : 0;
|
||||
f = open_output("cd-dma.jsonl");
|
||||
for (uint64_t i = start; i < total; ++i) {
|
||||
const DMACDROMHistoryEntry *e = &dma[i % DMA_CDROM_HISTORY_CAP];
|
||||
if (e->seq != i) fail("CD DMA history sequence mismatch");
|
||||
/* Retain transfer metadata only, never the ring's retail payload words. */
|
||||
fprintf(f,"{\"seq\":%llu,\"frame_start\":%u,\"frame_end\":%u,\"address\":%u,\"requested_words\":%u,\"moved_words\":%u,\"lba\":%d,\"sector_offset\":%d,\"completed\":%u,\"pc\":%u}\n",
|
||||
(unsigned long long)i, e->frame_start, e->frame_end, e->start_addr,
|
||||
e->requested_words,e->moved_words,e->lba,e->sector_read_pos_start,e->completed,e->pc);
|
||||
}
|
||||
if (ferror(f) || fclose(f)) fail("CD DMA metadata write");
|
||||
total = cdrom_timing_total();
|
||||
start = total > 4096 ? total - 4096 : 0;
|
||||
f = open_output("cd-timing.jsonl");
|
||||
for (uint64_t i = start; i < total; ++i) {
|
||||
CdTimingPub e;
|
||||
if (!cdrom_timing_record(i, &e) || e.seq != i) fail("CD timing history sequence mismatch");
|
||||
fprintf(f,"{\"seq\":%llu,\"frame\":%u,\"lba\":%d,\"due_cycle\":%llu,\"buffer_cycle\":%llu,\"irq_arm_cycle\":%llu,\"intc_cycle\":%llu,\"flags\":%u}\n",
|
||||
(unsigned long long)i,e.frame,e.lba,(unsigned long long)e.due_cycle,
|
||||
(unsigned long long)e.buffer_cycle,(unsigned long long)e.irq_arm_cycle,
|
||||
(unsigned long long)e.intc_cycle,e.flags);
|
||||
}
|
||||
if (ferror(f) || fclose(f)) fail("CD timing metadata write");
|
||||
f = open_output("device-events.json");
|
||||
if (event_ring_dump_stream(f) < 0 || fclose(f)) fail("device event metadata write");
|
||||
const CDROMTraceEntry *registers;
|
||||
total = cdrom_debug_get_trace(®isters);
|
||||
start = total > CDROM_TRACE_CAP ? total - CDROM_TRACE_CAP : 0;
|
||||
f = open_output("cd-registers.jsonl");
|
||||
for (uint64_t i = start; i < total; ++i) {
|
||||
const CDROMTraceEntry *e = ®isters[i % CDROM_TRACE_CAP];
|
||||
if (e->seq != i) fail("CD register history sequence mismatch");
|
||||
/* Exclude data-port payloads; retain status, command and IRQ metadata. */
|
||||
if (e->kind == 'D' || (e->kind == 'R' && (e->addr & 3u) == 2u)) continue;
|
||||
fprintf(f,"{\"seq\":%llu,\"cycle\":%llu,\"frame\":%u,\"kind\":%u,\"addr\":%u,\"val\":%u,\"pc\":%u,\"guest_ra\":%u,\"index\":%u,\"istat\":%u,\"irq_enable\":%u,\"irq_flag\":%u,\"response_read\":%u,\"response_count\":%u,\"sector_available\":%u,\"sector_pos\":%d}\n",
|
||||
(unsigned long long)i,(unsigned long long)e->cycle,e->frame,e->kind,e->addr,e->val,
|
||||
e->pc,e->guest_ra,e->index_reg,e->i_stat,e->irq_enable,e->irq_flag,e->response_read,
|
||||
e->response_count,e->sector_available,e->sector_read_pos);
|
||||
}
|
||||
if (ferror(f) || fclose(f)) fail("CD register metadata write");
|
||||
}
|
||||
int input_route_observer_init(uint32_t total) {
|
||||
if (log_file || total == 0) return 0;
|
||||
output_dir = getenv("PSX_INPUT_ROUTE_CAPTURE_DIR");
|
||||
if (!output_dir) return 1;
|
||||
if (!output_dir[0]) return 0;
|
||||
total_inputs = total;
|
||||
const char *watch = getenv("PSX_INPUT_ROUTE_WATCH_U16");
|
||||
if (watch) {
|
||||
const char *p = watch;
|
||||
do {
|
||||
char *end;
|
||||
if (*p < '0' || *p > '9' || watch_count == 32) return 0;
|
||||
errno = 0;
|
||||
unsigned long address = strtoul(p, &end, 0);
|
||||
if (errno || end == p || (*end && *end != ',') ||
|
||||
address > PSX_MAIN_RAM_BYTES - 2 || (address & 1)) return 0;
|
||||
for (unsigned i = 0; i < watch_count; ++i)
|
||||
if (watch_u16[i] == address) return 0;
|
||||
watch_u16[watch_count++] = (uint32_t)address;
|
||||
p = *end ? end + 1 : end;
|
||||
if (*end && !*p) return 0;
|
||||
} while (*p);
|
||||
}
|
||||
const char *every = getenv("PSX_INPUT_ROUTE_CAPTURE_EVERY");
|
||||
if (every) {
|
||||
char *end;
|
||||
unsigned long n = strtoul(every, &end, 10);
|
||||
if (!every[0] || *end || !n || n > 10000) return 0;
|
||||
capture_every = (uint32_t)n;
|
||||
}
|
||||
const char *tail = getenv("PSX_INPUT_ROUTE_NEUTRAL_TAIL");
|
||||
if (tail) {
|
||||
char *end;
|
||||
unsigned long n = strtoul(tail, &end, 10);
|
||||
if (!tail[0] || *end || n > 60000) return 0;
|
||||
tail_ticks = (uint32_t)n;
|
||||
}
|
||||
psx_sha256_init(&input_hash);
|
||||
psx_sha256_init(&delivered_hash);
|
||||
delivered_inputs = 0;
|
||||
pending = 0;
|
||||
first_non_neutral_seen = 0;
|
||||
log_file = open_output("checkpoints.jsonl");
|
||||
const char *cpu_state = getenv("PSX_INPUT_ROUTE_CPU_STATE");
|
||||
if (cpu_state && strcmp(cpu_state, "1") == 0)
|
||||
cpu_file = open_output("cpu-state.jsonl");
|
||||
if (getenv("PSX_INPUT_ROUTE_VIDEO_STATE") && strcmp(getenv("PSX_INPUT_ROUTE_VIDEO_STATE"),"1")==0)
|
||||
video_file = open_output("video-state.jsonl");
|
||||
if (watch_count) watch_file = open_output("ram-u16.jsonl");
|
||||
const char *trace = getenv("PSX_INPUT_ROUTE_TRACE");
|
||||
if (trace && strcmp(trace, "1") == 0) sio_trace_file = open_output("sio-bytes.jsonl");
|
||||
return 1;
|
||||
}
|
||||
void input_route_observer_input(uint16_t buttons) {
|
||||
if (!log_file) return;
|
||||
if (pending) fail("previous input was not delivered to SIO");
|
||||
pending_word = buttons;
|
||||
pending = 1;
|
||||
const uint8_t word[2] = {(uint8_t)buttons, (uint8_t)(buttons >> 8)};
|
||||
if (delivered_inputs < total_inputs)
|
||||
psx_sha256_update(&input_hash, word, sizeof(word));
|
||||
else if (buttons != 0xFFFF) fail("post-input sample was not neutral");
|
||||
}
|
||||
void input_route_observer_set_end(uint32_t total) {
|
||||
if (!total || total != delivered_inputs || pending) fail("invalid dynamic input end");
|
||||
total_inputs=total;
|
||||
}
|
||||
void input_route_observer_applied(uint16_t buttons, int connected, int analog) {
|
||||
if (!log_file) return;
|
||||
if (!pending || pending_word != buttons || !connected || analog)
|
||||
fail("SIO delivery differs from digital route");
|
||||
const uint8_t word[2] = {(uint8_t)buttons, (uint8_t)(buttons >> 8)};
|
||||
if (delivered_inputs < total_inputs)
|
||||
psx_sha256_update(&delivered_hash, word, sizeof(word));
|
||||
++delivered_inputs;
|
||||
pending = 0;
|
||||
if (buttons != 0xFFFF && !first_non_neutral_seen) {
|
||||
fprintf(stdout, "input_route_first_non_neutral: record=%u buttons=%04X connected=%d analog=%d\n",
|
||||
(unsigned)delivered_inputs, (unsigned)buttons, connected, analog);
|
||||
fflush(stdout);
|
||||
first_non_neutral_seen = 1;
|
||||
}
|
||||
}
|
||||
void input_route_observer_boundary(uint32_t completed, uint64_t runtime_frame) {
|
||||
if (log_file && (pending || completed != delivered_inputs))
|
||||
fail("boundary does not follow exact SIO delivery count");
|
||||
if (video_file) {
|
||||
uint32_t gpu[5],clock[3];
|
||||
gpu_observer_video_state(gpu); interrupts_observer_field_state(clock);
|
||||
fprintf(video_file,"{\"frame\":%u,\"runtime_frame\":%llu,\"boundary\":\"before_next_input\",\"cycle\":%llu,\"display_mode\":%u,\"vertical_start\":%u,\"vertical_end\":%u,\"gpu_field\":%u,\"lcf\":%u,\"cycles_since_vblank\":%u,\"next_period\":%u,\"profile_field\":%u}\n",
|
||||
completed,(unsigned long long)runtime_frame,(unsigned long long)psx_cycle_count,
|
||||
gpu[0],gpu[1],gpu[2],gpu[3],gpu[4],clock[0],clock[1],clock[2]);
|
||||
if(ferror(video_file) || fflush(video_file))fail("video state write");
|
||||
}
|
||||
if (cpu_file) {
|
||||
const CPUState *cpu = debug_cpu_ptr;
|
||||
if (!cpu) fail("CPU state unavailable");
|
||||
uint16_t counter[3], target[3];
|
||||
uint32_t mode[3], frac[3];
|
||||
int32_t irq_line[3];
|
||||
timers_get_snapshot(counter, mode, target, irq_line, frac);
|
||||
fprintf(cpu_file, "{\"frame\":%u,\"runtime_frame\":%llu,\"boundary\":\"before_next_input\",\"pc\":%u,\"ra\":%u,\"sr\":%u,\"cause\":%u,\"epc\":%u,\"i_stat\":%u,\"i_mask\":%u,\"timers\":[",
|
||||
completed, (unsigned long long)runtime_frame, cpu->pc, cpu->gpr[31],
|
||||
cpu->cop0[12], cpu->cop0[13], cpu->cop0[14], i_stat, i_mask);
|
||||
for (unsigned i=0; i<3; ++i)
|
||||
fprintf(cpu_file, "%s{\"channel\":%u,\"counter\":%u,\"mode\":%u,\"target\":%u,\"irq_line\":%d,\"fraction\":%u}",
|
||||
i ? "," : "", i, counter[i], mode[i], target[i], irq_line[i], frac[i]);
|
||||
fputs("]}\n", cpu_file);
|
||||
if (ferror(cpu_file) || fflush(cpu_file)) fail("CPU state write");
|
||||
}
|
||||
if (sio_trace_file) {
|
||||
const SioTraceEntry *ring;
|
||||
int next;
|
||||
uint32_t total = sio_get_trace(&ring, &next);
|
||||
if (total - trace_seen > SIO_TRACE_CAP) fail("SIO trace overwritten");
|
||||
while (trace_seen < total) {
|
||||
const SioTraceEntry *e = &ring[trace_seen % SIO_TRACE_CAP];
|
||||
if (e->seq != trace_seen) fail("SIO trace sequence mismatch");
|
||||
if (fprintf(sio_trace_file,
|
||||
"{\"boundary\":%u,\"seq\":%u,\"tx\":%u,\"rx\":%u,\"ctrl\":%u,\"dev_pre\":%u,\"dev_post\":%u}\n",
|
||||
completed, e->seq, e->tx, e->rx, e->ctrl, e->dev_pre, e->dev_post) < 0)
|
||||
fail("SIO trace write");
|
||||
++trace_seen;
|
||||
}
|
||||
if (fflush(sio_trace_file)) fail("SIO trace flush");
|
||||
}
|
||||
if (watch_file) {
|
||||
fprintf(watch_file, "{\"frame\":%u,\"runtime_frame\":%llu,\"boundary\":\"before_next_input\",\"cpu_pc\":%u,\"cpu_ra\":%u,\"ram_u16\":{",
|
||||
completed, (unsigned long long)runtime_frame,
|
||||
debug_cpu_ptr ? debug_cpu_ptr->pc : 0,
|
||||
debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0);
|
||||
for (unsigned i = 0; i < watch_count; ++i) {
|
||||
uint32_t a = watch_u16[i];
|
||||
unsigned value = g_psx_ram[a] | ((unsigned)g_psx_ram[a + 1] << 8);
|
||||
fprintf(watch_file, "%s\"%06X\":%u", i ? "," : "", a, value);
|
||||
}
|
||||
if (fputs("}}\n", watch_file) < 0 || fflush(watch_file)) fail("RAM watch write");
|
||||
}
|
||||
if (!log_file || (completed % (completed > total_inputs && capture_every > 60 ? 60 : capture_every)
|
||||
&& completed != total_inputs && completed != total_inputs + tail_ticks)) return;
|
||||
uint8_t digest[32]; char ram_hash[65], applied_hash[65], supplied_hash[65], name[80];
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
if (!di.depth24 && !di.disabled) {
|
||||
gl_renderer_sync_cpu(); vk_renderer_sync_cpu();
|
||||
}
|
||||
psx_sha256_compute(g_psx_ram, PSX_MAIN_RAM_BYTES, digest);
|
||||
hex_hash(digest, ram_hash);
|
||||
psx_sha256_ctx supplied = input_hash;
|
||||
psx_sha256_final(&supplied, digest); hex_hash(digest, supplied_hash);
|
||||
psx_sha256_ctx applied = delivered_hash;
|
||||
psx_sha256_final(&applied, digest); hex_hash(digest, applied_hash);
|
||||
unsigned w = di.disabled ? 1u : (unsigned)di.width;
|
||||
unsigned h = di.disabled ? 1u : (unsigned)di.height;
|
||||
if (!w || w > 640) w = 1;
|
||||
if (!h || h > 512) h = 1;
|
||||
uint8_t *rgb = (uint8_t *)calloc((size_t)w*h, 3);
|
||||
if (!rgb) fail("screenshot allocation");
|
||||
if (!di.disabled) for (unsigned y=0; y<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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;}
|
||||
@@ -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) {}
|
||||
@@ -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) {}
|
||||
@@ -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) {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user