mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Merge pull request #647: guarded mod immediates fixed until restart
Guarded mod immediates fixed until restart
This commit is contained in:
@@ -1054,6 +1054,28 @@ re-enter guest execution, because pending branch/load state is live.
|
||||
do not change guest behavior. Function entry replacement remains the separate
|
||||
`psx_mod_register_function_entry_plugin` interface.
|
||||
|
||||
### Guarded immediate selectors
|
||||
|
||||
A plugin that must change a constant, offset or address inside compiled game
|
||||
code (a capacity compare, a relocated table) lists the instruction under
|
||||
`[mod_immediate]` in `game.toml` and picks the variant with
|
||||
`psx_mod_set_immediate_selector(group, selector)` from its activation
|
||||
callback. The instruction bytes stay original, so static AOT code, cached
|
||||
overlays and the interpreter keep matching their guards and run the same
|
||||
variant. Selection is accepted only during offline activation, then sealed until
|
||||
restart; later attempts, including switching back to selector 0, are refused.
|
||||
Every new session starts at selector 0 (the stock instruction). Nonzero online
|
||||
activation is refused and invalidates the launch even if a plugin ignores the
|
||||
setter result. This capability does not negotiate online variants.
|
||||
|
||||
Non-stock frozen selections join the existing save-state content scope, including
|
||||
for packages declaring shared saves. A state made with a different selection is
|
||||
outside the session's slot namespace; the chosen values do not rewind or switch
|
||||
mid-session. Memory-card paths/formats are unchanged by this selector policy. Existing
|
||||
unscoped states are not automatically assigned a non-stock selection: they do
|
||||
not record which variant created them. Config rules and the emitted switch: docs/config_schema.md "Mod
|
||||
immediates".
|
||||
|
||||
### Presentation bezel packages
|
||||
|
||||
The framework registers `psx.bezel`, a trusted presentation plugin for OpenGL
|
||||
|
||||
@@ -447,6 +447,49 @@ max = 0x1BF
|
||||
- Regenerate after changing the list. With no sites the generated code is
|
||||
unchanged.
|
||||
|
||||
## Mod immediates (`[mod_immediate]`)
|
||||
|
||||
A trusted mod sometimes needs the same compiled instruction to behave
|
||||
differently from one session to the next without changing the guest bytes
|
||||
(a capacity compared against `slti v0, a0, 32`, a table address formed by
|
||||
`addiu a1, a1, 0x6988`, a struct offset in `lbu v0, 0x2E9(v1)`). Patching RAM
|
||||
fails for statically compiled code: every function carries a CRC of its
|
||||
original bytes and a patched function drops to the interpreter. Listing the
|
||||
instruction as a mod immediate site keeps the bytes and the CRC, and moves the
|
||||
choice into the generated code:
|
||||
|
||||
```toml
|
||||
[mod_immediate]
|
||||
sites = [
|
||||
{ address = "0x80023500", expected = "0x28820020", group = 1, values = [0x40, 0x80] }, # slti v0, a0, 0x20
|
||||
{ address = "0x80023B14", expected = "0x24A5AEB1", group = 1, values = [0xAEB1, 0x9000] },
|
||||
]
|
||||
```
|
||||
|
||||
- `expected` is the complete word at `address` and must be an I-type ALU
|
||||
instruction (`ADDI`..`LUI`, opcodes 0x08-0x0F) or a byte/half/word load or
|
||||
store (`LB`..`SW`, 0x20-0x2B). Branches, jumps and R-type instructions are
|
||||
refused. `group` is 0..63; `values` lists up to 15 replacement 16-bit
|
||||
immediates (decimal, hex, or negative), selector 1 first.
|
||||
- Selector 0 is the original instruction. A trusted plugin selects variant N of
|
||||
every site in a group with `psx_mod_set_immediate_selector(group, N)` from its
|
||||
activation callback; the selector is 0 again at every session start, so an
|
||||
inactive mod, a disabled feature and netplay all run the stock code.
|
||||
- Generated code emits `switch (psx_mod_immediate_select(group))` with one case
|
||||
per value, each translated exactly like the instruction it stands for
|
||||
(cycle accounting, PGXP hooks, load-delay handling), plus the original as the
|
||||
default. The dirty-RAM interpreter substitutes the same word
|
||||
(`psx_mod_immediate_patch`), so native and interpreted execution agree. The
|
||||
forwarder is part of the overlay ABI (v31).
|
||||
- A site is keyed by physical address and word. Overlays share load addresses,
|
||||
so one address may carry several sites with different words; a variant whose
|
||||
word differs is left alone in overlay mode and is an error in a main EXE.
|
||||
- The sites enter the overlay-cache identity (the hash changes only when the list
|
||||
is non-empty). Regenerate after changing the list; with no sites the generated
|
||||
code is unchanged.
|
||||
- The mod owns the consequences: a relocated table needs its own free memory and
|
||||
a save written under one selector is not interchangeable with another's.
|
||||
|
||||
## Runtime block
|
||||
|
||||
Consumed by the cmake macro `psxrecomp_v4_add_runtime_target` (eventually)
|
||||
@@ -1226,3 +1269,7 @@ ranges. A BIOS with no copies (runs
|
||||
entirely from ROM) is valid: normalization degenerates to the KSEG mask.
|
||||
Semantic invariants (disjoint windows, no fold-output/input intersection,
|
||||
single bless window) are enforced at load; violations refuse to build.
|
||||
|
||||
Immediate selector choices are fixed during offline mod activation until the
|
||||
next session restart. Non-stock choices isolate savestate slots through the
|
||||
existing content scope; live setter calls and nonzero online selection fail.
|
||||
|
||||
@@ -126,8 +126,9 @@
|
||||
* already holds every release-line callback; shards built against either
|
||||
* v28 or v29 are rejected and recompiled.
|
||||
* v31/v32: reserved for the experimental selector/adapter stack.
|
||||
* v33: guarded load/store effective-address adapter on the master layout. */
|
||||
#define PSX_OVERLAY_ABI_VERSION 33
|
||||
* v33: guarded load/store effective-address adapter on the master layout.
|
||||
* v34: frozen, activation-only immediate selectors on the v33 layout. */
|
||||
#define PSX_OVERLAY_ABI_VERSION 34
|
||||
|
||||
/* Process-lifetime overlay candidate capacity. Every accepted manifest F
|
||||
* record consumes one slot, even when another DLL carries an identical
|
||||
@@ -391,6 +392,8 @@ typedef struct OverlayCallbacks {
|
||||
void (*mod_instruction)(CPUState*, uint32_t, uint32_t);
|
||||
/* Guarded load/store effective-address adapters (ABI v33). */
|
||||
uint32_t (*mod_memory_address)(const CPUState*, uint32_t, uint32_t, uint32_t);
|
||||
/* [[mod_immediate.sites]] selector of a group (ABI v34); NULL = stock. */
|
||||
uint32_t (*mod_immediate_select)(uint32_t group);
|
||||
} OverlayCallbacks;
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -270,6 +270,9 @@ uint32_t psx_mod_memory_address(const CPUState *cpu, uint32_t pc, uint32_t word,
|
||||
overlay_flush_cycles();
|
||||
return g_cbs.mod_memory_address ? g_cbs.mod_memory_address(cpu, pc, word, address) : address;
|
||||
}
|
||||
uint32_t psx_mod_immediate_select(uint32_t group) {
|
||||
return g_cbs.mod_immediate_select ? g_cbs.mod_immediate_select(group) : 0u;
|
||||
}
|
||||
int psx_mod_function_entry(CPUState *cpu, uint32_t address) {
|
||||
overlay_flush_cycles();
|
||||
return g_cbs.mod_function_entry ? g_cbs.mod_function_entry(cpu, address) : 0;
|
||||
|
||||
@@ -596,6 +596,35 @@ if(BUILD_TESTING)
|
||||
add_test(NAME draw_distance_codegen_test
|
||||
COMMAND draw_distance_codegen_test)
|
||||
|
||||
# [[mod_immediate.sites]]: config rules, the per-selector switch emitted
|
||||
# for loads, stores, ALU immediates and delay slots, the guards and the
|
||||
# overlay-cache identity.
|
||||
add_executable(mod_immediate_codegen_test
|
||||
tests/mod_immediate_codegen_test.cpp
|
||||
src/ps1_exe_parser.cpp
|
||||
src/mips_decoder.cpp
|
||||
src/control_flow.cpp
|
||||
src/function_analysis.cpp
|
||||
src/code_generator.cpp
|
||||
src/pgxp_hook_emitter.cpp
|
||||
src/annotations.cpp
|
||||
src/bios_address_model.cpp
|
||||
src/config_loader.cpp
|
||||
src/recompiler_patch.cpp
|
||||
)
|
||||
target_include_directories(mod_immediate_codegen_test PRIVATE
|
||||
include
|
||||
src
|
||||
lib/rabbitizer/include
|
||||
lib/rabbitizer/cplusplus/include
|
||||
lib/fmt/include
|
||||
lib/toml11
|
||||
lib/ELFIO
|
||||
)
|
||||
target_link_libraries(mod_immediate_codegen_test PRIVATE fmt rabbitizer)
|
||||
add_test(NAME mod_immediate_codegen_test
|
||||
COMMAND mod_immediate_codegen_test)
|
||||
|
||||
# The same explicit cull kinds in the dirty-RAM interpreter: gpu.c's
|
||||
# sorted, capped site store and real interpreted bgez/bltz/branch-keep/
|
||||
# clip-edge instructions at margin 0 and at a forced margin. The test
|
||||
@@ -652,6 +681,18 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_BINARY_DIR}/draw_distance_interp_stderr.txt)
|
||||
endif()
|
||||
|
||||
# [[mod_immediate.sites]] runtime store, selectors and word substitution
|
||||
# (mod_immediate.c). The interpreter call site is pinned by
|
||||
# runtime/tests/test_interp_mod_immediate_guards.py.
|
||||
add_executable(mod_immediate_unit_test
|
||||
../runtime/tests/test_mod_immediate.c)
|
||||
target_include_directories(mod_immediate_unit_test PRIVATE
|
||||
../runtime/include
|
||||
../runtime/src)
|
||||
add_test(NAME mod_immediate_unit_test
|
||||
COMMAND mod_immediate_unit_test
|
||||
${CMAKE_CURRENT_BINARY_DIR}/mod_immediate_unit_stderr.txt)
|
||||
|
||||
# Uncached (KSEG1) code charges a fetch before every instruction; cached
|
||||
# code keeps the line-leader rule (test_uncached_fetch_charge.py checks
|
||||
# the BIOS emitter on OpenBIOS and the cycle model).
|
||||
|
||||
@@ -67,6 +67,9 @@ struct CodeGenConfig {
|
||||
// Only explicitly listed guest functions call the runtime dispatcher.
|
||||
std::set<uint32_t> mod_function_entry_funcs;
|
||||
std::set<uint32_t> mod_instruction_sites;
|
||||
// [[mod_immediate.sites]]: guarded instructions whose immediate a trusted mod
|
||||
// selects at run time; emitted as a selector switch over every variant.
|
||||
std::vector<PSXRecompV4::ModImmediateSite> mod_immediate_sites;
|
||||
|
||||
// [recompiler] hot_funcs: emit __attribute__((hot)) on these guest
|
||||
// addresses (MotK VLC leaves, etc.). Host locality hint only.
|
||||
@@ -495,6 +498,14 @@ private:
|
||||
// The [[draw_distance.clamp]] statement for (addr, instr), or "".
|
||||
std::string draw_distance_clamp_prefix(uint32_t addr, uint32_t instr) const;
|
||||
std::string mod_instruction_call(uint32_t addr, uint32_t instr) const;
|
||||
// The [[mod_immediate.sites]] entry for (addr, instr), or nullptr. In a main
|
||||
// EXE a word mismatch at a listed address is a hard error; overlay code may
|
||||
// hold unrelated words at a shared load address and is left unchanged.
|
||||
const PSXRecompV4::ModImmediateSite* find_mod_immediate_site(uint32_t addr, uint32_t instr) const;
|
||||
// The instruction translated once per selector: `switch (psx_mod_immediate_select(group))`
|
||||
// with case N running the word whose imm16 is values[N-1] and default the original.
|
||||
std::string mod_immediate_translation(uint32_t addr, uint32_t instr,
|
||||
const PSXRecompV4::ModImmediateSite& site);
|
||||
|
||||
// Register name mapping
|
||||
static std::string reg_name(int reg_num);
|
||||
|
||||
@@ -921,6 +921,42 @@ std::string CodeGenerator::mod_instruction_call(uint32_t addr, uint32_t instr) c
|
||||
});
|
||||
return selected ? fmt::format("psx_mod_instruction(cpu, 0x{:08X}u, 0x{:08X}u);\n", addr, instr) : "";
|
||||
}
|
||||
const PSXRecompV4::ModImmediateSite* CodeGenerator::find_mod_immediate_site(
|
||||
uint32_t addr, uint32_t instr) const {
|
||||
const uint32_t phys = addr & 0x1FFFFFFFu;
|
||||
bool address_listed = false;
|
||||
for (const auto& site : config_.mod_immediate_sites) {
|
||||
if ((site.address & 0x1FFFFFFFu) != phys) continue;
|
||||
address_listed = true;
|
||||
if (site.expected == instr) return &site;
|
||||
}
|
||||
if (address_listed && !config_.overlay_mode) {
|
||||
fmt::print(stderr,
|
||||
"ERROR: [[mod_immediate.sites]] expected word does not match "
|
||||
"0x{:08X} at 0x{:08X}\n", instr, addr);
|
||||
std::exit(1);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string CodeGenerator::mod_immediate_translation(
|
||||
uint32_t addr, uint32_t instr, const PSXRecompV4::ModImmediateSite& site) {
|
||||
const std::string& ind = config_.indent;
|
||||
std::string out = ind + fmt::format(
|
||||
"switch (psx_mod_immediate_select({}u)) {{ /* mod immediate 0x{:08X} */\n",
|
||||
site.group, addr);
|
||||
for (size_t i = 0; i < site.values.size(); ++i) {
|
||||
const uint32_t word = (instr & 0xFFFF0000u) | site.values[i];
|
||||
out += ind + fmt::format("case {}u: {{ /* 0x{:08X} */\n", i + 1u, word);
|
||||
out += translate_instruction_core(addr, word, "", instr) + "\n";
|
||||
out += ind + " break;\n" + ind + "}\n";
|
||||
}
|
||||
out += ind + "default: {\n";
|
||||
out += translate_instruction_core(addr, instr) + "\n";
|
||||
out += ind + " break;\n" + ind + "}\n" + ind + "}";
|
||||
return out;
|
||||
}
|
||||
|
||||
// [[draw_distance.clamp]]: a main-EXE ordering-table range guard. While a
|
||||
// mod has the clamps on, the site's register is clamped to `max` before the
|
||||
// instruction runs, so the guard keeps a far primitive in the last OT slot
|
||||
@@ -962,9 +998,12 @@ std::string CodeGenerator::draw_distance_clamp_prefix(uint32_t addr,
|
||||
std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) {
|
||||
std::string clamp = draw_distance_clamp_prefix(addr, instr);
|
||||
const std::string hook = mod_instruction_call(addr, instr);
|
||||
if (clamp.empty()) return hook + translate_instruction_core(addr, instr);
|
||||
const PSXRecompV4::ModImmediateSite* imm_site = find_mod_immediate_site(addr, instr);
|
||||
const std::string body = imm_site ? mod_immediate_translation(addr, instr, *imm_site)
|
||||
: translate_instruction_core(addr, instr);
|
||||
if (clamp.empty()) return hook + body;
|
||||
// The core translation carries its own indent; give the clamp the same.
|
||||
return hook + config_.indent + clamp + "\n" + translate_instruction_core(addr, instr);
|
||||
return hook + config_.indent + clamp + "\n" + body;
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t instr,
|
||||
@@ -2256,6 +2295,10 @@ std::string CodeGenerator::translate_basic_block(
|
||||
if (defer_load) {
|
||||
const std::string lhs = fmt::format("cpu->gpr[{}] =", load_dest);
|
||||
const size_t pos = emitted.find(lhs);
|
||||
// A [[mod_immediate.sites]] load carries one translation per
|
||||
// selector; every copy must write the deferred temporary.
|
||||
const bool per_selector =
|
||||
emitted.find("psx_mod_immediate_select(") != std::string::npos;
|
||||
if (pos != std::string::npos) {
|
||||
const std::string temp = fmt::format("psx_ldd_{:08X}", addr);
|
||||
// Declare the temp in the pair block, not inline at the
|
||||
@@ -2272,6 +2315,16 @@ std::string CodeGenerator::translate_basic_block(
|
||||
if (hook != std::string::npos)
|
||||
emitted.replace(hook, pgxp_tail.size(),
|
||||
fmt::format(", _pgxa, {});", temp));
|
||||
if (per_selector) {
|
||||
// The remaining selector copies, same two rewrites.
|
||||
for (size_t at = emitted.find(lhs); at != std::string::npos;
|
||||
at = emitted.find(lhs, at + temp.size()))
|
||||
emitted.replace(at, lhs.size(), temp + " =");
|
||||
for (size_t at = emitted.find(pgxp_tail); at != std::string::npos;
|
||||
at = emitted.find(pgxp_tail, at + 1))
|
||||
emitted.replace(at, pgxp_tail.size(),
|
||||
fmt::format(", _pgxa, {});", temp));
|
||||
}
|
||||
ss << config_.indent << "{ /* MIPS-I load-delay pair */\n";
|
||||
ss << config_.indent
|
||||
<< fmt::format(" uint32_t {} = 0;\n", temp);
|
||||
@@ -3622,6 +3675,8 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
|
||||
ss << "extern int psx_datashard_enter(CPUState* cpu, uint32_t key); /* data-shard replay/capture (data_shards.c) */\n";
|
||||
ss << "extern void psx_mod_instruction(CPUState*, uint32_t, uint32_t);\n";
|
||||
ss << "extern uint32_t psx_mod_memory_address(const CPUState*, uint32_t, uint32_t, uint32_t);\n";
|
||||
if (!config_.mod_immediate_sites.empty())
|
||||
ss << "extern uint32_t psx_mod_immediate_select(uint32_t group); /* [[mod_immediate.sites]] selector (mod_immediate.c) */\n";
|
||||
ss << "extern int psx_mod_function_entry(CPUState* cpu, uint32_t address); /* trusted opt-in game-mod hook */\n";
|
||||
ss << "extern int psx_ws_masked_reject(uint32_t flags, uint32_t mask); /* guarded packed-coordinate reject */\n";
|
||||
ss << "extern int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla);\n";
|
||||
|
||||
@@ -81,6 +81,23 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) {
|
||||
// does not use the feature keeps the hash (and every overlay cache) it had.
|
||||
if (!c.mod_instruction_sites.empty())
|
||||
h.words("mod_instruction_sites", c.mod_instruction_sites);
|
||||
if (!c.mod_immediate_sites.empty()) {
|
||||
// Appended only when used (same rule as above). Order-independent.
|
||||
auto sites = c.mod_immediate_sites;
|
||||
std::sort(sites.begin(), sites.end(), [](const auto& x, const auto& y) {
|
||||
const uint32_t ax = x.address & 0x1FFFFFFFu, ay = y.address & 0x1FFFFFFFu;
|
||||
return ax != ay ? ax < ay : x.expected < y.expected;
|
||||
});
|
||||
h.tag("mod_immediate_sites");
|
||||
h.u32((uint32_t)sites.size());
|
||||
for (const auto& site : sites) {
|
||||
h.u32(site.address & 0x1FFFFFFFu);
|
||||
h.u32(site.expected);
|
||||
h.u32(site.group);
|
||||
h.u32((uint32_t)site.values.size());
|
||||
for (const uint16_t v : site.values) h.u32(v);
|
||||
}
|
||||
}
|
||||
h.words("cull_bias", c.ws_cull_bias_sites);
|
||||
if (!c.ws_cull_bias_lower_sites.empty())
|
||||
h.words("cull_bias_lower", c.ws_cull_bias_lower_sites);
|
||||
@@ -1174,6 +1191,60 @@ bool draw_distance_clamp_reads(uint32_t instr, uint32_t reg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string mod_immediate_site_error(const ModImmediateSite& site) {
|
||||
if ((site.address & 3u) != 0u) return "address is not instruction-aligned";
|
||||
if ((site.address & 0x1FFFFFFFu) >= 0x00800000u || site.address >= 0xC0000000u)
|
||||
return "address is not main RAM";
|
||||
const uint32_t op = site.expected >> 26;
|
||||
const bool alu_imm = op >= 0x08u && op <= 0x0Fu;
|
||||
const bool load_store = op >= 0x20u && op <= 0x2Bu && op != 0x27u;
|
||||
if (!alu_imm && !load_store)
|
||||
return "expected is not an I-type ALU/load/store instruction "
|
||||
"(ADDI..LUI or LB..SW)";
|
||||
if (site.group >= 64u) return "group must be 0..63";
|
||||
if (site.values.empty()) return "values must list at least one immediate";
|
||||
if (site.values.size() > 15u) return "at most 15 values";
|
||||
return {};
|
||||
}
|
||||
|
||||
// [[mod_immediate.sites]] (docs/config_schema.md "Mod immediates").
|
||||
static std::vector<ModImmediateSite> parse_mod_immediate_sites(
|
||||
const toml::value& cfg, const fs::path& config_path) {
|
||||
std::vector<ModImmediateSite> sites;
|
||||
if (!cfg.contains("mod_immediate")) return sites;
|
||||
const toml::value& mi = toml::find(cfg, "mod_immediate");
|
||||
if (!mi.contains("sites")) return sites;
|
||||
std::set<std::pair<uint32_t, uint32_t>> seen;
|
||||
for (const auto& item : toml::find<toml::array>(mi, "sites")) {
|
||||
ModImmediateSite site;
|
||||
site.address = parse_hex(toml::find<std::string>(item, "address"),
|
||||
"mod_immediate.sites.address");
|
||||
site.expected = parse_hex(toml::find<std::string>(item, "expected"),
|
||||
"mod_immediate.sites.expected");
|
||||
site.group = (uint32_t)toml::find<int64_t>(item, "group");
|
||||
for (const auto& v : toml::find<toml::array>(item, "values")) {
|
||||
const int64_t n = v.as_integer();
|
||||
if (n < -32768 || n > 65535)
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: mod_immediate site 0x{:08X}: value {} does not fit "
|
||||
"16 bits", config_path.string(), site.address, n));
|
||||
site.values.push_back((uint16_t)n);
|
||||
}
|
||||
if (const std::string why = mod_immediate_site_error(site); !why.empty())
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: mod_immediate site 0x{:08X}: {}", config_path.string(),
|
||||
site.address, why));
|
||||
// Overlays share load addresses, so one address may carry several
|
||||
// sites; only the (address, word) pair must be unique.
|
||||
if (!seen.insert({site.address & 0x1FFFFFFFu, site.expected}).second)
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: duplicate mod_immediate site 0x{:08X}",
|
||||
config_path.string(), site.address));
|
||||
sites.push_back(std::move(site));
|
||||
}
|
||||
return sites;
|
||||
}
|
||||
|
||||
// [[draw_distance.clamp]] (docs/config_schema.md "Draw-distance clamps").
|
||||
static std::vector<DrawDistanceClampSite> parse_draw_distance_clamps(
|
||||
const toml::value& cfg, const fs::path& config_path) {
|
||||
@@ -2876,6 +2947,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
loaded.draw_distance_clamp_sites =
|
||||
parse_draw_distance_clamps(cfg, config_path);
|
||||
loaded.quality_presets = parse_quality_presets(cfg, root, config_path);
|
||||
loaded.mod_immediate_sites = parse_mod_immediate_sites(cfg, config_path);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,27 @@ struct DrawDistanceClampSite {
|
||||
int32_t max = 0;
|
||||
};
|
||||
|
||||
// [[mod_immediate.sites]]: a guarded I-type instruction whose 16-bit immediate a
|
||||
// trusted mod may swap at run time. The complete word at `address` must equal
|
||||
// `expected` (an unrelated overlay word at the same address is left alone).
|
||||
// Selector 0 always runs `expected` unchanged; selector N (1-based) runs the
|
||||
// same instruction with `values[N-1]` in the immediate field. The selector
|
||||
// belongs to `group` (0..63) and is set by the mod
|
||||
// (psx_mod_set_immediate_selector). Because the instruction bytes in RAM never
|
||||
// change, static / cached native code keeps matching its original-bytes guard;
|
||||
// generated code, the interpreter and every overlay path pick the immediate
|
||||
// from the same selector. Only ADDI..LUI (0x08-0x0F) and the byte/half/word
|
||||
// loads and stores (0x20-0x2B) qualify.
|
||||
struct ModImmediateSite {
|
||||
uint32_t address = 0;
|
||||
uint32_t expected = 0;
|
||||
uint32_t group = 0;
|
||||
std::vector<uint16_t> values; // imm16 per selector 1..N
|
||||
};
|
||||
|
||||
// Empty on success; otherwise why `site` cannot be a mod-immediate site.
|
||||
std::string mod_immediate_site_error(const ModImmediateSite& site);
|
||||
|
||||
// True when `instr` is an ALU instruction (I-type or SPECIAL R-type, not a
|
||||
// load, store, branch or jump) that reads GPR `reg`. These are the only
|
||||
// instructions a draw-distance clamp may precede.
|
||||
@@ -1502,6 +1523,8 @@ struct GameConfig {
|
||||
// [quality.low/medium/high/ultra] -- graphics presets, lowest first. Empty:
|
||||
// the title has no presets and [video] alone applies (unchanged).
|
||||
std::vector<QualityPreset> quality_presets;
|
||||
// [[mod_immediate.sites]]: run-time-selectable immediates (ModImmediateSite).
|
||||
std::vector<ModImmediateSite> mod_immediate_sites;
|
||||
};
|
||||
|
||||
// Effective clip_edge_width: explicit value, else screen_w_imms[0], else 320.
|
||||
|
||||
@@ -220,6 +220,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
std::set<uint32_t> ws_tag_funcs; // [widescreen] sprite_tag_funcs
|
||||
std::set<uint32_t> ds_funcs; // [data_shards] funcs
|
||||
std::set<uint32_t> mod_instruction_sites;
|
||||
std::vector<PSXRecompV4::ModImmediateSite> mod_immediate_sites; // [[mod_immediate.sites]]
|
||||
std::set<uint32_t> mod_entry_funcs; // trusted game-mod entry hooks
|
||||
std::vector<PSXRecompV4::WidescreenMaskedRejectSite> ws_cull_masked_reject;
|
||||
std::set<uint32_t> hot_funcs; // [recompiler] hot_funcs
|
||||
@@ -289,6 +290,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
mod_entry_funcs.insert(cfg.mod_function_entry_funcs.begin(),
|
||||
cfg.mod_function_entry_funcs.end());
|
||||
mod_instruction_sites.insert(cfg.mod_instruction_sites.begin(), cfg.mod_instruction_sites.end());
|
||||
mod_immediate_sites = cfg.mod_immediate_sites;
|
||||
hot_funcs.insert(cfg.hot_funcs.begin(), cfg.hot_funcs.end());
|
||||
if (cfg.load_charge_batch) {
|
||||
load_charge_batch_funcs.insert(cfg.load_charge_batch_funcs.begin(),
|
||||
@@ -407,6 +409,15 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
mod_entry_funcs.insert(wscfg.mod_function_entry_funcs.begin(),
|
||||
wscfg.mod_function_entry_funcs.end());
|
||||
mod_instruction_sites.insert(wscfg.mod_instruction_sites.begin(), wscfg.mod_instruction_sites.end());
|
||||
// Same TOML as --config adds nothing; a distinct one adds its own sites.
|
||||
for (const auto& site : wscfg.mod_immediate_sites) {
|
||||
const bool known = std::any_of(mod_immediate_sites.begin(), mod_immediate_sites.end(),
|
||||
[&](const PSXRecompV4::ModImmediateSite& have) {
|
||||
return (have.address & 0x1FFFFFFFu) == (site.address & 0x1FFFFFFFu) &&
|
||||
have.expected == site.expected;
|
||||
});
|
||||
if (!known) mod_immediate_sites.push_back(site);
|
||||
}
|
||||
ws_cull_bias.insert(wscfg.ws_cull_bias_sites.begin(), wscfg.ws_cull_bias_sites.end());
|
||||
ws_cull_bias_lower.insert(wscfg.ws_cull_bias_lower_sites.begin(), wscfg.ws_cull_bias_lower_sites.end());
|
||||
ws_cull_range.insert(wscfg.ws_cull_range_sites.begin(), wscfg.ws_cull_range_sites.end());
|
||||
@@ -1508,6 +1519,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
codegen_config.mod_function_entry_funcs = mod_entry_funcs;
|
||||
codegen_config.ws_cull_masked_reject_sites = ws_cull_masked_reject;
|
||||
codegen_config.mod_instruction_sites = mod_instruction_sites;
|
||||
codegen_config.mod_immediate_sites = mod_immediate_sites;
|
||||
codegen_config.hot_funcs = hot_funcs;
|
||||
codegen_config.load_charge_batch_funcs = load_charge_batch_funcs;
|
||||
codegen_config.vsync_query_hle_funcs = vsync_query_hle_funcs;
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
// [[mod_immediate.sites]]: config parsing and validation, the per-selector
|
||||
// switch emitted for a listed instruction (ALU immediate, load, store, branch
|
||||
// delay slot, deferred load), main-EXE vs overlay guard behaviour, the shared
|
||||
// declaration and the overlay-cache identity.
|
||||
#include "code_generator.h"
|
||||
#include "config_loader.h"
|
||||
#include "control_flow.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#else
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kBase = 0x80010000u;
|
||||
int failures = 0;
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "FAIL: %s\n", message);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
void append_word(std::vector<uint8_t>& bytes, uint32_t word) {
|
||||
for (int i = 0; i < 4; i++) bytes.push_back(static_cast<uint8_t>(word >> (8 * i)));
|
||||
}
|
||||
|
||||
size_t count_of(const std::string& hay, const std::string& needle) {
|
||||
size_t n = 0;
|
||||
for (size_t at = hay.find(needle); at != std::string::npos;
|
||||
at = hay.find(needle, at + needle.size()))
|
||||
++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
fs::path write_temp_config(const char* stem, const std::string& body) {
|
||||
const auto nonce = std::chrono::high_resolution_clock::now().time_since_epoch().count();
|
||||
fs::path path = fs::temp_directory_path() /
|
||||
(std::string(stem) + "-" + std::to_string(nonce) + ".toml");
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
out << body;
|
||||
return path;
|
||||
}
|
||||
|
||||
std::string base_config() {
|
||||
return R"toml([game]
|
||||
name = "Mod Immediate Test"
|
||||
id = "TEST-00000"
|
||||
exe = "TEST.EXE"
|
||||
load_address = "0x80010000"
|
||||
entry_pc = "0x80010000"
|
||||
text_size = "0x1000"
|
||||
stack_base = "0x801FFFF0"
|
||||
|
||||
[recompiler]
|
||||
seeds = "seeds.txt"
|
||||
out_dir = "generated"
|
||||
)toml";
|
||||
}
|
||||
|
||||
std::string sites_block(std::initializer_list<std::string> entries) {
|
||||
std::string out = "\n[mod_immediate]\nsites = [\n";
|
||||
for (const auto& e : entries) out += " " + e + ",\n";
|
||||
return out + "]\n";
|
||||
}
|
||||
|
||||
std::string site(const char* address, const char* expected, int group,
|
||||
const char* values) {
|
||||
return std::string("{ address = \"") + address + "\", expected = \"" + expected +
|
||||
"\", group = " + std::to_string(group) + ", values = [" + values + "] }";
|
||||
}
|
||||
|
||||
PSXRecompV4::GameConfig load(const std::string& body) {
|
||||
fs::path path = write_temp_config("mod-immediate", body);
|
||||
auto config = PSXRecompV4::load_game_config(path);
|
||||
fs::remove(path);
|
||||
return config;
|
||||
}
|
||||
|
||||
bool load_throws_with(const std::string& body, const char* needle) {
|
||||
fs::path path = write_temp_config("mod-immediate-bad", body);
|
||||
bool matched = false;
|
||||
try {
|
||||
(void)PSXRecompV4::load_game_config(path);
|
||||
} catch (const std::exception& e) {
|
||||
matched = std::string(e.what()).find(needle) != std::string::npos;
|
||||
if (!matched) std::fprintf(stderr, " (threw: %s)\n", e.what());
|
||||
}
|
||||
fs::remove(path);
|
||||
return matched;
|
||||
}
|
||||
|
||||
PSXRecomp::GeneratedFunction generate(std::initializer_list<uint32_t> words,
|
||||
const PSXRecomp::CodeGenConfig& config) {
|
||||
PSXRecomp::PS1Executable exe{};
|
||||
exe.header.load_address = kBase;
|
||||
exe.header.initial_pc = kBase;
|
||||
for (uint32_t w : words) append_word(exe.code_data, w);
|
||||
append_word(exe.code_data, 0x03E00008u);
|
||||
append_word(exe.code_data, 0x00000000u);
|
||||
const uint32_t size = static_cast<uint32_t>(exe.code_data.size());
|
||||
exe.header.file_size = size;
|
||||
PSXRecomp::Function function{};
|
||||
function.start_addr = kBase;
|
||||
function.end_addr = kBase + size;
|
||||
function.size = size;
|
||||
function.name = "imm_site_test";
|
||||
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
|
||||
const auto cfg = analyzer.analyze_function(function);
|
||||
PSXRecomp::CodeGenerator generator(exe, config);
|
||||
return generator.generate_function(function, cfg);
|
||||
}
|
||||
|
||||
// Windows has no fork: the Nth probe re-runs this binary as `--exit-probe N`.
|
||||
const char* g_self = nullptr;
|
||||
int g_exit_probe = -1;
|
||||
int g_probe_seq = 0;
|
||||
|
||||
template <class F>
|
||||
bool exits_with_failure(F&& body) {
|
||||
#if defined(_WIN32)
|
||||
const int seq = g_probe_seq++;
|
||||
if (g_exit_probe >= 0) {
|
||||
if (seq != g_exit_probe) return false;
|
||||
body();
|
||||
std::fflush(nullptr);
|
||||
std::_Exit(0);
|
||||
}
|
||||
std::fflush(nullptr);
|
||||
const std::string quoted = std::string("\"") + g_self + "\"";
|
||||
const std::string n = std::to_string(seq);
|
||||
const intptr_t rc = _spawnl(_P_WAIT, g_self, quoted.c_str(), "--exit-probe", n.c_str(), nullptr);
|
||||
return rc > 0;
|
||||
#else
|
||||
(void)g_probe_seq;
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
if (!std::freopen("/dev/null", "w", stderr)) _exit(3);
|
||||
body();
|
||||
_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (pid < 0 || waitpid(pid, &status, 0) != pid) return false;
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr uint32_t kSltiV0V0_32 = 0x28420020u; // slti v0, v0, 0x20
|
||||
constexpr uint32_t kAddiuA0V1 = 0x246402E9u; // addiu a0, v1, 0x2e9
|
||||
constexpr uint32_t kLbuV0V1 = 0x906202E9u; // lbu v0, 0x2e9(v1)
|
||||
constexpr uint32_t kSbA1V1 = 0xA0650308u; // sb a1, 0x308(v1)
|
||||
constexpr uint32_t kUseV0 = 0x00423021u; // addu a2, v0, v0 (reads v0, writes another)
|
||||
constexpr uint32_t kBeqZero = 0x10000002u; // b +2
|
||||
constexpr uint32_t kNop = 0x00000000u;
|
||||
|
||||
PSXRecomp::CodeGenConfig with_sites(std::vector<PSXRecompV4::ModImmediateSite> sites,
|
||||
bool overlay = false) {
|
||||
PSXRecomp::CodeGenConfig config{};
|
||||
config.mod_immediate_sites = std::move(sites);
|
||||
config.overlay_mode = overlay;
|
||||
return config;
|
||||
}
|
||||
|
||||
PSXRecompV4::ModImmediateSite make(uint32_t address, uint32_t expected, uint32_t group,
|
||||
std::vector<uint16_t> values) {
|
||||
PSXRecompV4::ModImmediateSite s;
|
||||
s.address = address;
|
||||
s.expected = expected;
|
||||
s.group = group;
|
||||
s.values = std::move(values);
|
||||
return s;
|
||||
}
|
||||
|
||||
void loader() {
|
||||
check(load(base_config()).mod_immediate_sites.empty(), "no sites by default");
|
||||
|
||||
const auto cfg = load(base_config() + sites_block({
|
||||
site("0x80023500", "0x28820020", 1, "64, 128"),
|
||||
site("0x8002336C", "0xA0650308", 1, "0x328, 0x368"),
|
||||
site("0x80023500", "0x28820021", 2, "-1"), // same address, other overlay word
|
||||
}));
|
||||
check(cfg.mod_immediate_sites.size() == 3, "three sites parsed");
|
||||
if (cfg.mod_immediate_sites.size() == 3) {
|
||||
const auto& a = cfg.mod_immediate_sites[0];
|
||||
check(a.address == 0x80023500u && a.expected == 0x28820020u && a.group == 1u &&
|
||||
a.values.size() == 2 && a.values[0] == 64 && a.values[1] == 128,
|
||||
"first site fields");
|
||||
check(cfg.mod_immediate_sites[1].values[1] == 0x368, "hex values");
|
||||
check(cfg.mod_immediate_sites[2].values[0] == 0xFFFF, "negative value wraps to imm16");
|
||||
}
|
||||
|
||||
check(load_throws_with(base_config() + sites_block({site("0x80023502", "0x28820020", 1, "64")}),
|
||||
"not instruction-aligned"), "unaligned address refused");
|
||||
check(load_throws_with(base_config() + sites_block({site("0x80023500", "0x10400002", 1, "64")}),
|
||||
"not an I-type"), "a branch is refused");
|
||||
check(load_throws_with(base_config() + sites_block({site("0x80023500", "0x0043102B", 1, "64")}),
|
||||
"not an I-type"), "an R-type is refused");
|
||||
check(load_throws_with(base_config() + sites_block({site("0x80023500", "0x28820020", 64, "64")}),
|
||||
"group must be 0..63"), "group 64 refused");
|
||||
check(load_throws_with(base_config() + sites_block({site("0x80023500", "0x28820020", 1, "")}),
|
||||
"at least one"), "empty values refused");
|
||||
check(load_throws_with(base_config() + sites_block({site("0x80023500", "0x28820020", 1, "70000")}),
|
||||
"does not fit 16 bits"), "value out of range refused");
|
||||
check(load_throws_with(base_config() + sites_block({
|
||||
site("0x80023500", "0x28820020", 1, "64"),
|
||||
site("0xA0023500", "0x28820020", 1, "128")}),
|
||||
"duplicate mod_immediate"), "duplicate (address, word) refused");
|
||||
check(load_throws_with(base_config() + sites_block({site("0x80800000", "0x28820020", 1, "64")}),
|
||||
"not main RAM"), "address past main RAM refused");
|
||||
}
|
||||
|
||||
void emission() {
|
||||
// slti: one case per value plus the stock default, each with its own immediate.
|
||||
const auto out = generate({kNop, kSltiV0V0_32, kNop},
|
||||
with_sites({make(kBase + 4u, kSltiV0V0_32, 1, {64, 128})})).full_code;
|
||||
check(out.find("switch (psx_mod_immediate_select(1u))") != std::string::npos,
|
||||
"switch on the group selector");
|
||||
check(count_of(out, "case 1u:") == 1 && count_of(out, "case 2u:") == 1 &&
|
||||
count_of(out, "default:") >= 1,
|
||||
"one case per value plus default");
|
||||
check(out.find("< 64") != std::string::npos && out.find("< 128") != std::string::npos &&
|
||||
out.find("< 32") != std::string::npos,
|
||||
"each variant compares against its own immediate");
|
||||
|
||||
// lbu: offsets differ per variant; deferred loads rewrite every copy.
|
||||
const auto load_out = generate({kNop, kLbuV0V1, kUseV0, kNop},
|
||||
with_sites({make(kBase + 4u, kLbuV0V1, 3, {0x2E9 + 0x100, 0x2E9 - 0x100})})).full_code;
|
||||
check(count_of(load_out, "switch (psx_mod_immediate_select(3u))") == 1, "load site switched");
|
||||
check(load_out.find("1001") != std::string::npos,
|
||||
"relocated load offset emitted (0x2E9+0x100 = 1001)");
|
||||
check(count_of(load_out, "psx_ldd_80010004") >= 4,
|
||||
"a deferred load writes the temporary in every selector copy");
|
||||
check(load_out.find("cpu->gpr[2] = psx_ldd_80010004;") != std::string::npos,
|
||||
"the deferred load is still written back");
|
||||
|
||||
// sb store + addiu pointer formation.
|
||||
const auto st = generate({kSbA1V1, kAddiuA0V1, kNop},
|
||||
with_sites({make(kBase, kSbA1V1, 4, {0x328}),
|
||||
make(kBase + 4u, kAddiuA0V1, 4, {0x400})})).full_code;
|
||||
check(count_of(st, "switch (psx_mod_immediate_select(4u))") == 2, "store and addiu switched");
|
||||
|
||||
// A site in a branch delay slot.
|
||||
const auto ds = generate({kBeqZero, kSltiV0V0_32, kNop, kNop},
|
||||
with_sites({make(kBase + 4u, kSltiV0V0_32, 1, {64})})).full_code;
|
||||
check(ds.find("switch (psx_mod_immediate_select(1u))") != std::string::npos,
|
||||
"delay-slot site is switched");
|
||||
|
||||
// No sites: vanilla.
|
||||
PSXRecomp::CodeGenConfig plain{};
|
||||
const auto vanilla = generate({kNop, kSltiV0V0_32, kNop}, plain).full_code;
|
||||
check(vanilla.find("psx_mod_immediate_select") == std::string::npos, "no sites: vanilla code");
|
||||
|
||||
// A site elsewhere leaves this instruction alone.
|
||||
const auto other = generate({kNop, kSltiV0V0_32, kNop},
|
||||
with_sites({make(kBase + 0x100u, kSltiV0V0_32, 1, {64})})).full_code;
|
||||
check(other.find("psx_mod_immediate_select") == std::string::npos, "unlisted address: vanilla");
|
||||
|
||||
// Overlay mode: a different word at a listed address stays vanilla.
|
||||
const auto ov = generate({kNop, kLbuV0V1, kNop},
|
||||
with_sites({make(kBase + 4u, kSltiV0V0_32, 1, {64})}, true)).full_code;
|
||||
check(ov.find("psx_mod_immediate_select") == std::string::npos,
|
||||
"overlay word mismatch stays vanilla");
|
||||
const auto ov_match = generate({kNop, kSltiV0V0_32, kNop},
|
||||
with_sites({make(kBase + 4u, kSltiV0V0_32, 1, {64})}, true)).full_code;
|
||||
check(ov_match.find("psx_mod_immediate_select(1u)") != std::string::npos,
|
||||
"overlay word match is switched");
|
||||
}
|
||||
|
||||
void guards() {
|
||||
const auto cfg = with_sites({make(kBase + 4u, kSltiV0V0_32, 1, {64})});
|
||||
check(!exits_with_failure([&] { generate({kNop, kSltiV0V0_32, kNop}, cfg); }),
|
||||
"matching word generates");
|
||||
check(exits_with_failure([&] { generate({kNop, kLbuV0V1, kNop}, cfg); }),
|
||||
"a main-EXE word mismatch fails generation");
|
||||
}
|
||||
|
||||
void shared_decls() {
|
||||
PSXRecomp::PS1Executable exe{};
|
||||
exe.header.load_address = kBase;
|
||||
std::vector<PSXRecomp::GeneratedFunction> functions;
|
||||
{
|
||||
PSXRecomp::CodeGenerator plain(exe);
|
||||
check(plain.build_shared_decls_header(functions).find("psx_mod_immediate_select") ==
|
||||
std::string::npos, "no sites: the declarations do not change");
|
||||
}
|
||||
{
|
||||
const auto cfg = with_sites({make(kBase + 4u, kSltiV0V0_32, 1, {64})});
|
||||
PSXRecomp::CodeGenerator gen(exe, cfg);
|
||||
check(gen.build_shared_decls_header(functions).find(
|
||||
"extern uint32_t psx_mod_immediate_select(uint32_t group);") != std::string::npos,
|
||||
"sites: the selector is declared");
|
||||
}
|
||||
}
|
||||
|
||||
void address_adapter_guard() {
|
||||
const uint32_t original = 0x8ca30000u; /* lw v1,0(a1) */
|
||||
auto cfg = with_sites({make(kBase + 4u, original, 3, {4, 8})});
|
||||
cfg.mod_instruction_sites.insert(kBase + 4u);
|
||||
const auto out = generate({kNop, original, kNop}, cfg).full_code;
|
||||
char guard[128];
|
||||
std::snprintf(guard,sizeof guard,"psx_mod_memory_address(cpu, 0x%08Xu, 0x%08Xu,", kBase+4u, original);
|
||||
check(count_of(out, guard) == 3, "all variants retain the original word for address-adapter guards");
|
||||
}
|
||||
|
||||
void overlay_identity() {
|
||||
auto none = load(base_config());
|
||||
auto some = load(base_config() + sites_block({site("0x80023500", "0x28820020", 1, "64")}));
|
||||
auto other = load(base_config() + sites_block({site("0x80023500", "0x28820020", 1, "65")}));
|
||||
auto ordered = load(base_config() + sites_block({
|
||||
site("0x80023500", "0x28820020", 1, "64"), site("0x80023504", "0x28820020", 1, "64")}));
|
||||
auto reversed = load(base_config() + sites_block({
|
||||
site("0x80023504", "0x28820020", 1, "64"), site("0x80023500", "0x28820020", 1, "64")}));
|
||||
using PSXRecompV4::overlay_codegen_config_hash;
|
||||
check(overlay_codegen_config_hash(none) != overlay_codegen_config_hash(some),
|
||||
"sites enter the overlay-cache identity");
|
||||
check(overlay_codegen_config_hash(some) != overlay_codegen_config_hash(other),
|
||||
"a changed value changes the identity");
|
||||
check(overlay_codegen_config_hash(ordered) == overlay_codegen_config_hash(reversed),
|
||||
"site order does not");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_self = argv[0];
|
||||
if (argc >= 3 && std::string(argv[1]) == "--exit-probe") g_exit_probe = std::atoi(argv[2]);
|
||||
loader();
|
||||
emission();
|
||||
guards();
|
||||
shared_decls();
|
||||
overlay_identity();
|
||||
address_adapter_guard();
|
||||
if (failures) {
|
||||
std::fprintf(stderr, "mod_immediate_codegen_test: %d failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
std::printf("mod_immediate_codegen_test: OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -277,6 +277,8 @@ if(BUILD_TESTING)
|
||||
"${PSXRECOMP_ROOT}/tools/tests/test_counted_mips_relocations.py")
|
||||
add_test(NAME interp_mod_entry_guards_test COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_interp_mod_entry_guards.py")
|
||||
add_test(NAME interp_mod_immediate_guards_test COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_interp_mod_immediate_guards.py")
|
||||
add_test(NAME mirror_pc_preserved_guards_test COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_mirror_pc_preserved_guards.py")
|
||||
add_test(NAME segment_miss_wiring_test COMMAND "${Python3_EXECUTABLE}"
|
||||
@@ -1377,6 +1379,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME netplay_content_gate_test COMMAND netplay_content_gate_test)
|
||||
|
||||
add_executable(mod_runtime_test
|
||||
src/mod_immediate.c
|
||||
tests/test_mod_runtime.cpp
|
||||
src/iso_reader_c.cpp
|
||||
src/host_span_ring.c
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/* mod_immediate.h - [[mod_immediate.sites]]: instruction immediates a trusted
|
||||
* mod selects at run time.
|
||||
*
|
||||
* A title lists guarded I-type instructions in game.toml:
|
||||
*
|
||||
* [mod_immediate]
|
||||
* sites = [
|
||||
* { address = "0x80023500", expected = "0x28820020", group = 1, values = [64, 128] },
|
||||
* ]
|
||||
*
|
||||
* Selector 0 (the default, restored at every session start) runs the original
|
||||
* instruction. Selector N runs the same instruction with values[N-1] as its
|
||||
* 16-bit immediate. A mod picks the selector of each group at activation with
|
||||
* psx_mod_set_immediate_selector (mod_plugins.h). Values are then fixed until
|
||||
* restart; later setter calls are refused.
|
||||
*
|
||||
* The instruction bytes in RAM never change, so static AOT code, cached overlays
|
||||
* and the interpreter keep matching their original-bytes guards: generated code
|
||||
* switches on psx_mod_immediate_select(group) per listed instruction, and the
|
||||
* dirty-RAM interpreter substitutes the word with psx_mod_immediate_patch.
|
||||
* docs/config_schema.md "Mod immediates" has the config rules.
|
||||
*/
|
||||
#ifndef PSX_MOD_IMMEDIATE_H
|
||||
#define PSX_MOD_IMMEDIATE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PSX_MOD_IMMEDIATE_GROUPS 64
|
||||
#define PSX_MOD_IMMEDIATE_VALUES_MAX 15
|
||||
#define PSX_MOD_IMMEDIATE_SITES_MAX 4096
|
||||
|
||||
typedef struct PSXModImmediateSite {
|
||||
uint32_t address; /* any segment; stored physical */
|
||||
uint32_t expected; /* the complete original instruction word */
|
||||
uint32_t group; /* 0..63 */
|
||||
uint32_t count; /* 1..PSX_MOD_IMMEDIATE_VALUES_MAX */
|
||||
uint16_t values[PSX_MOD_IMMEDIATE_VALUES_MAX]; /* imm16 for selector 1..count */
|
||||
} PSXModImmediateSite;
|
||||
|
||||
/* Nonzero while a site exists and any selector is nonzero: the interpreter's
|
||||
* per-instruction gate. */
|
||||
extern int g_psx_mod_immediate_live;
|
||||
|
||||
/* The title's sites (game config). Copied, sorted by (address, word) and capped
|
||||
* at PSX_MOD_IMMEDIATE_SITES_MAX. Returns the number kept. */
|
||||
int psx_mod_immediate_set_sites(const PSXModImmediateSite *sites, int count);
|
||||
int psx_mod_immediate_site_count(void);
|
||||
|
||||
/* Selector of `group`; 0 for an out-of-range group. Called by generated code. */
|
||||
uint32_t psx_mod_immediate_select(uint32_t group);
|
||||
/* Session reset: every selector back to 0 (stock instructions). */
|
||||
void psx_mod_immediate_reset(void);
|
||||
/* Host lifecycle only: begin_session resets choices and leaves setters locked.
|
||||
* The mod runtime opens only for active activation callbacks, after cleanup,
|
||||
* then seals before guest execution. Online activation rejects nonzero choices;
|
||||
* the host checks session_valid even if the plugin ignores the setter result. */
|
||||
void psx_mod_immediate_begin_session(int online);
|
||||
void psx_mod_immediate_begin_activation(void);
|
||||
void psx_mod_immediate_end_activation(void);
|
||||
int psx_mod_immediate_session_valid(void);
|
||||
|
||||
/* The instruction the interpreter executes at `pc`: `insn` itself unless a
|
||||
* listed site with that exact word has a nonzero selector. */
|
||||
uint32_t psx_mod_immediate_patch(uint32_t pc, uint32_t insn);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -927,6 +927,21 @@ int psx_mod_set_disc_speed(uint32_t divisor,
|
||||
int psx_mod_set_draw_distance_clamp(int enabled);
|
||||
int psx_mod_draw_distance_clamp_enabled(void);
|
||||
|
||||
/*
|
||||
* Select the immediate variant of one [[mod_immediate.sites]] group
|
||||
* (mod_immediate.h). Selector 0 runs the stock instructions; N runs
|
||||
* values[N-1] of every site in `group`. The instruction bytes in RAM never
|
||||
* change, so static and cached native code stay valid. Returns 1 when the
|
||||
* title configured sites, 0 otherwise (nothing the guest sees changes), and
|
||||
* rejects a group >= 64 or a selector > 15. Reset to 0 at every session start:
|
||||
* call it from activation, before the guest runs. The caller owns the memory
|
||||
* layout the selector implies (a savestate or card written under another
|
||||
* selector is not interchangeable). Only offline activation may select a value;
|
||||
* the host seals choices before guest execution. Later setter calls fail.
|
||||
*/
|
||||
int psx_mod_set_immediate_selector(uint32_t group, uint32_t selector);
|
||||
uint32_t psx_mod_immediate_selector(uint32_t group);
|
||||
|
||||
/* Controller presentation values exposed to trusted game-owned plugins. */
|
||||
enum {
|
||||
PSX_MOD_CONTROLLER_ANALOG = 1,
|
||||
|
||||
@@ -126,8 +126,9 @@
|
||||
* already holds every release-line callback; shards built against either
|
||||
* v28 or v29 are rejected and recompiled.
|
||||
* v31/v32: reserved for the experimental selector/adapter stack.
|
||||
* v33: guarded load/store effective-address adapter on the master layout. */
|
||||
#define PSX_OVERLAY_ABI_VERSION 33
|
||||
* v33: guarded load/store effective-address adapter on the master layout.
|
||||
* v34: frozen, activation-only immediate selectors on the v33 layout. */
|
||||
#define PSX_OVERLAY_ABI_VERSION 34
|
||||
|
||||
/* Process-lifetime overlay candidate capacity. Every accepted manifest F
|
||||
* record consumes one slot, even when another DLL carries an identical
|
||||
@@ -391,6 +392,8 @@ typedef struct OverlayCallbacks {
|
||||
void (*mod_instruction)(CPUState*, uint32_t, uint32_t);
|
||||
/* Guarded load/store effective-address adapters (ABI v33). */
|
||||
uint32_t (*mod_memory_address)(const CPUState*, uint32_t, uint32_t, uint32_t);
|
||||
/* [[mod_immediate.sites]] selector of a group (ABI v34); NULL = stock. */
|
||||
uint32_t (*mod_immediate_select)(uint32_t group);
|
||||
} OverlayCallbacks;
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -277,6 +277,9 @@ uint32_t psx_mod_memory_address(const CPUState *cpu, uint32_t pc, uint32_t word,
|
||||
overlay_flush_cycles();
|
||||
return g_cbs.mod_memory_address ? g_cbs.mod_memory_address(cpu, pc, word, address) : address;
|
||||
}
|
||||
uint32_t psx_mod_immediate_select(uint32_t group) {
|
||||
return g_cbs.mod_immediate_select ? g_cbs.mod_immediate_select(group) : 0u;
|
||||
}
|
||||
int psx_mod_function_entry(CPUState *cpu, uint32_t address) {
|
||||
overlay_flush_cycles();
|
||||
return g_cbs.mod_function_entry ? g_cbs.mod_function_entry(cpu, address) : 0;
|
||||
|
||||
@@ -425,6 +425,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/debug_trace_ranges.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/dirty_ram_interp.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/draw_distance.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_immediate.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/game_dispatch_compat.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_segment_miss.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/fntrace.c
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "psx_segment_miss.h" /* segment misses in static game code (§5.5) */
|
||||
#include "draw_distance.h" /* [[draw_distance.clamp]] sites */
|
||||
#include "mod_plugins.h" /* instruction guards and effective-address adapters */
|
||||
#include "mod_immediate.h" /* [[mod_immediate.sites]] selectors */
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
@@ -1530,6 +1531,9 @@ static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
|
||||
if (g_psx_mod_instruction_hooks) psx_mod_instruction(cpu, pc, insn);
|
||||
const uint32_t original_instruction = insn;
|
||||
/* [[mod_immediate.sites]]: execute the selected immediate variant. The
|
||||
* guest's bytes (and the guard above) stay the original instruction. */
|
||||
if (g_psx_mod_immediate_live) insn = psx_mod_immediate_patch(pc, insn);
|
||||
|
||||
/* 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. */
|
||||
|
||||
+32
-7
@@ -56,6 +56,7 @@ extern "C" uint32_t g_guest_last_store_addr, g_guest_last_store_val;
|
||||
#include "overlay_backend.h"
|
||||
#include "gpu.h"
|
||||
#include "draw_distance.h"
|
||||
#include "mod_immediate.h"
|
||||
#include "display_scanout.h"
|
||||
#include "pgxp.h"
|
||||
#include "pgxp_session.h"
|
||||
@@ -1997,6 +1998,7 @@ static void reset_mod_owned_presentation(void) {
|
||||
psx_mod_set_retained_scene_predicate(nullptr);
|
||||
psx_mod_set_adaptive_backdrop_preload(0);
|
||||
(void)psx_mod_set_draw_distance_clamp(0);
|
||||
psx_mod_immediate_reset(); /* [[mod_immediate.sites]] selectors */
|
||||
/* [timing] guest_cycle_scale mod gate: shut until this session's
|
||||
* activation opens it (an online match clears the plan, so a mod-gated
|
||||
* scale never carries into netplay). */
|
||||
@@ -17904,6 +17906,24 @@ int main(int argc, char** argv) {
|
||||
(void)psx_draw_distance_set_clamp_sites(sites.data(),
|
||||
(int)sites.size());
|
||||
}
|
||||
{
|
||||
/* [[mod_immediate.sites]]: the interpreter's copy of the
|
||||
* sites the generated code switches on. */
|
||||
std::vector<PSXModImmediateSite> sites;
|
||||
sites.reserve(gc.mod_immediate_sites.size());
|
||||
for (const auto& site : gc.mod_immediate_sites) {
|
||||
PSXModImmediateSite out{};
|
||||
out.address = site.address;
|
||||
out.expected = site.expected;
|
||||
out.group = site.group;
|
||||
out.count = (uint32_t)site.values.size();
|
||||
for (size_t i = 0; i < site.values.size() &&
|
||||
i < PSX_MOD_IMMEDIATE_VALUES_MAX; i++)
|
||||
out.values[i] = site.values[i];
|
||||
sites.push_back(out);
|
||||
}
|
||||
(void)psx_mod_immediate_set_sites(sites.data(), (int)sites.size());
|
||||
}
|
||||
{
|
||||
std::vector<uint32_t> addresses, expected, thresholds;
|
||||
std::vector<uint32_t> object_regs, x_regs, z_regs, y_regs;
|
||||
@@ -19861,13 +19881,6 @@ int main(int argc, char** argv) {
|
||||
* activates and the session stays vanilla. */
|
||||
auto start_mod_session = [&](bool netplay) -> bool {
|
||||
mod_runtime_set_progress_hook(starvation_watchdog_heartbeat);
|
||||
std::string save_scope, save_error;
|
||||
if (!PSXRecompV4::mod_runtime_savestate_scope(save_scope, &save_error) ||
|
||||
!savestate_set_content_scope(save_scope.c_str())) {
|
||||
(void)savestate_set_content_scope("invalid");
|
||||
std::fprintf(stderr, "psxrecomp: cannot establish save-state scope: %s\n", save_error.c_str());
|
||||
return false;
|
||||
}
|
||||
/* Clear game-owned controller overrides/policies and load/disc-speed
|
||||
* choices first so disabling a package cannot leave its prior state
|
||||
* latched across a soft return. */
|
||||
@@ -19901,7 +19914,19 @@ int main(int argc, char** argv) {
|
||||
* sandboxed own view only; the shared game keeps the stock cull. */
|
||||
gpu_ws_set_local_view_only(
|
||||
netplay && PSXRecompV4::mod_runtime_netplay_view_active() ? 1 : 0);
|
||||
psx_mod_immediate_begin_session(netplay ? 1 : 0);
|
||||
mod_runtime_activate_plugins();
|
||||
if (!psx_mod_immediate_session_valid()) {
|
||||
std::fprintf(stderr, "psxrecomp: immediate-selector enhancements are offline-only\n");
|
||||
return false;
|
||||
}
|
||||
std::string save_scope, save_error;
|
||||
if (!PSXRecompV4::mod_runtime_savestate_scope(save_scope, &save_error) ||
|
||||
!savestate_set_content_scope(save_scope.c_str())) {
|
||||
(void)savestate_set_content_scope("invalid");
|
||||
std::fprintf(stderr, "psxrecomp: cannot establish save-state scope: %s\n", save_error.c_str());
|
||||
return false;
|
||||
}
|
||||
if (netplay && memcard_extension_codec_active()) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: selected mod uses extended memory cards, which are unsupported in netplay\n");
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/* mod_immediate.c - the [[mod_immediate.sites]] store and selectors
|
||||
* (mod_immediate.h). */
|
||||
#include "mod_immediate.h"
|
||||
#include "mod_plugins.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int g_psx_mod_immediate_live = 0;
|
||||
|
||||
static PSXModImmediateSite s_imm_sites[PSX_MOD_IMMEDIATE_SITES_MAX];
|
||||
static int s_imm_site_count = 0;
|
||||
static uint8_t s_selector[PSX_MOD_IMMEDIATE_GROUPS];
|
||||
static int s_activating, s_online, s_rejected_online;
|
||||
|
||||
static void imm_update_live(void) {
|
||||
int any = 0;
|
||||
for (int g = 0; g < PSX_MOD_IMMEDIATE_GROUPS; g++) any |= s_selector[g] != 0u;
|
||||
g_psx_mod_immediate_live = (any && s_imm_site_count > 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
static int imm_site_cmp(const void *a, const void *b) {
|
||||
const PSXModImmediateSite *x = (const PSXModImmediateSite *)a;
|
||||
const PSXModImmediateSite *y = (const PSXModImmediateSite *)b;
|
||||
if (x->address != y->address) return x->address < y->address ? -1 : 1;
|
||||
return (x->expected > y->expected) - (x->expected < y->expected);
|
||||
}
|
||||
|
||||
int psx_mod_immediate_set_sites(const PSXModImmediateSite *sites, int count) {
|
||||
if (!sites || count < 0) count = 0;
|
||||
if (count > PSX_MOD_IMMEDIATE_SITES_MAX) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: [[mod_immediate.sites]] lists %d sites; keeping the "
|
||||
"first %d\n", count, PSX_MOD_IMMEDIATE_SITES_MAX);
|
||||
count = PSX_MOD_IMMEDIATE_SITES_MAX;
|
||||
}
|
||||
int kept = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (sites[i].group >= PSX_MOD_IMMEDIATE_GROUPS || sites[i].count == 0u ||
|
||||
sites[i].count > PSX_MOD_IMMEDIATE_VALUES_MAX)
|
||||
continue;
|
||||
s_imm_sites[kept] = sites[i];
|
||||
s_imm_sites[kept].address &= 0x1FFFFFFFu;
|
||||
kept++;
|
||||
}
|
||||
if (kept > 1) qsort(s_imm_sites, (size_t)kept, sizeof s_imm_sites[0], imm_site_cmp);
|
||||
s_imm_site_count = kept;
|
||||
imm_update_live();
|
||||
return kept;
|
||||
}
|
||||
|
||||
int psx_mod_immediate_site_count(void) { return s_imm_site_count; }
|
||||
|
||||
uint32_t psx_mod_immediate_select(uint32_t group) {
|
||||
return group < PSX_MOD_IMMEDIATE_GROUPS ? s_selector[group] : 0u;
|
||||
}
|
||||
|
||||
void psx_mod_immediate_reset(void) {
|
||||
s_activating = s_online = s_rejected_online = 0;
|
||||
for (int g = 0; g < PSX_MOD_IMMEDIATE_GROUPS; g++) s_selector[g] = 0u;
|
||||
imm_update_live();
|
||||
}
|
||||
|
||||
void psx_mod_immediate_begin_session(int online) {
|
||||
psx_mod_immediate_reset();
|
||||
s_online = online != 0;
|
||||
}
|
||||
|
||||
void psx_mod_immediate_begin_activation(void) { s_activating = 1; }
|
||||
void psx_mod_immediate_end_activation(void) { s_activating = 0; }
|
||||
int psx_mod_immediate_session_valid(void) { return !s_rejected_online; }
|
||||
|
||||
uint32_t psx_mod_immediate_patch(uint32_t pc, uint32_t insn) {
|
||||
const uint32_t phys = pc & 0x1FFFFFFFu;
|
||||
int lo = 0, hi = s_imm_site_count;
|
||||
while (lo < hi) {
|
||||
const int mid = lo + (hi - lo) / 2;
|
||||
if (s_imm_sites[mid].address < phys) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
for (; lo < s_imm_site_count && s_imm_sites[lo].address == phys; lo++) {
|
||||
const PSXModImmediateSite *site = &s_imm_sites[lo];
|
||||
if (site->expected != insn) continue;
|
||||
const uint32_t sel = s_selector[site->group];
|
||||
if (sel == 0u || sel > site->count) return insn;
|
||||
return (insn & 0xFFFF0000u) | site->values[sel - 1u];
|
||||
}
|
||||
return insn;
|
||||
}
|
||||
|
||||
/* Selected only during offline activation; immutable until the next boot. */
|
||||
int psx_mod_set_immediate_selector(uint32_t group, uint32_t selector) {
|
||||
if (group >= PSX_MOD_IMMEDIATE_GROUPS || selector > PSX_MOD_IMMEDIATE_VALUES_MAX)
|
||||
return 0;
|
||||
if (!s_activating) return 0;
|
||||
if (s_online && selector != 0u) {
|
||||
s_rejected_online = 1;
|
||||
return 0;
|
||||
}
|
||||
s_selector[group] = (uint8_t)selector;
|
||||
imm_update_live();
|
||||
return s_imm_site_count > 0;
|
||||
}
|
||||
|
||||
uint32_t psx_mod_immediate_selector(uint32_t group) {
|
||||
return psx_mod_immediate_select(group);
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "mod_packages.h"
|
||||
#include "mod_plugins.h"
|
||||
#include "mod_netplay.h"
|
||||
#include "mod_immediate.h"
|
||||
#include "psx_netplay.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
@@ -2477,16 +2478,23 @@ bool mod_runtime_savestate_scope(std::string& scope, std::string* error) {
|
||||
}
|
||||
const bool isolated = std::any_of(s.plan.ordered.begin(), s.plan.ordered.end(),
|
||||
[](const ModPackage* package) { return package->save_compatibility == "isolated"; });
|
||||
if (!isolated) return true;
|
||||
const std::string& fingerprint = s.plan.fingerprint;
|
||||
if (!s.plan.ok || fingerprint.size() != 64 ||
|
||||
!std::all_of(fingerprint.begin(), fingerprint.end(), [](char c) {
|
||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
|
||||
}) || !netplay_resources_verified(s.plan)) {
|
||||
if (error && error->empty()) *error = "Isolated save states require a verified content plan";
|
||||
return false;
|
||||
if (isolated) {
|
||||
const std::string& fingerprint = s.plan.fingerprint;
|
||||
if (!s.plan.ok || fingerprint.size() != 64 ||
|
||||
!std::all_of(fingerprint.begin(), fingerprint.end(), [](char c) {
|
||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
|
||||
}) || !netplay_resources_verified(s.plan)) {
|
||||
if (error && error->empty()) *error = "Isolated save states require a verified content plan";
|
||||
return false;
|
||||
}
|
||||
scope = fingerprint;
|
||||
}
|
||||
if (g_psx_mod_immediate_live) {
|
||||
std::string identity = "psxrecomp/fixed-immediates/v1\n" + scope + "\n";
|
||||
for (uint32_t group = 0; group < PSX_MOD_IMMEDIATE_GROUPS; ++group)
|
||||
identity += std::to_string(psx_mod_immediate_select(group)) + ",";
|
||||
scope = sha256_text(identity);
|
||||
}
|
||||
scope = fingerprint;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2936,6 +2944,7 @@ extern "C" void mod_runtime_activate_plugins(void) {
|
||||
s.audio_tracks.clear();
|
||||
s.extent_start = 0;
|
||||
s.activating = true;
|
||||
psx_mod_immediate_begin_activation();
|
||||
for (const ModResolution::Plugin& plugin : s.plan.plugins) {
|
||||
HostLaunchTimingScope plugin_timing(HOST_LAUNCH_PLUGIN_CALLBACK,
|
||||
plugin.package_id.c_str(), plugin.feature_id.c_str());
|
||||
@@ -2943,6 +2952,7 @@ extern "C" void mod_runtime_activate_plugins(void) {
|
||||
mod_invoke_activation_plugin(plugin.id);
|
||||
plugin_timing.success();
|
||||
}
|
||||
psx_mod_immediate_end_activation();
|
||||
s.activating = false;
|
||||
build_function_entry_hooks(s);
|
||||
timing.success();
|
||||
|
||||
@@ -2636,6 +2636,8 @@ static void init_callbacks(void) {
|
||||
s_callbacks.mod_instruction = psx_mod_instruction;
|
||||
extern uint32_t psx_mod_memory_address(const CPUState*, uint32_t, uint32_t, uint32_t);
|
||||
s_callbacks.mod_memory_address = psx_mod_memory_address;
|
||||
extern uint32_t psx_mod_immediate_select(uint32_t group);
|
||||
s_callbacks.mod_immediate_select = psx_mod_immediate_select;
|
||||
}
|
||||
/* ABI v14: GTE precision-store tracker — the emitter emits a direct
|
||||
* gte_precision_store_word() call for every swc2 (GTE store-word),
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#define PSX_HAS_GAME_DISPATCH 1
|
||||
#include "../src/draw_distance.c"
|
||||
#include "../src/gpu.c"
|
||||
#include "../src/mod_immediate.c"
|
||||
#include "../src/dirty_ram_interp.c"
|
||||
|
||||
#include <stdio.h>
|
||||
@@ -51,9 +52,10 @@ uint32_t i_mask;
|
||||
uint64_t psx_cycle_count;
|
||||
uint32_t g_psx_mod_function_entry_hooks;
|
||||
uint32_t g_psx_mod_instruction_hooks;
|
||||
static uint32_t address_calls, shadow_address;
|
||||
static uint32_t address_calls, shadow_address, address_word;
|
||||
uint32_t psx_mod_memory_address(const CPUState *cpu, uint32_t pc, uint32_t word, uint32_t address) {
|
||||
(void)cpu; (void)pc; (void)word;
|
||||
address_word = word;
|
||||
++address_calls;
|
||||
return address + 0x1000u;
|
||||
}
|
||||
@@ -355,6 +357,19 @@ static void test_memory_addresses(void) {
|
||||
step(&cpu, TEXT_LO, 0x8ca50000u); /* lw a1,0(a1) */
|
||||
CHECK(cpu.gpr[5] == 0x12345678u && shadow_address == 0x80009000u,
|
||||
"load destination may alias original base register");
|
||||
const uint32_t original = 0x8ca30000u; /* lw v1,0(a1) */
|
||||
const PSXModImmediateSite immediate = {TEXT_LO, original, 3u, 1u, {4}};
|
||||
psx_mod_immediate_set_sites(&immediate, 1);
|
||||
psx_mod_immediate_begin_session(0);
|
||||
psx_mod_immediate_begin_activation();
|
||||
CHECK(psx_mod_set_immediate_selector(3, 1), "select relocated offset at activation");
|
||||
psx_mod_immediate_end_activation();
|
||||
memset(&cpu, 0, sizeof cpu); cpu.read_word = psx_read_word;
|
||||
cpu.gpr[5] = 0x80008000u; put_word(0x80009004u, 0x76543210u);
|
||||
step(&cpu, TEXT_LO, original);
|
||||
CHECK(cpu.gpr[3] == 0x76543210u && address_word == original && shadow_address == 0x80009004u,
|
||||
"selected immediate and address adapter share original guard and resolved PGXP address");
|
||||
psx_mod_immediate_reset(); psx_mod_immediate_set_sites(NULL, 0);
|
||||
g_psx_mod_instruction_hooks = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Pin the dirty-RAM interpreter's [[mod_immediate.sites]] call site: the word
|
||||
substitution must follow the guarded instruction hook (which compares the
|
||||
original word) and precede every use of `insn` for decode and execution."""
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
SRC = pathlib.Path(__file__).resolve().parents[1] / "src" / "dirty_ram_interp.c"
|
||||
|
||||
|
||||
def test_interp_substitutes_after_instruction_hook():
|
||||
text = SRC.read_text(encoding="utf-8")
|
||||
start = text.index("static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn,\n"
|
||||
" uint32_t *next_pc_out) {")
|
||||
body = text[start:text.index("static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,", start + 10)]
|
||||
patch = "if (g_psx_mod_immediate_live) insn = psx_mod_immediate_patch(pc, insn);"
|
||||
hook = "if (g_psx_mod_instruction_hooks) psx_mod_instruction(cpu, pc, insn);"
|
||||
assert body.count(patch) == 1
|
||||
assert body.index(hook) < body.index("const uint32_t original_instruction = insn;") < body.index(patch), "hook guards the original word"
|
||||
# decode of the load/store fields happens after the substitution
|
||||
assert body.index(patch) < body.index("const uint32_t ld_op = op_field(insn);")
|
||||
|
||||
|
||||
def test_interp_includes_header():
|
||||
text = SRC.read_text(encoding="utf-8")
|
||||
assert re.search(r'#include "mod_immediate\.h"', text)
|
||||
|
||||
def test_session_seals_before_scope_and_guest():
|
||||
text = (SRC.parent / "main.cpp").read_text(encoding="utf-8")
|
||||
body = text[text.index("auto start_mod_session ="):]
|
||||
assert body.index("psx_mod_immediate_begin_session(") < body.index("mod_runtime_activate_plugins();")
|
||||
assert body.index("mod_runtime_activate_plugins();") < body.index("mod_runtime_savestate_scope(")
|
||||
text = (SRC.parent / "mod_runtime.cpp").read_text(encoding="utf-8")
|
||||
body = text[text.index('extern "C" void mod_runtime_activate_plugins(void)'):]
|
||||
assert body.index("mod_reset_plugin_sessions();") < body.index("psx_mod_immediate_begin_activation();")
|
||||
assert body.index("mod_invoke_activation_plugin(plugin.id);") < body.index("psx_mod_immediate_end_activation();")
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_interp_substitutes_after_instruction_hook()
|
||||
test_interp_includes_header()
|
||||
test_session_seals_before_scope_and_guest()
|
||||
@@ -0,0 +1,188 @@
|
||||
/* [[mod_immediate.sites]] runtime store, selectors and the interpreter's word
|
||||
* substitution (mod_immediate.c): physical addresses, (address, word) lookup,
|
||||
* sorting, the cap and its log line, invalid entries dropped, empty and NULL
|
||||
* lists, selector range checks, the live gate and session reset, and
|
||||
* psx_mod_immediate_patch swapping exactly the imm16 of a listed word.
|
||||
* The interpreter call site is pinned by test_interp_mod_immediate_guards.py. */
|
||||
#include "../src/mod_immediate.c"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#if defined(_WIN32)
|
||||
#include <io.h>
|
||||
#define dup _dup
|
||||
#define dup2 _dup2
|
||||
#define fileno _fileno
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
|
||||
fprintf(stderr, __VA_ARGS__); \
|
||||
fputc('\n', stderr); \
|
||||
++failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define SLTI_V0_V0_20 0x28420020u /* slti v0, v0, 0x20 */
|
||||
#define SLTIU_T2_A1_20 0x2CAA0020u /* sltiu t2, a1, 0x20 */
|
||||
#define LBU_V0_V1_2E9 0x906202E9u /* lbu v0, 0x2e9(v1) */
|
||||
|
||||
static const char *s_capture_path = "mod_immediate_unit_stderr.txt";
|
||||
|
||||
static void capture_stderr(void (*fn)(void), char *buf, size_t cap) {
|
||||
FILE *tmp = fopen(s_capture_path, "w+b");
|
||||
buf[0] = '\0';
|
||||
if (!tmp) {
|
||||
CHECK(0, "cannot open the stderr capture file %s", s_capture_path);
|
||||
fn();
|
||||
return;
|
||||
}
|
||||
fflush(stderr);
|
||||
int saved = dup(fileno(stderr));
|
||||
dup2(fileno(tmp), fileno(stderr));
|
||||
fn();
|
||||
fflush(stderr);
|
||||
dup2(saved, fileno(stderr));
|
||||
close(saved);
|
||||
rewind(tmp);
|
||||
size_t n = fread(buf, 1, cap - 1, tmp);
|
||||
buf[n] = '\0';
|
||||
fclose(tmp);
|
||||
remove(s_capture_path);
|
||||
}
|
||||
|
||||
/* ---- store ----------------------------------------------------------------- */
|
||||
#define BIG_N (PSX_MOD_IMMEDIATE_SITES_MAX + 44)
|
||||
static PSXModImmediateSite big[BIG_N];
|
||||
static void set_big(void) { (void)psx_mod_immediate_set_sites(big, BIG_N); }
|
||||
|
||||
static PSXModImmediateSite mk(uint32_t addr, uint32_t word, uint32_t group, uint32_t n,
|
||||
uint16_t a, uint16_t b) {
|
||||
PSXModImmediateSite s;
|
||||
memset(&s, 0, sizeof s);
|
||||
s.address = addr; s.expected = word; s.group = group; s.count = n;
|
||||
s.values[0] = a; s.values[1] = b;
|
||||
return s;
|
||||
}
|
||||
|
||||
static void test_store(void) {
|
||||
const PSXModImmediateSite sites[] = {
|
||||
mk(0x80020010u, SLTI_V0_V0_20, 1, 2, 0x40, 0x80),
|
||||
mk(0xA0020000u, LBU_V0_V1_2E9, 2, 1, 0x3E9, 0), /* KSEG1 spelling */
|
||||
mk(0x00020010u, SLTIU_T2_A1_20, 1, 2, 0x40, 0x80), /* same address, other word */
|
||||
mk(0x80020020u, SLTI_V0_V0_20, 64, 1, 1, 0), /* bad group: dropped */
|
||||
mk(0x80020030u, SLTI_V0_V0_20, 1, 0, 0, 0), /* no values: dropped */
|
||||
};
|
||||
CHECK(psx_mod_immediate_set_sites(sites, 5) == 3, "three valid sites stored");
|
||||
CHECK(psx_mod_immediate_site_count() == 3, "count 3");
|
||||
CHECK(s_imm_sites[0].address == 0x00020000u && s_imm_sites[1].address == 0x00020010u &&
|
||||
s_imm_sites[2].address == 0x00020010u && s_imm_sites[1].expected < s_imm_sites[2].expected,
|
||||
"stored sorted by (physical address, word)");
|
||||
|
||||
psx_mod_immediate_begin_session(0);
|
||||
psx_mod_immediate_begin_activation();
|
||||
(void)psx_mod_set_immediate_selector(1, 1);
|
||||
CHECK(psx_mod_immediate_patch(0x80020010u, SLTI_V0_V0_20) == 0x28420040u,
|
||||
"KSEG0 lookup, selector 1 -> 0x40");
|
||||
CHECK(psx_mod_immediate_patch(0x00020010u, SLTIU_T2_A1_20) == 0x2CAA0040u,
|
||||
"the other word at the shared address is its own site");
|
||||
CHECK(psx_mod_immediate_patch(0x80020010u, LBU_V0_V1_2E9) == LBU_V0_V1_2E9,
|
||||
"an unlisted word at a listed address is unchanged");
|
||||
CHECK(psx_mod_immediate_patch(0x80020000u, LBU_V0_V1_2E9) == LBU_V0_V1_2E9,
|
||||
"group 2 is still selector 0");
|
||||
(void)psx_mod_set_immediate_selector(2, 1);
|
||||
CHECK(psx_mod_immediate_patch(0x80020000u, LBU_V0_V1_2E9) == 0x906203E9u,
|
||||
"KSEG1 entry found through KSEG0, imm16 swapped");
|
||||
CHECK(psx_mod_immediate_patch(0x80020004u, LBU_V0_V1_2E9) == LBU_V0_V1_2E9,
|
||||
"between entries is unchanged");
|
||||
|
||||
CHECK(psx_mod_immediate_set_sites(NULL, 5) == 0, "NULL list stores none");
|
||||
CHECK(psx_mod_immediate_set_sites(sites, -1) == 0, "negative count stores none");
|
||||
CHECK(psx_mod_immediate_patch(0x80020010u, SLTI_V0_V0_20) == SLTI_V0_V0_20,
|
||||
"empty store is unchanged");
|
||||
|
||||
for (int i = 0; i < BIG_N; i++)
|
||||
big[i] = mk(0x80040000u + 4u * (uint32_t)i, SLTI_V0_V0_20, 3, 1, 0x40, 0);
|
||||
char log[512];
|
||||
capture_stderr(set_big, log, sizeof log);
|
||||
CHECK(psx_mod_immediate_site_count() == PSX_MOD_IMMEDIATE_SITES_MAX,
|
||||
"over-long list stored %d", psx_mod_immediate_site_count());
|
||||
CHECK(strstr(log, "keeping the first 4096") != NULL, "overflow not logged (got \"%s\")", log);
|
||||
(void)psx_mod_immediate_set_sites(NULL, 0);
|
||||
}
|
||||
|
||||
/* ---- selectors ------------------------------------------------------------- */
|
||||
static void test_selectors(void) {
|
||||
psx_mod_immediate_begin_session(0);
|
||||
psx_mod_immediate_begin_activation();
|
||||
CHECK(psx_mod_set_immediate_selector(1, 1) == 0, "no sites: the setter says so");
|
||||
CHECK(psx_mod_immediate_selector(1) == 1u, "the selector is still recorded");
|
||||
CHECK(!g_psx_mod_immediate_live, "no sites: interpreter gate stays off");
|
||||
const PSXModImmediateSite one = mk(0x80020000u, SLTI_V0_V0_20, 1, 2, 0x40, 0x80);
|
||||
(void)psx_mod_immediate_set_sites(&one, 1);
|
||||
CHECK(g_psx_mod_immediate_live, "sites with a nonzero selector: gate on");
|
||||
CHECK(psx_mod_set_immediate_selector(1, 0) == 1, "sites: the setter says so");
|
||||
CHECK(!g_psx_mod_immediate_live, "selector 0: gate off");
|
||||
CHECK(psx_mod_set_immediate_selector(64, 1) == 0 && psx_mod_set_immediate_selector(1, 16) == 0,
|
||||
"out-of-range group / selector refused");
|
||||
CHECK(psx_mod_immediate_select(1) == 0u && psx_mod_immediate_select(99) == 0u,
|
||||
"unset and out-of-range groups read 0");
|
||||
(void)psx_mod_set_immediate_selector(1, 2);
|
||||
CHECK(psx_mod_immediate_select(1) == 2u && g_psx_mod_immediate_live, "selector 2 live");
|
||||
psx_mod_immediate_begin_session(0);
|
||||
psx_mod_immediate_begin_activation();
|
||||
CHECK(psx_mod_immediate_select(1) == 0u && !g_psx_mod_immediate_live, "reset clears");
|
||||
/* A selector past the site's value count runs stock. */
|
||||
(void)psx_mod_set_immediate_selector(1, 5);
|
||||
CHECK(psx_mod_immediate_patch(0x80020000u, SLTI_V0_V0_20) == SLTI_V0_V0_20,
|
||||
"selector beyond the value count is stock");
|
||||
psx_mod_immediate_begin_session(0);
|
||||
psx_mod_immediate_begin_activation();
|
||||
(void)psx_mod_immediate_set_sites(NULL, 0);
|
||||
}
|
||||
|
||||
static void test_session_boundary(void) {
|
||||
const PSXModImmediateSite one = mk(0x80020000u, SLTI_V0_V0_20, 1, 2, 0x40, 0x80);
|
||||
psx_mod_immediate_set_sites(&one, 1);
|
||||
psx_mod_immediate_reset();
|
||||
CHECK(!psx_mod_set_immediate_selector(1, 1), "closed before activation");
|
||||
psx_mod_immediate_begin_session(0);
|
||||
psx_mod_immediate_begin_activation();
|
||||
CHECK(psx_mod_set_immediate_selector(1, 2), "offline activation selects128");
|
||||
psx_mod_immediate_end_activation();
|
||||
CHECK(!psx_mod_set_immediate_selector(1, 1) && !psx_mod_set_immediate_selector(1, 0),
|
||||
"both mutation and disabling require restart");
|
||||
CHECK(psx_mod_immediate_select(1) == 2, "rejected changes retain128");
|
||||
psx_mod_immediate_begin_session(1);
|
||||
psx_mod_immediate_begin_activation();
|
||||
CHECK(psx_mod_immediate_select(1) == 0 && !g_psx_mod_immediate_live, "online starts stock");
|
||||
CHECK(!psx_mod_set_immediate_selector(1, 1), "online nonzero refused");
|
||||
psx_mod_immediate_end_activation();
|
||||
CHECK(!psx_mod_immediate_session_valid() && psx_mod_immediate_select(1) == 0,
|
||||
"ignored online refusal still invalidates launch");
|
||||
psx_mod_immediate_begin_session(0);
|
||||
psx_mod_immediate_begin_activation();
|
||||
CHECK(psx_mod_immediate_session_valid() && psx_mod_immediate_select(1) == 0, "next offline session reset");
|
||||
CHECK(psx_mod_set_immediate_selector(1, 1), "new offline session can select64");
|
||||
psx_mod_immediate_end_activation();
|
||||
psx_mod_immediate_reset();
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc > 1) s_capture_path = argv[1];
|
||||
test_store();
|
||||
test_selectors();
|
||||
test_session_boundary();
|
||||
if (failures) {
|
||||
fprintf(stderr, "%d check(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
puts("PASS mod immediate store, selectors and patch");
|
||||
return 0;
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "host_file_identity.h"
|
||||
#include "mod_packages.h"
|
||||
#include "mod_plugins.h"
|
||||
#include "mod_immediate.h"
|
||||
#include "psx_sha256.h"
|
||||
#include "psx_lobby_client.h"
|
||||
|
||||
@@ -284,7 +285,12 @@ static void test_disc_audio_plugin() {
|
||||
if(!psx_mod_append_cdda_tracks(tracks,2,starts,counts) ||
|
||||
starts[0]!=150 || starts[1]!=153 || counts[0]!=3 || counts[1]!=2) failures++;
|
||||
}
|
||||
static int immediate_activation_choice, immediate_reset_accepted;
|
||||
static void test_immediate_reset(void) {
|
||||
immediate_reset_accepted = psx_mod_set_immediate_selector(1, 2);
|
||||
}
|
||||
static void test_media_plugin(void) {
|
||||
if (immediate_activation_choice) psx_mod_set_immediate_selector(1, immediate_activation_choice);
|
||||
media_access = psx_mod_current_resource_bytes("rom", &media_snapshot, &media_size);
|
||||
const uint8_t* foreign = nullptr;
|
||||
uint64_t size = 0;
|
||||
@@ -1388,6 +1394,38 @@ int main(int argc, char** argv) {
|
||||
std::string save_scope;
|
||||
check(PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error) && save_scope.empty(),
|
||||
"shared packages retain historical save-state paths");
|
||||
const PSXModImmediateSite site = {0x80020000u, 0x28420020u, 1u, 2u, {64, 128}};
|
||||
psx_mod_immediate_set_sites(&site, 1);
|
||||
check(psx_mod_register_session_reset_plugin("immediate.test.reset",test_immediate_reset),
|
||||
"register reset-boundary probe");
|
||||
immediate_activation_choice = 1;
|
||||
psx_mod_immediate_begin_session(0);
|
||||
mod_runtime_activate_plugins();
|
||||
check(!immediate_reset_accepted && psx_mod_immediate_select(1)==1,
|
||||
"cleanup cannot select; active plugin selects64");
|
||||
std::string scope64, scope128;
|
||||
check(PSXRecompV4::mod_runtime_savestate_scope(scope64,&error) && scope64.size()==64,
|
||||
"non-stock selectors isolate even shared-package savestates");
|
||||
check(!psx_mod_set_immediate_selector(1, 2), "live selection refused");
|
||||
check(PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error) && save_scope==scope64,
|
||||
"failed live selection preserves current state compatibility");
|
||||
immediate_activation_choice = 2;
|
||||
psx_mod_immediate_begin_session(0);
|
||||
mod_runtime_activate_plugins();
|
||||
check(psx_mod_immediate_select(1)==2, "next boot selects128");
|
||||
check(PSXRecompV4::mod_runtime_savestate_scope(scope128,&error) && scope128.size()==64 && scope128!=scope64,
|
||||
"128-slot session cannot read64-slot state namespace");
|
||||
psx_mod_immediate_begin_session(1);
|
||||
mod_runtime_activate_plugins();
|
||||
check(!psx_mod_immediate_session_valid() && psx_mod_immediate_select(1)==0,
|
||||
"online activation cannot enable selectors even if plugin ignores refusal");
|
||||
immediate_activation_choice = 0;
|
||||
psx_mod_immediate_begin_session(0);
|
||||
mod_runtime_activate_plugins();
|
||||
check(PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error) && save_scope.empty(),
|
||||
"stock selectors restore historical paths");
|
||||
psx_mod_immediate_reset();
|
||||
psx_mod_immediate_set_sites(nullptr, 0);
|
||||
}
|
||||
const std::string host_media_fp = PSXRecompV4::mod_runtime_plan_fingerprint_portable();
|
||||
check(host_media_fp.size() == 64, "verified external media has a portable netplay identity");
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
* dropped at link time (LTO plus section GC / dead_strip), so only the stubs
|
||||
* below are needed. */
|
||||
#include "../src/gpu.c"
|
||||
#include "../src/mod_immediate.c"
|
||||
#include "../src/dirty_ram_interp.c"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
Reference in New Issue
Block a user