mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 10:59:16 -04:00
980274a405
Segment-aware code, rollout PR B (docs/SEGMENT_AWARE_CODE.md §5.2). Game/overlay emitter: CodeGenerator gains a code segment and runtime_pc(addr) = code_seg | phys. Links (including a jalr in a branch delay slot), fetch tags and the uncached-fetch test in emit_pre_icache, interrupt resume PCs, CPS exit PCs and continuation keys, store-PC stamps, the reserved-instruction EPC, the slice resume PC, call_by_address and stale-guard targets, the call contract's return PC, cosim PCs, the VSync-query hook's base PC, the data stubs' unknown-dispatch PC and the dispatch rows' addr/resume_pc all go through it. func_/block_ names, .ranges and entry-hook ids keep the compile address. The VSync-query hook is not emitted for an uncached code segment: its hand-timed body charges cached fetches only. The segment defaults to the image's own, so runtime_pc(addr) == addr for every existing input and KSEG0 titles regenerate byte for byte. BIOS emitter: StrictTranslator::translate(d, runtime_pc) stamps stores, the syscall EPC and the break and unaligned-access PCs with the PC the code runs at; the psx_ldd_ temporaries keep the ROM address. The full-function emitter passes relocate_ra() to every translate() call and publishes the runtime PC on a fallthrough. Relocated kernel and shell code therefore stamps its RAM address, as the interpreter does. memory.c re-keys, in the same change, the seven SCPH-1001 store-PC keys that named ROM addresses of relocated code: RAM-0 filters 0x00000F00 (ROM 0xBFC10A00), 0x80056EB4, 0x800585E4, 0x80058788, 0x80059C50 (ROM 0xBFC3EEB4/405E4/40788/41C50), the opt-in Tomba EvCB key 0x00001CE4 (ROM 0xBFC117E4) and the GP0 source key 0x80050B1C (ROM 0xBFC38B1C). Without the re-key the new stamps would silently disable all seven. A RAM PC is ambiguous where the ROM key was not, so each key goes through scph1001_relocated_store(pc, rom): it matches only while the active image is SCPH-1001 and the word at pc is still the ROM word copied there. Other BIOSes and game code at those addresses never match, as before. Tests: emitter_runtime_pc_test (new, CTest, run with PSX_CPS unset and set to 0) compiles a small program through generate_all_functions with the code segment left default, KSEG0, KUSEG and KSEG1, in CPS, CPS-overlay and legacy mode. It requires every emission path to be reached and checks every guest-visible PC class, the identity keys, physical-only differences and KSEG1 per-instruction fetch charging, and the translator's runtime PCs in every PC-bearing form. The ledger gains two regression guards: bios-runtime-pc (OpenBIOS output, plus every translate() call site) and store-pc-keys-runtime (memory.c keys against SCPH1001.toml's windows). KNOWN_GAPS is unchanged: B closes no ledger id on its own (§5.2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
502 lines
24 KiB
C++
502 lines
24 KiB
C++
// Every guest-visible PC the emitters bake goes through runtime_pc()
|
|
// (docs/SEGMENT_AWARE_CODE.md §5.2, rollout PR B).
|
|
//
|
|
// Game/overlay emitter: CodeGenerator::runtime_pc(addr) = code_seg | phys.
|
|
// The same synthetic function is compiled at KSEG0 compile addresses with the
|
|
// code segment left at its default, set to KSEG0 explicitly, to KUSEG and to
|
|
// KSEG1. The checks:
|
|
// - the default and an explicit KSEG0 give byte-identical output, and the
|
|
// default follows the image (a KSEG1 image keeps its own segment), so the
|
|
// refactor changes nothing for any existing input;
|
|
// - with KUSEG or KSEG1, every PC in guest-visible state carries that
|
|
// segment: links (including a jalr in a branch delay slot), fetch tags,
|
|
// interrupt resume PCs, CPS exits (split j, fallthrough past the unit
|
|
// edge, image-edge tail transfer) and continuation keys, stale-static
|
|
// guards (named and addressed), store-PC stamps, the reserved-instruction
|
|
// EPC, the slice resume PC, call_by_address targets, the call-contract
|
|
// return PC, the VSync-query hook's base PC, alias-group entry keys and
|
|
// wrapper entries, and the unknown-dispatch PC of data stubs;
|
|
// - identity keys (func_/block_ names, debug_server_log_call_entry, the
|
|
// cyc_watch block observer, psx_native_bad_entry's owner) keep the
|
|
// compile address;
|
|
// - nothing else changes: with every 0x........ constant reduced to its
|
|
// physical address, the KUSEG output equals the KSEG0 output;
|
|
// - a KSEG1 code segment charges a fetch before every instruction, exactly
|
|
// as compiling the same bytes at KSEG1 addresses does (#429's rule, now
|
|
// keyed on the runtime PC; PR D's KSEG1 variants rely on it).
|
|
// Each is checked in CPS mode (the default), CPS overlay mode and legacy
|
|
// (PSX_CPS=0) mode.
|
|
//
|
|
// BIOS translator: StrictTranslator::translate(d, runtime_pc) stamps stores,
|
|
// the syscall EPC and the break/unaligned-access PCs (immediate and deferred
|
|
// loads) with the runtime PC while the psx_ldd_ temporaries keep the ROM
|
|
// address. The syscall form follows PSX_CPS at process start, so ctest runs
|
|
// this binary twice (emitter_runtime_pc_test and _legacy).
|
|
//
|
|
// Not covered here: the game dispatch rows main_psx.cpp writes. Until PR C
|
|
// sets a non-KSEG0 code segment for a real executable they equal the compile
|
|
// address; C's ledger checks query them.
|
|
|
|
#include "code_generator.h"
|
|
#include "control_flow.h"
|
|
#include "mips_decoder.h"
|
|
#include "strict_translator.h"
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <map>
|
|
#include <regex>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
int failures = 0;
|
|
|
|
void check(bool condition, const std::string& message) {
|
|
if (!condition) {
|
|
std::fprintf(stderr, "FAIL: %s\n", message.c_str());
|
|
++failures;
|
|
}
|
|
}
|
|
|
|
std::string hex(uint32_t v) {
|
|
char buf[16];
|
|
std::snprintf(buf, sizeof buf, "%08X", v);
|
|
return buf;
|
|
}
|
|
|
|
// CodeGenerator reads PSX_CPS when it is constructed.
|
|
void set_env(const char* name, const char* value) {
|
|
#ifdef _WIN32
|
|
_putenv_s(name, value);
|
|
#else
|
|
setenv(name, value, 1);
|
|
#endif
|
|
}
|
|
|
|
void append_word(std::vector<uint8_t>& bytes, uint32_t word) {
|
|
bytes.push_back(static_cast<uint8_t>(word));
|
|
bytes.push_back(static_cast<uint8_t>(word >> 8));
|
|
bytes.push_back(static_cast<uint8_t>(word >> 16));
|
|
bytes.push_back(static_cast<uint8_t>(word >> 24));
|
|
}
|
|
|
|
constexpr uint32_t kPhys = 0x00010000u;
|
|
constexpr uint32_t KUSEG = 0x00000000u, KSEG0 = 0x80000000u, KSEG1 = 0xA0000000u;
|
|
|
|
// Functions (compile offsets from kPhys):
|
|
// +00 main: jal/bgezal/jalr links and CPS continuations, stores for
|
|
// store-PC stamps (the sh at +10 is a widescreen backdrop site and the
|
|
// sb at +14 a persisted-option site, which emit their own stamps);
|
|
// bgezal's target +28 is a mid-function block leader.
|
|
// +40 callee, also the VSync-query HLE function.
|
|
// +48 a reserved word (RI exception).
|
|
// +50 j to the known function +40; +58 j into the middle of +00.
|
|
// +60 plain sh/sb stores and no control flow: falls through into +68.
|
|
// +68 a jalr in the delay slot of an always-taken beq.
|
|
// +7C, +84 end in jal / jalr whose return lies in the next (known) function.
|
|
// +8C a conditional branch to the known +40, falling into the known +94.
|
|
// +94 a conditional branch into the middle of +00, falling into +9C.
|
|
// +9C, +A4 end in jal / jalr whose return lies in the next function; the jal
|
|
// targets +B4.
|
|
// +AC a data section; +B4 undecodable words (the PSX_EMIT_PROD data stub).
|
|
// +BC host with the alias entries +C0 and +C4; it runs off the image edge.
|
|
// +9C, +A4, +AC and +B4 are left out of the known-function set, so edges into
|
|
// them go through call_by_address.
|
|
const uint32_t kProgram[] = {
|
|
0x27BDFFF8u, // +00 addiu $sp, $sp, -8
|
|
0xAFBF0004u, // +04 sw $ra, 4($sp)
|
|
0x0C004010u, // +08 jal +40
|
|
0x00000000u, // +0C nop
|
|
0xA7A80002u, // +10 sh $t0, 2($sp)
|
|
0xA3A80003u, // +14 sb $t0, 3($sp)
|
|
0x04110003u, // +18 bgezal $zero, +28
|
|
0x00000000u, // +1C nop
|
|
0x0100F809u, // +20 jalr $t0
|
|
0x00000000u, // +24 nop
|
|
0xABA80004u, // +28 swl $t0, 4($sp)
|
|
0xBBA80004u, // +2C swr $t0, 4($sp)
|
|
0xEBA80000u, // +30 swc2 $8, 0($sp)
|
|
0x8FBF0004u, // +34 lw $ra, 4($sp)
|
|
0x03E00008u, // +38 jr $ra
|
|
0x27BD0008u, // +3C addiu $sp, $sp, 8
|
|
0x03E00008u, // +40 jr $ra (callee)
|
|
0x00000000u, // +44 nop
|
|
0x50000001u, // +48 beql (reserved on the R3000A: RI exception)
|
|
0x00000000u, // +4C nop
|
|
0x08004010u, // +50 j +40 (known function)
|
|
0x00000000u, // +54 nop
|
|
0x08004001u, // +58 j +04 (mid-function)
|
|
0x00000000u, // +5C nop
|
|
0xA7A80006u, // +60 sh $t0, 6($sp) (plain store sites: +10/+14
|
|
0xA3A80007u, // +64 sb $t0, 7($sp) are hooked; falls into +68)
|
|
0x10000002u, // +68 beq $zero, $zero, +74
|
|
0x0100F809u, // +6C jalr $t0 (in the delay slot)
|
|
0x00000000u, // +70 nop
|
|
0x03E00008u, // +74 jr $ra
|
|
0x00000000u, // +78 nop
|
|
0x0C004010u, // +7C jal +40 (returns into +84)
|
|
0x00000000u, // +80 nop
|
|
0x0100F809u, // +84 jalr $t0 (returns into +8C)
|
|
0x00000000u, // +88 nop
|
|
0x1500FFECu, // +8C bne $t0, $zero, +40
|
|
0x00000000u, // +90 nop
|
|
0x1500FFDBu, // +94 bne $t0, $zero, +04
|
|
0x00000000u, // +98 nop
|
|
0x0C00402Du, // +9C jal +B4 (not known; returns into +A4)
|
|
0x00000000u, // +A0 nop
|
|
0x0100F809u, // +A4 jalr $t0 (returns into +AC)
|
|
0x00000000u, // +A8 nop
|
|
0xFFFFFFFFu, // +AC data
|
|
0xFFFFFFFFu, // +B0 data
|
|
0x44000000u, // +B4 COP1: no translation (TODO words)
|
|
0x44000000u, // +B8 COP1
|
|
0x25080001u, // +BC addiu $t0, $t0, 1 (alias host)
|
|
0xAFA80000u, // +C0 sw $t0, 0($sp) (alias entry)
|
|
0x25080001u, // +C4 addiu $t0, $t0, 1 (alias entry)
|
|
0x0C004010u, // +C8 jal +40
|
|
0x00000000u, // +CC nop
|
|
0x25080001u, // +D0 addiu $t0, $t0, 1 (runs off the image edge)
|
|
};
|
|
constexpr uint32_t kCount = sizeof(kProgram) / sizeof(kProgram[0]);
|
|
|
|
enum class Mode { Cps, CpsOverlay, Legacy };
|
|
|
|
const char* mode_name(Mode m) {
|
|
return m == Mode::Cps ? "cps" : m == Mode::CpsOverlay ? "cps-overlay" : "legacy";
|
|
}
|
|
|
|
struct FuncSpec {
|
|
uint32_t start, end;
|
|
uint32_t alias_host; // nonzero: an alias entry of the host at this offset
|
|
bool data; // a data section
|
|
};
|
|
const FuncSpec kFuncs[] = {
|
|
{0x00u, 0x40u, 0, false}, {0x40u, 0x48u, 0, false}, {0x48u, 0x50u, 0, false},
|
|
{0x50u, 0x58u, 0, false}, {0x58u, 0x60u, 0, false}, {0x60u, 0x68u, 0, false},
|
|
{0x68u, 0x7Cu, 0, false}, {0x7Cu, 0x84u, 0, false}, {0x84u, 0x8Cu, 0, false},
|
|
{0x8Cu, 0x94u, 0, false}, {0x94u, 0x9Cu, 0, false}, {0x9Cu, 0xA4u, 0, false},
|
|
{0xA4u, 0xACu, 0, false}, {0xACu, 0xB4u, 0, true}, {0xB4u, 0xBCu, 0, false},
|
|
{0xBCu, 0xD4u, 0, false}, {0xC0u, 0xD4u, 0xBCu, false}, {0xC4u, 0xD4u, 0xBCu, false},
|
|
};
|
|
constexpr uint32_t kVsyncFunc = 0x40u;
|
|
const std::set<uint32_t> kNotKnown = {0x9Cu, 0xA4u, 0xACu, 0xB4u};
|
|
constexpr uint32_t kBackdropSite = 0x10u; // sh
|
|
constexpr uint32_t kPersistSite = 0x14u; // sb
|
|
// Words with no emitted fetch of their own: the reserved word raises RI
|
|
// before its timing (the emitter's existing order) and the word after it is
|
|
// unreachable; data and undecodable words become stubs.
|
|
const std::set<uint32_t> kNoFetch = {0x48u, 0x4Cu, 0xACu, 0xB0u, 0xB4u, 0xB8u};
|
|
|
|
// seg_override < 0: leave the code segment at its default.
|
|
std::string generate(uint32_t image_seg, long long seg_override, Mode mode) {
|
|
set_env("PSX_CPS", mode == Mode::Legacy ? "0" : "1");
|
|
const uint32_t base = image_seg | kPhys;
|
|
PSXRecomp::PS1Executable exe{};
|
|
exe.header.load_address = base;
|
|
exe.header.initial_pc = base;
|
|
exe.header.file_size = kCount * 4u;
|
|
for (uint32_t w : kProgram) append_word(exe.code_data, w);
|
|
|
|
PSXRecomp::CodeGenConfig config{};
|
|
config.emit_comments = true;
|
|
config.indent = " ";
|
|
config.overlay_mode = (mode == Mode::CpsOverlay);
|
|
config.vsync_query_hle_funcs[base + kVsyncFunc] = {0x80090000u, 0x80090004u,
|
|
0x80090008u, 0x8009000Cu};
|
|
config.ws_backdrop_x_sites.insert(base + kBackdropSite);
|
|
config.persist_init_store_sites.insert(base + kPersistSite);
|
|
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
|
|
PSXRecomp::CodeGenerator generator(exe, config);
|
|
if (seg_override >= 0) generator.set_code_segment(static_cast<uint32_t>(seg_override));
|
|
|
|
std::vector<PSXRecomp::Function> funcs;
|
|
std::set<uint32_t> known;
|
|
for (const FuncSpec& fs : kFuncs) {
|
|
PSXRecomp::Function f{};
|
|
f.start_addr = base + fs.start;
|
|
f.end_addr = base + fs.end;
|
|
f.size = fs.end - fs.start;
|
|
f.name = "func_" + hex(f.start_addr);
|
|
f.is_data_section = fs.data;
|
|
if (fs.alias_host) {
|
|
f.alias_walk_lo = base + fs.alias_host;
|
|
for (const FuncSpec& o : kFuncs)
|
|
if (o.alias_host == fs.alias_host) f.alias_group_entries.push_back(base + o.start);
|
|
}
|
|
funcs.push_back(f);
|
|
if (!kNotKnown.count(fs.start)) known.insert(f.start_addr);
|
|
}
|
|
generator.set_known_functions(known);
|
|
std::map<uint32_t, PSXRecomp::ControlFlowGraph> cfgs;
|
|
for (const auto& f : funcs)
|
|
if (!f.is_data_section) cfgs[f.start_addr] = analyzer.analyze_function(f);
|
|
|
|
std::string out;
|
|
for (const auto& gf : generator.generate_all_functions(funcs, cfgs)) out += gf.full_code;
|
|
return out;
|
|
}
|
|
|
|
// Constants of one site class, in emission order.
|
|
std::vector<uint32_t> constants(const std::string& code, const std::string& pattern) {
|
|
std::vector<uint32_t> out;
|
|
const std::regex re(pattern);
|
|
for (auto it = std::sregex_iterator(code.begin(), code.end(), re);
|
|
it != std::sregex_iterator(); ++it) {
|
|
for (size_t g = 1; g < it->size(); ++g) {
|
|
if ((*it)[g].matched) {
|
|
out.push_back(static_cast<uint32_t>(std::stoul((*it)[g].str(), nullptr, 16)));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Every 0x........ constant (literal or in a comment) reduced to physical.
|
|
std::string to_phys(const std::string& code) {
|
|
static const std::regex re("0x([0-9A-F]{8})");
|
|
std::string out;
|
|
auto last = code.cbegin();
|
|
for (auto it = std::sregex_iterator(code.begin(), code.end(), re);
|
|
it != std::sregex_iterator(); ++it) {
|
|
out.append(last, code.cbegin() + it->position());
|
|
const uint32_t v = static_cast<uint32_t>(std::stoul((*it)[1].str(), nullptr, 16));
|
|
out += "0x" + hex(v & 0x1FFFFFFFu);
|
|
last = code.cbegin() + it->position() + it->length();
|
|
}
|
|
out.append(last, code.cend());
|
|
return out;
|
|
}
|
|
|
|
struct Site {
|
|
const char* id;
|
|
const char* pattern;
|
|
bool routed; // true: carries the code segment; false: compile identity
|
|
};
|
|
|
|
const Site kSites[] = {
|
|
{"link", R"(= 0x([0-9A-F]{8})u; /\* (?:jal link|jalr link|branch-and-link))", true},
|
|
{"cps-jal-note", R"(/\* CPS jal -> 0x([0-9A-F]{8}) \*/)", true},
|
|
{"delay-slot-link", R"(= 0x([0-9A-F]{8}); cpu->pc = _jt_)", true},
|
|
{"fetch-tag", R"(psx_icache_fetch\(cpu, 0x([0-9A-F]{8})u\))", true},
|
|
{"irq-resume", R"(psx_check_interrupts_at\(cpu, 0x([0-9A-F]{8})u\))", true},
|
|
{"exit-pc", R"(cpu->pc = 0x([0-9A-F]{8})u;)", true},
|
|
{"continuation-key", R"(case 0x([0-9A-F]{8})u:)", true},
|
|
{"store-pc", R"(g_debug_last_store_pc = 0x([0-9A-F]{8})u;)", true},
|
|
{"ri-epc", R"(cpu->cop0\[14\] = 0x([0-9A-F]{8})u;)", true},
|
|
{"slice-pc", R"(psx_slice_block\(cpu, 0x([0-9A-F]{8})u)", true},
|
|
{"call-by-address", R"(call_by_address\(cpu, 0x([0-9A-F]{8})u\))", true},
|
|
{"call-contract", R"(psx_call_contract\(cpu, 0x([0-9A-F]{8})u)", true},
|
|
{"stale-guard", R"(psx_game_text_native_ok\(0x([0-9A-F]{8})u\))", true},
|
|
{"vsync-hle", R"(psx_vsync_query_hle_enter\(cpu, 0x([0-9A-F]{8})u,)", true},
|
|
{"alias-entry", R"(psx_alias_body_[0-9A-F]{8}\(cpu, 0x([0-9A-F]{8})u\))", true},
|
|
{"unknown-dispatch", R"(psx_unknown_dispatch\(cpu, 0x([0-9A-F]{8})u,)", true},
|
|
{"cosim", R"(cosim_(?:block|instr)\(0x([0-9A-F]{8})u\))", true},
|
|
{"func-name", R"(func_([0-9A-F]{8}))", false},
|
|
{"block-label", R"(block_([0-9A-F]{8}))", false},
|
|
{"call-entry-hook", R"(debug_server_log_call_entry\(0x([0-9A-F]{8})u\))", false},
|
|
{"cyc-observe", R"(debug_server_cyc_observe\(0x([0-9A-F]{8})u\))", false},
|
|
{"bad-entry-owner", R"(psx_native_bad_entry\(cpu, 0x([0-9A-F]{8})u)", false},
|
|
};
|
|
|
|
// Emitted text that must be present, per mode, so a route cannot vanish
|
|
// unnoticed: each names one emission site.
|
|
struct Marker {
|
|
const char* text;
|
|
bool cps, overlay, legacy;
|
|
};
|
|
const Marker kMarkers[] = {
|
|
{"/* CPS taken: split */", true, true, false},
|
|
{"/* CPS not taken: split */", true, true, false},
|
|
{"/* taken: split piece */", false, false, true},
|
|
{"/* not taken: split piece */", false, false, true},
|
|
{"/* taken: split (mid-func) */", false, false, true},
|
|
{"/* not taken: split (mid-func) */", false, false, true},
|
|
{"psx_ws_backdrop_x(", true, true, true},
|
|
{"psx_game_option_store(", true, true, true},
|
|
{"/* CPS j: split */", true, true, false},
|
|
{"/* external jal */", false, false, true},
|
|
{"/* jal cont: split piece */", false, false, true},
|
|
{"/* jalr cont: split piece */", false, false, true},
|
|
{"/* jal cont: split */", false, false, true},
|
|
{"/* jalr cont: split */", false, false, true},
|
|
{"/* j to split piece */", false, false, true},
|
|
{"/* j to split (mid-func) */", false, false, true},
|
|
{"/* fallthrough to split piece */", true, true, true},
|
|
{"/* fallthrough to next function */", true, true, true},
|
|
{"/* CPS fallthrough past unit edge */", true, true, false},
|
|
{"/* image-edge fallthrough: tail-transfer */", true, true, true},
|
|
{"/* alias entry into host", true, true, true},
|
|
{"switch (entry) {", true, true, true},
|
|
{"/* jalr */", true, true, true},
|
|
{"/* branch-and-link before delay slot */", true, true, true},
|
|
};
|
|
|
|
void check_game_emitter(Mode mode) {
|
|
const std::string m = mode_name(mode);
|
|
const std::string def = generate(KSEG0, -1, mode);
|
|
check(def == generate(KSEG0, KSEG0, mode),
|
|
m + ": an explicit KSEG0 code segment changed the output");
|
|
// A KSEG1 image keeps its own segment by default (the pre-B behaviour).
|
|
const std::string k1_image = generate(KSEG1, -1, mode);
|
|
check(k1_image == generate(KSEG1, KSEG1, mode),
|
|
m + ": the default code segment does not follow the image");
|
|
|
|
for (const Marker& mk : kMarkers) {
|
|
const bool want = mode == Mode::Cps ? mk.cps
|
|
: mode == Mode::CpsOverlay ? mk.overlay : mk.legacy;
|
|
if (want)
|
|
check(def.find(mk.text) != std::string::npos,
|
|
m + ": the program no longer reaches \"" + mk.text + "\"");
|
|
}
|
|
|
|
for (uint32_t seg : {KUSEG, KSEG1}) {
|
|
const std::string sn = seg == KUSEG ? "KUSEG" : "KSEG1";
|
|
const std::string code = generate(KSEG0, seg, mode);
|
|
// KSEG1 adds per-instruction fetches (checked below); KUSEG is
|
|
// cached like KSEG0, so only constant segments may differ.
|
|
if (seg == KUSEG)
|
|
check(to_phys(code) == to_phys(def),
|
|
m + "/" + sn + ": output differs from KSEG0 beyond constant segments");
|
|
std::set<std::string> seen;
|
|
for (const Site& s : kSites) {
|
|
const auto vals = constants(code, s.pattern);
|
|
if (!vals.empty()) seen.insert(s.id);
|
|
for (uint32_t v : vals) {
|
|
// Continuation keys also include jump-table case values in
|
|
// general; this program has none, so every key is a PC.
|
|
const uint32_t want = s.routed ? seg : KSEG0;
|
|
if (s.routed && v == 0) continue;
|
|
check((v & 0xE0000000u) == want,
|
|
m + "/" + sn + ": " + s.id + " 0x" + hex(v) + " is not in " +
|
|
(s.routed ? sn : std::string("the compile segment")));
|
|
}
|
|
}
|
|
// Every class this mode emits must actually have been seen.
|
|
std::vector<std::string> want_seen = {
|
|
"link", "delay-slot-link", "fetch-tag", "irq-resume", "exit-pc",
|
|
"continuation-key", "store-pc", "ri-epc", "slice-pc", "stale-guard",
|
|
"alias-entry", "unknown-dispatch", "func-name", "block-label",
|
|
"call-entry-hook", "cyc-observe"};
|
|
if (mode != Mode::Legacy) want_seen.push_back("cps-jal-note");
|
|
if (mode == Mode::Legacy) {
|
|
want_seen.push_back("call-by-address");
|
|
want_seen.push_back("call-contract");
|
|
}
|
|
if (mode == Mode::CpsOverlay) want_seen.push_back("bad-entry-owner");
|
|
// The VSync-query hook's hand-timed body charges cached fetches only,
|
|
// so an uncached code segment runs the compiled instructions instead.
|
|
if (seg == KUSEG) want_seen.push_back("vsync-hle");
|
|
else check(seen.count("vsync-hle") == 0,
|
|
m + "/" + sn + ": the VSync-query hook is emitted for uncached code");
|
|
for (const auto& id : want_seen)
|
|
check(seen.count(id) == 1, m + "/" + sn + ": no " + id + " site emitted");
|
|
}
|
|
check(def.find("psx_vsync_query_hle_enter(cpu, 0x" + hex(KSEG0 | (kPhys + kVsyncFunc)) +
|
|
"u,") != std::string::npos,
|
|
m + ": the KSEG0 VSync-query hook is missing or moved");
|
|
|
|
// KSEG1 code segment at KSEG0 compile addresses charges exactly what the
|
|
// same bytes compiled at KSEG1 addresses charge: a fetch per instruction.
|
|
const auto k1_tags = constants(generate(KSEG0, KSEG1, mode),
|
|
R"(psx_icache_fetch\(cpu, 0x([0-9A-F]{8})u\))");
|
|
const auto k1_image_tags = constants(k1_image,
|
|
R"(psx_icache_fetch\(cpu, 0x([0-9A-F]{8})u\))");
|
|
check(k1_tags == k1_image_tags,
|
|
m + ": a KSEG1 code segment does not charge like a KSEG1 image");
|
|
std::set<uint32_t> charged(k1_tags.begin(), k1_tags.end());
|
|
for (uint32_t i = 0; i < kCount; ++i) {
|
|
if (kNoFetch.count(i * 4u)) continue;
|
|
check(charged.count(KSEG1 | (kPhys + i * 4u)) == 1,
|
|
m + ": KSEG1 instruction +" + hex(i * 4u) + " has no fetch of its own");
|
|
}
|
|
const auto k0_tags = constants(def, R"(psx_icache_fetch\(cpu, 0x([0-9A-F]{8})u\))");
|
|
check(k0_tags.size() < k1_tags.size(),
|
|
m + ": the cached KSEG0 body lost the line-leader rule");
|
|
}
|
|
|
|
void check_strict_translator(bool strict_cps) {
|
|
// SCPH-1001 Kernel Part 2 runs at 0x500 from ROM 0xBFC10000: the store at
|
|
// ROM 0xBFC10A00 executes at 0x00000F00 (memory.c's RAM-0 filter key).
|
|
const uint32_t rom = 0xBFC10A00u, rt_pc = 0x00000F00u;
|
|
struct Case {
|
|
uint32_t raw;
|
|
const char* want; // substring that must carry the runtime PC
|
|
};
|
|
// The syscall form is latched from PSX_CPS when the process starts.
|
|
const char* syscall_want = strict_cps
|
|
? "cpu->pc = 0x00000F00u; if (psx_syscall(cpu, cpu->gpr[2])) return;"
|
|
: "cpu->pc = 0x00000F00u; psx_syscall(cpu, cpu->gpr[2]); return;";
|
|
const Case cases[] = {
|
|
{0xAC880000u, "g_debug_last_store_pc = 0x00000F00u"}, // sw
|
|
{0xA4880000u, "psx_unaligned_access(cpu, psx_addr, 0x00000F00u)"}, // sh
|
|
{0xA0880000u, "g_debug_last_store_pc = 0x00000F00u"}, // sb
|
|
{0xA8880000u, "g_debug_last_store_pc = 0x00000F00u"}, // swl
|
|
{0xB8880000u, "g_debug_last_store_pc = 0x00000F00u"}, // swr
|
|
{0xE8880000u, "g_debug_last_store_pc = 0x00000F00u"}, // swc2
|
|
{0x8C880000u, "psx_unaligned_access(cpu, psx_addr, 0x00000F00u)"}, // lw
|
|
{0x84880000u, "psx_unaligned_access(cpu, psx_addr, 0x00000F00u)"}, // lh
|
|
{0x94880000u, "psx_unaligned_access(cpu, psx_addr, 0x00000F00u)"}, // lhu
|
|
{0x0000000Cu, syscall_want}, // syscall
|
|
{0x0000000Du, "0x00000F00u); return;"}, // break
|
|
};
|
|
for (const Case& c : cases) {
|
|
const auto d = PSXRecomp::MipsDecoder::decode(c.raw, rom);
|
|
const auto tr = PSXRecompV4::StrictTranslator::translate(d, rt_pc);
|
|
check(tr.supported, "translator: 0x" + hex(c.raw) + " unsupported");
|
|
check(tr.c_code.find(c.want) != std::string::npos,
|
|
"translator: 0x" + hex(c.raw) + " lacks \"" + c.want + "\": " + tr.c_code);
|
|
check(tr.c_code.find("0xBFC10A00u") == std::string::npos,
|
|
"translator: 0x" + hex(c.raw) + " still bakes the ROM address: " + tr.c_code);
|
|
// The one-argument form is the in-place case (runtime == ROM).
|
|
const auto same = PSXRecompV4::StrictTranslator::translate(d);
|
|
check(same.c_code == PSXRecompV4::StrictTranslator::translate(d, rom).c_code,
|
|
"translator: translate(d) != translate(d, d.address) for 0x" + hex(c.raw));
|
|
// A deferred load's temporary is an identity key: it keeps the ROM
|
|
// address, while its unaligned-access PC is the runtime PC.
|
|
if (!tr.c_code_deferred.empty())
|
|
check(tr.c_code_deferred.find("psx_ldd_BFC10A00") != std::string::npos &&
|
|
tr.c_code_deferred.find("psx_unaligned_access(cpu, psx_addr, 0x00000F00u)") !=
|
|
std::string::npos &&
|
|
tr.c_code_deferred.find("0xBFC10A00u") == std::string::npos,
|
|
"translator: deferred 0x" + hex(c.raw) +
|
|
" must name psx_ldd_<ROM> and fault at the runtime PC: " +
|
|
tr.c_code_deferred);
|
|
}
|
|
int deferred = 0;
|
|
for (uint32_t raw : {0x8C880000u, 0x84880000u, 0x94880000u})
|
|
deferred += !PSXRecompV4::StrictTranslator::translate(
|
|
PSXRecomp::MipsDecoder::decode(raw, rom), rt_pc)
|
|
.c_code_deferred.empty();
|
|
check(deferred == 3, "translator: lw/lh/lhu no longer emit a deferred form");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
// strict_translator.cpp latches PSX_CPS at static initialisation, before
|
|
// check_game_emitter() starts setting it per mode.
|
|
const char* cps_env = std::getenv("PSX_CPS");
|
|
const bool strict_cps = cps_env == nullptr || cps_env[0] != '0';
|
|
const char* mode = std::getenv("PSX_CODEGEN_CYCLE_PER_INSN");
|
|
if (mode != nullptr && mode[0] == '0') {
|
|
std::puts("SKIP: PSX_CODEGEN_CYCLE_PER_INSN=0 emits no fetch charges");
|
|
return 0;
|
|
}
|
|
check_game_emitter(Mode::Cps);
|
|
check_game_emitter(Mode::CpsOverlay);
|
|
check_game_emitter(Mode::Legacy);
|
|
check_strict_translator(strict_cps);
|
|
if (failures != 0) {
|
|
std::fprintf(stderr, "emitter_runtime_pc_test: %d failure(s)\n", failures);
|
|
return 1;
|
|
}
|
|
std::puts("PASS: every baked guest PC follows the code segment; identity keys and "
|
|
"KSEG0 output unchanged");
|
|
return 0;
|
|
}
|