Merge pull request #647: guarded mod immediates fixed until restart

Guarded mod immediates fixed until restart
This commit is contained in:
Matt Stanley
2026-10-19 16:47:57 -07:00
committed by GitHub
27 changed files with 1200 additions and 23 deletions
+22
View File
@@ -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
+47
View File
@@ -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.
+5 -2
View File
@@ -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;
+41
View File
@@ -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).
+11
View File
@@ -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);
+57 -2
View File
@@ -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";
+72
View File
@@ -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;
}
+23
View File
@@ -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.
+12
View File
@@ -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;
}
+3
View File
@@ -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
+74
View File
@@ -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
+15
View File
@@ -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,
+5 -2
View File
@@ -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;
+1
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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");
+107
View File
@@ -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);
}
+19 -9
View File
@@ -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();
+2
View File
@@ -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),
+16 -1
View File
@@ -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()
+188
View File
@@ -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;
}
+38
View File
@@ -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");
+1
View File
@@ -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>