Support guarded mod callbacks and verified disc extents for combined content

This commit is contained in:
Matthew Stanley
2026-09-30 22:51:23 -07:00
parent 071847abf8
commit b9f9ba230f
23 changed files with 573 additions and 14 deletions
+30
View File
@@ -786,6 +786,19 @@ valid until the committed plan is replaced or cleared. Decode into host or
enhancement memory and rebuild derived data when activation changes; never
reopen the owner path as a substitute for the verified snapshot.
An activation callback can mount a slice of its verified resource for native CD
streaming with `psx_mod_append_disc_extent(id, byte_offset, sector_count, &lba)`.
The slice contains 2336-byte Mode-2 sectors beginning at the duplicated XA
subheader. The runtime appends extents after the mounted disc, synthesizes raw
sector headers and a data-track TOC/subchannel entry, and retains the original
CD-ROM/XA decoder, interrupts and timing. The returned LBAs are deterministic
for the same activation order. Registration rejects missing/unowned resources,
invalid ranges/subheaders, late calls and addresses beyond the CD MSF limit.
Plan replacement or netplay clearing removes the extents; base sectors remain
unchanged. The game plugin supplies its own file lookup, names and stream
selection. Donor bytes remain external and are covered by the resource fingerprint.
Verified resource fingerprints include format, canonical size and SHA-256,
rather than the selected path, so moving identical media or changing N64 byte
order does not change save compatibility. Donor bytes are never package files,
@@ -803,6 +816,23 @@ rejects encrypted entries and unsafe or absolute paths, limits archives to 4096
files and 256 MiB expanded size, stages extraction, validates the manifest, and
publishes the version atomically.
### Guarded instruction callbacks
Trusted plugins can register
`psx_mod_register_instruction_plugin(id, address, expected_word, callback)`.
List native code locations in `[recompiler] mod_instruction_sites` and regenerate.
Callbacks run immediately before the instruction; the registered full word,
executing word and current RAM word must agree. Segment aliases share a site.
Only active resolved plugins run, with their feature's resource context.
The dirty-RAM backend uses the same checks, including nested delay slots.
Overlay ABI v27 carries the forwarder; site metadata changes the codegen hash.
Callbacks may change registers and data. They must preserve PC and must not
re-enter guest execution, because pending branch/load state is live.
`psx_mod_finish_function` is unavailable in this callback scope. Inactive plans
do not change guest behavior. Function entry replacement remains the separate
`psx_mod_register_function_entry_plugin` interface.
### Presentation bezel packages
The framework registers `psx.bezel`, a trusted presentation plugin for OpenGL
+51
View File
@@ -213,6 +213,57 @@ on a fixed region -> next.
## 5. Status / Log (update every session)
- **2026-09-30 (Phase 5 owner-requested source checkpoint):** Preserve the
existing V8:2 combined-content title and generic callback/disc-extent support
on local `checkpoint/v82-combined-20260930` branches. Source, tests and
attribution only; donor media, generated outputs and playtest evidence stay
external. Owner explicitly paused further implementation, including
widescreen, original music and netplay, pending a check-in. No publication.
- **2026-09-30 (Phase 5 Dreamland mushroom freeze):** `beads-eio.12.5`
identified an imported projectile flag write altering its callback, followed
by a skipped prologue and invalid return into the BIOS exception loop. The
title converter now tracks the local projectile factory and target pointers.
Added 4163 relocation/flag property cases and an opt-in native mushroom-event
stress driver; the old overlay fails and the repaired overlay completes both
families. All 16 title checks pass. No framework behavior changes.
- **2026-09-30 (Phase 5 Dreamland effect cleanup):** `beads-eio.12.2` traced
the owner's frozen V8:2 match to an imported projectile effect writing its
matrix over the native child link. The title's input-overlay converter now
recognizes both effect-factory object returns. Five corrected field accesses,
owner-media regression checks, complete overlay code generation and a live
explosion allocation/free check validate the repair. No framework execution,
timing or generated-source changes; owner handoff remains at 1x rendering.
- **2026-09-30 (Phase 5 instruction callbacks):** `beads-kbj0` adds plan-owned,
exact-word-guarded callbacks before opt-in game instructions, including branch
and delay slots. Native/overlay metadata participates in the codegen hash;
dirty-RAM execution routes through the same guard after load retirement.
Overlay ABI v27 forwards the callback. Runtime, parser/emitter and interpreter
routing checks pass. V8:2 uses seven seams for independent vehicle stats,
upgrades, transformation parts and special dependencies. Chassey's live match,
input/pause and savestate roundtrip were observed after correcting the title
allocator to retain shrinking allocations, matching its source implementation.
- **2026-09-30 (Phase 5 mod-defined callbacks):** `beads-eio.3.212` adds
trusted mod-defined guest functions in an unused address range, separate
from original function-entry hooks. Active plans own availability and
callback resource context. Generated BIOS dispatch handles these through
its normal return/stack contract, with no synthetic guest instructions.
Runtime tests cover aliases, collisions, activation/clearing and nested
callback scopes; emitter tests cover routing before hardware dispatch.
OpenBIOS/SCPH1001 regenerated; V8:2 title rebuilt. Imported content still
needs its title-side runtime/selection adapters (`beads-eio.12.1`).
- **2026-09-30 (Phase 5 combined V8 project):** Owner selected USA 2nd Offense
as the game-code base while retaining psxrecomp as the runtime/recompiler.
New local `Vigilante82PSXRecomp` builds against `071847ab`, reaches a stock
match, responds to driving input and reloads a savestate in an isolated run.
The reference's thirteen original-car conversions roundtrip successfully;
imported runtime/selector adapters remain title work (`beads-eio.12.1`).
No generic execution or hardware change was needed for this bring-up.
- **2026-09-30 (Phase 5 callback reentrancy):** Plugin activation, VBlank and
function-entry callbacks now use the same scoped resource/completion context.
A native call inside an entry hook can deliver VBlank; previously those
+3
View File
@@ -66,6 +66,7 @@ struct CodeGenConfig {
// Trusted game-mod entry hooks ([recompiler] mod_function_entry_funcs).
// Only explicitly listed guest functions call the runtime dispatcher.
std::set<uint32_t> mod_function_entry_funcs;
std::set<uint32_t> mod_instruction_sites;
// [recompiler] hot_funcs: emit __attribute__((hot)) on these guest
// addresses (MotK VLC leaves, etc.). Host locality hint only.
@@ -426,6 +427,8 @@ private:
// Instruction translation
std::string translate_instruction(uint32_t addr, uint32_t instr);
std::string translate_instruction_body(uint32_t addr, uint32_t instr);
std::string mod_instruction_call(uint32_t addr, uint32_t instr) const;
// Register name mapping
static std::string reg_name(int reg_num);
+12
View File
@@ -852,7 +852,17 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
return keep_branch_if_wide("0 /* unknown branch condition: defaults to not-taken */");
}
std::string CodeGenerator::mod_instruction_call(uint32_t addr, uint32_t instr) const {
const bool selected = std::any_of(config_.mod_instruction_sites.begin(),
config_.mod_instruction_sites.end(), [=](uint32_t site) {
return (site & 0x1FFFFFFFu) == (addr & 0x1FFFFFFFu);
});
return selected ? fmt::format("psx_mod_instruction(cpu, 0x{:08X}u, 0x{:08X}u);\n", addr, instr) : "";
}
std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) {
return mod_instruction_call(addr, instr) + translate_instruction_body(addr, instr);
}
std::string CodeGenerator::translate_instruction_body(uint32_t addr, uint32_t instr) {
uint32_t opcode = (instr >> 26) & 0x3F;
uint32_t funct = instr & 0x3F;
@@ -2078,6 +2088,7 @@ std::string CodeGenerator::translate_basic_block(
} else {
// Control flow is handled at block exit
if (addr == exit_branch_addr) {
ss << config_.indent << mod_instruction_call(addr, instr);
std::string delay_saved_cond; // branch condition captured before delay
std::string delay_saved_target; // JR/JALR target captured before delay
@@ -3371,6 +3382,7 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
ss << "#endif\n";
ss << "extern int psx_game_text_native_ok(uint32_t addr); /* stale-static guard (dispatch shard) */\n";
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 int psx_mod_function_entry(CPUState* cpu, uint32_t address); /* trusted opt-in game-mod hook */\n";
ss << "extern void psx_datashard_ret(CPUState* cpu); /* data-shard capture finalize */\n";
ss << "extern int psx_vsync_query_hle_enter(CPUState* cpu, uint32_t func, uint32_t counter_addr, uint32_t gpustat_ptr_addr, uint32_t timer1_ptr_addr, uint32_t timer1_cache_addr); /* load_accel.c */\n";
+10
View File
@@ -62,6 +62,7 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) {
h.words("sprite_tag_funcs", c.ws_sprite_tag_funcs);
h.words("mod_function_entry_funcs", c.mod_function_entry_funcs);
h.words("mod_instruction_sites", c.mod_instruction_sites);
h.words("cull_bias", c.ws_cull_bias_sites);
h.words("cull_range", c.ws_cull_range_sites);
h.words("cull_a1", c.ws_cull_a1_sites);
@@ -1497,6 +1498,14 @@ GameConfig load_game_config(const fs::path& config_path_in) {
}
// Optional [recompiler] hot_funcs — __attribute__((hot)) on emitted C.
// Optional trusted, statically linked game-mod entry hooks.
std::vector<uint32_t> mod_instruction_sites;
if (recomp.contains("mod_instruction_sites"))
for (const auto& a : toml::find<std::vector<std::string>>(recomp, "mod_instruction_sites")) {
const auto pc = parse_hex(a, "recompiler.mod_instruction_sites");
if ((pc & 3u) || pc >= 0xC0000000u || (pc & 0x1FFFFFFFu) >= 0x00800000u)
throw std::runtime_error("mod instruction site must be aligned main RAM");
mod_instruction_sites.push_back(pc);
}
std::vector<uint32_t> mod_function_entry_funcs;
if (recomp.contains("mod_function_entry_funcs")) {
const auto& arr = toml::find<std::vector<std::string>>(
@@ -2208,6 +2217,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
/*ws_auto_ui_squash*/ ws_auto_ui_squash,
/*data_shard_funcs*/ data_shard_funcs,
/*mod_function_entry_funcs*/ mod_function_entry_funcs,
/*mod_instruction_sites*/ mod_instruction_sites,
/*hot_funcs*/ hot_funcs,
/*load_charge_batch*/ load_charge_batch,
/*load_charge_batch_funcs*/ load_charge_batch_funcs,
+2
View File
@@ -868,6 +868,8 @@ struct GameConfig {
// entries that dispatch trusted, statically linked mod callbacks. Empty by
// default, so projects that do not opt in emit no callback overhead.
std::vector<uint32_t> mod_function_entry_funcs;
// Opt-in pre-instruction callbacks; plugins supply exact-word guards.
std::vector<uint32_t> mod_instruction_sites;
// [recompiler] hot_funcs: guest addresses that get __attribute__((hot))
// on their generated C bodies (profile/host locality; no guest semantics).
+5 -1
View File
@@ -1792,6 +1792,8 @@ void FullFunctionEmitter::emit_dispatch(
out += " * NULL (the default) = pure LLE, dispatch identical to a build without\n";
out += " * the tier. */\n";
out += "extern int (*g_psx_bios_hle_hook)(CPUState* cpu, uint32_t phys);\n\n";
out += "extern uint32_t g_psx_mod_guest_functions;\n";
out += "extern int psx_mod_dispatch_guest_function(CPUState*, uint32_t);\n\n";
out += "#ifdef PSX_HAS_GAME_DISPATCH\n";
out += "extern int psx_game_address_in_text(uint32_t addr);\n";
out += "#endif\n\n";
@@ -2168,6 +2170,8 @@ void FullFunctionEmitter::emit_dispatch(
out += " cpu->pc = 0;\n";
out += fmt::format(" int lo = 0, hi = {} - 1;\n", total_entries);
out += " int found = 0;\n";
out += " if (g_psx_mod_guest_functions)\n";
out += " found = psx_mod_dispatch_guest_function(cpu, addr);\n";
out += " /* BIOS HLE tier: consult the hook FIRST, on the pre-normalize\n";
out += " * physical address. It must see (a) the A0/B0/C0 service vectors\n";
out += " * (game thunks jr there with the function number in $t1) and (b)\n";
@@ -2176,7 +2180,7 @@ void FullFunctionEmitter::emit_dispatch(
out += " * backends. Handled (rc 1) \xE2\x87\x92 the service completed against guest\n";
out += " * state and the guest resumes at $ra via the trampoline's normal\n";
out += " * return/tail contract below. rc 0 \xE2\x87\x92 pure LLE fall-through. */\n";
out += " if (g_psx_bios_hle_hook &&\n";
out += " if (!found && g_psx_bios_hle_hook &&\n";
out += " g_psx_bios_hle_hook(cpu, addr & 0x1FFFFFFFu)) {\n";
out += " cpu->pc = cpu->gpr[31];\n";
out += " found = 1;\n";
+4
View File
@@ -195,6 +195,7 @@ int main(int argc, char** argv) {
bool reachable_discovery = false;
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::set<uint32_t> mod_entry_funcs; // trusted game-mod entry hooks
std::set<uint32_t> hot_funcs; // [recompiler] hot_funcs
std::set<uint32_t> load_charge_batch_funcs; // [recompiler] load_charge_batch*
@@ -252,6 +253,7 @@ int main(int argc, char** argv) {
ds_funcs.insert(cfg.data_shard_funcs.begin(), cfg.data_shard_funcs.end());
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());
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(),
@@ -358,6 +360,7 @@ int main(int argc, char** argv) {
ws_tag_funcs.insert(wscfg.ws_sprite_tag_funcs.begin(), wscfg.ws_sprite_tag_funcs.end());
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());
ws_cull_bias.insert(wscfg.ws_cull_bias_sites.begin(), wscfg.ws_cull_bias_sites.end());
ws_cull_range.insert(wscfg.ws_cull_range_sites.begin(), wscfg.ws_cull_range_sites.end());
ws_cull_a1.insert(wscfg.ws_cull_a1_sites.begin(), wscfg.ws_cull_a1_sites.end());
@@ -1226,6 +1229,7 @@ int main(int argc, char** argv) {
codegen_config.ws_sprite_tag_funcs = ws_tag_funcs;
codegen_config.data_shard_funcs = ds_funcs;
codegen_config.mod_function_entry_funcs = mod_entry_funcs;
codegen_config.mod_instruction_sites = mod_instruction_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;
@@ -260,6 +260,19 @@ void patch_range_delay_boundaries(BiosConfig config) {
}
}
void mod_guest_dispatch_precedes_hardware_routes() {
const auto result = run_case("mod-guest-dispatch", {0x03E00008u, 0u},
{function_at(kBase, kBase + 4u, {kBase})});
const auto dispatch = result.dispatch.find("static void psx_dispatch_impl(");
const auto hook = result.dispatch.find("found = psx_mod_dispatch_guest_function(cpu, addr)", dispatch);
const auto hle = result.dispatch.find("if (!found && g_psx_bios_hle_hook", dispatch);
const auto hardware = result.dispatch.find("if (!found && psx_bios_try_native_call_stub", dispatch);
expect(dispatch != std::string::npos && hook > dispatch && hook < hle && hle < hardware,
"mod-defined guest calls are handled before address normalization and hardware dispatch");
expect(result.dispatch.find("if (g_psx_mod_guest_functions)", dispatch) < hook,
"no active mod guest functions keeps the dispatch path dormant");
}
int main() {
BiosConfig config{};
config.config_path = "test://full-function-emitter";
@@ -282,6 +295,7 @@ int main() {
fragment_split_load_falls_back();
noncomplementary_lwl_falls_back();
complementary_lwl_lwr_stays_native();
mod_guest_dispatch_precedes_hardware_routes();
patch_range_guards(config);
patch_range_delay_boundaries(config);
@@ -1202,6 +1202,34 @@ void mod_function_completion_codegen_test() {
"unconfigured functions retain the stock entry path");
}
void mod_instruction_codegen_test(const fs::path& root) {
const auto path = write_config(root, "instruction-hooks",
"mod_instruction_sites = [\"0x80010000\", \"0x80010004\"]\n");
const auto parsed = PSXRecompV4::load_game_config(path);
check(parsed.mod_instruction_sites.size() == 2, "instruction sites parse");
auto changed = parsed;
changed.mod_instruction_sites.clear();
check(PSXRecompV4::overlay_codegen_config_hash(parsed) !=
PSXRecompV4::overlay_codegen_config_hash(changed), "instruction sites invalidate cached code");
const auto invalid = write_config(root, "instruction-unaligned",
"mod_instruction_sites = [\"0x80010001\"]\n");
check_throws([&] { PSXRecompV4::load_game_config(invalid); }, "aligned main RAM",
"instruction sites reject invalid addresses");
PSXRecomp::CodeGenConfig config{};
config.mod_instruction_sites = {0x80010000u, 0x80010004u};
for (bool overlay : {false, true}) {
for (uint32_t word : {0x2402002Au, 0x10400002u}) {
const auto code = generate_first_instruction(word, {}, overlay, config);
check(code.find(fmt::format("psx_mod_instruction(cpu, 0x80010000u, 0x{:08X}u);", word)) != std::string::npos,
"native/overlay emits instruction guard for ALU and branch");
check(code.find("psx_mod_instruction(cpu, 0x80010004u, 0x00000000u);") != std::string::npos,
"native/overlay includes sequential and branch delay-slot hooks");
}
}
check(generate_first_instruction(0x2402002Au, {}, false).find("psx_mod_instruction(cpu,") == std::string::npos,
"unconfigured code emits no instruction callbacks");
}
void cfg_fallthrough_reachability_test() {
constexpr uint32_t base = 0x80010000u;
PSXRecomp::PS1Executable exe{};
@@ -1252,6 +1280,7 @@ int main() {
jump_table_producer_codegen_test();
cfg_codegen_load_delay_test();
mod_function_completion_codegen_test();
mod_instruction_codegen_test(root);
cfg_fallthrough_reachability_test();
} catch (const std::exception& e) {
fmt::print(stderr, "FAIL unexpected exception: {}\n", e.what());
+1
View File
@@ -488,6 +488,7 @@ if(BUILD_TESTING)
add_executable(mod_runtime_test
tests/test_mod_runtime.cpp
src/iso_reader_c.cpp
src/mod_runtime.cpp
src/mod_packages.cpp
src/mod_media.cpp
+10
View File
@@ -529,6 +529,8 @@ bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void));
* select it by id, and it runs only while a resolved plan activates that id. */
bool mod_register_function_entry_plugin(const std::string& id, uint32_t address,
PSXModFunctionEntryCallback callback);
bool mod_register_guest_function_plugin(const std::string& id, uint32_t address,
PSXModFunctionEntryCallback callback);
bool mod_plugin_registered(const std::string& id);
void mod_invoke_activation_plugin(const std::string& id);
void mod_invoke_vblank_plugin(const std::string& id);
@@ -539,6 +541,14 @@ struct ModFunctionEntryHook {
/* Hooks one implementation registered, in registration order. mod_runtime
* flattens these into an address table when the plan's plugins activate. */
std::vector<ModFunctionEntryHook> mod_function_entry_hooks(const std::string& id);
std::vector<ModFunctionEntryHook> mod_guest_functions(const std::string& id);
struct ModInstructionHook {
uint32_t address, expected;
PSXModFunctionEntryCallback callback;
};
bool mod_register_instruction_plugin(const std::string& id, uint32_t address,
uint32_t expected, PSXModFunctionEntryCallback callback);
std::vector<ModInstructionHook> mod_instruction_hooks(const std::string& id);
void mod_clear_plugins_for_tests();
/* Every id a trusted implementation registered (activation, vblank or
+29
View File
@@ -23,6 +23,26 @@ int psx_mod_register_vblank_plugin(const char* id,
PSXModVBlankCallback callback);
int psx_mod_register_function_entry_plugin(
const char* id, uint32_t address, PSXModFunctionEntryCallback callback);
/* Mod-defined guest functions have no original machine-code body. Addresses
* must be aligned and in physical 0x0F000000..0x0FFFFFFF (an unused bus range),
* never hardware/BIOS/game text. Only statically linked code can register one;
* the active package plan owns its availability and resource context. Address
* aliases share one globally unique registration. The callback supplies the
* full function behavior; return publishes $ra through normal dispatch. */
int psx_mod_register_guest_function_plugin(
const char* id, uint32_t address, PSXModFunctionEntryCallback callback);
int psx_mod_dispatch_guest_function(struct CPUState* cpu, uint32_t address);
extern uint32_t g_psx_mod_guest_functions;
/* Run immediately before a configured instruction, including delay slots.
* The complete instruction word must match both registration and live RAM.
* Callbacks may update registers/data but cannot redirect PC, finish a guest
* function, or re-enter guest execution (pending load/branch state is live).
* Native emits opt in with [recompiler] mod_instruction_sites; dirty-RAM
* execution uses the same guarded table. Inactive plans are inert. */
int psx_mod_register_instruction_plugin(const char* id, uint32_t address,
uint32_t expected, PSXModFunctionEntryCallback callback);
void psx_mod_instruction(struct CPUState* cpu, uint32_t address, uint32_t instruction);
extern uint32_t g_psx_mod_instruction_hooks;
/* Called from generated functions listed by the game config and from every
* interpreted entry, so the hook contract does not depend on the backend.
* Hooks match by code address (segment bits ignored) and run only for plugins
@@ -213,6 +233,15 @@ int psx_mod_current_resource_path(const char* resource_id,
int psx_mod_current_resource_bytes(const char* resource_id,
const uint8_t** bytes, uint64_t* size);
/* Append raw Mode-2 sectors (2336 bytes, starting with the XA subheader)
* from a verified resource owned by this activation callback. Returns their
* native CD LBA in first_lba. Registration is deterministic, append-only and
* rejected outside activation, for invalid ranges or beyond 99:59:74.
* The immutable committed snapshot remains mounted until the plan changes;
* no host path or donor data is saved into guest RAM/savestates. */
int psx_mod_append_disc_extent(const char* resource_id, uint64_t byte_offset,
uint32_t sector_count, uint32_t* first_lba);
/* Display aspects have no framework ceiling: the native-wide surfaces size
* themselves from the live width, and each title caps its own view at what
* it has validated (fixed ratio, or the adaptive maximum below). Requests
+4
View File
@@ -53,6 +53,10 @@ void mod_runtime_on_vblank(void);
void mod_runtime_patch_disc_sector(uint32_t lba, int raw_sector,
uint8_t* bytes, uint32_t size);
void mod_runtime_enable_disc_patches(void);
int mod_runtime_read_disc_extent(uint32_t lba, int raw_sector,
uint8_t* bytes, uint32_t size);
uint32_t mod_runtime_disc_extent_start(void);
uint32_t mod_runtime_disc_sector_count(uint32_t base_count);
#ifdef __cplusplus
}
+3 -1
View File
@@ -113,7 +113,7 @@
* in overlay code (all sign-branch shapes). Appended last; NULL keeps the
* native sign. */
/* v26: trusted entry hooks return whether they completed the guest function. */
#define PSX_OVERLAY_ABI_VERSION 26
#define PSX_OVERLAY_ABI_VERSION 27
/* Process-lifetime overlay candidate capacity. Every accepted manifest F
* record consumes one slot, even when another DLL carries an identical
@@ -356,6 +356,8 @@ typedef struct {
int32_t (*ws_cull_scale)(int32_t bound, int32_t half_extent);
/* Exact-NCLIP branch sign (ABI v25). */
int32_t (*nclip_exact_sign)(int32_t native_mac0, uint32_t pc);
/* Guarded pre-instruction mod callback (ABI v27). */
void (*mod_instruction)(CPUState*, uint32_t, uint32_t);
} OverlayCallbacks;
#ifdef __cplusplus
@@ -233,6 +233,10 @@ int32_t psx_ws_player_x_bound(int32_t vanilla) {
int32_t psx_ws_screen_x_bound(int32_t vanilla) {
return g_cbs.ws_screen_x_bound ? g_cbs.ws_screen_x_bound(vanilla) : vanilla;
}
void psx_mod_instruction(CPUState *cpu, uint32_t address, uint32_t word) {
overlay_flush_cycles();
if (g_cbs.mod_instruction) g_cbs.mod_instruction(cpu, address, word);
}
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;
+4
View File
@@ -1460,6 +1460,10 @@ static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn,
}
}
extern uint32_t g_psx_mod_instruction_hooks;
extern void psx_mod_instruction(CPUState*, uint32_t, uint32_t);
if (g_psx_mod_instruction_hooks) psx_mod_instruction(cpu, 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. */
const uint32_t ld_op = op_field(insn);
+28 -8
View File
@@ -7,6 +7,7 @@
#include "iso_reader.h"
#include "mod_runtime.h"
#include <cstdio>
#include <cstring>
extern "C" {
@@ -20,18 +21,17 @@ void* iso_open(const char* path) {
}
int iso_read_sector(void* handle, uint32_t lba, uint8_t* buffer, int size) {
if (!handle) return 0;
if (!handle || !buffer || size < 2048) return 0;
auto* reader = static_cast<PS1::ISOReader*>(handle);
(void)size; /* ReadSector always reads 2048 bytes */
if (!reader->ReadSector(lba, buffer)) return 0;
if (!mod_runtime_read_disc_extent(lba, 0, buffer, size) && !reader->ReadSector(lba, buffer)) return 0;
mod_runtime_patch_disc_sector(lba, 0, buffer, 2048);
return 1;
}
int iso_read_raw_sector(void* handle, uint32_t lba, uint8_t* buffer, int size) {
if (!handle || size < 2352) return 0;
if (!handle || !buffer || size < 2352) return 0;
auto* reader = static_cast<PS1::ISOReader*>(handle);
if (!reader->ReadRawSector(lba, buffer)) return 0;
if (!mod_runtime_read_disc_extent(lba, 1, buffer, size) && !reader->ReadRawSector(lba, buffer)) return 0;
mod_runtime_patch_disc_sector(lba, 1, buffer, 2352);
return 1;
}
@@ -39,6 +39,23 @@ int iso_read_raw_sector(void* handle, uint32_t lba, uint8_t* buffer, int size) {
int iso_read_subq(void* handle, uint32_t lba, uint8_t* buffer, int size,
int* valid) {
if (!handle || !buffer || size < 12 || !valid) return 0;
const auto* reader=static_cast<PS1::ISOReader*>(handle);
const uint32_t start=mod_runtime_disc_extent_start();
if (start && lba>=start && lba<mod_runtime_disc_sector_count(start)) {
const auto bcd=[](uint32_t v) {return uint8_t((v/10)*16+v%10);};
std::memset(buffer,0,12);buffer[0]=0x41;
buffer[1]=bcd(reader->TrackCount()+1);buffer[2]=1;
const uint32_t rel=lba-start,abs=lba+150;
buffer[3]=bcd(rel/4500);buffer[4]=bcd(rel/75%60);buffer[5]=bcd(rel%75);
buffer[6]=bcd(abs/4500);buffer[7]=bcd(abs/75%60);buffer[8]=bcd(abs%75);
uint16_t crc=0;
for(unsigned i=0;i<10;++i) {
crc^=uint16_t(buffer[i])<<8;
for(unsigned j=0;j<8;++j)crc=uint16_t((crc<<1)^((crc&0x8000)?0x1021:0));
}
crc=uint16_t(~crc);buffer[10]=uint8_t(crc);buffer[11]=uint8_t(crc>>8);
*valid=1;return 1;
}
bool crc_valid = false;
if (!static_cast<PS1::ISOReader*>(handle)->ReadSubChannelQ(
lba, buffer, &crc_valid)) return 0;
@@ -53,22 +70,25 @@ int iso_has_subq_replacements(void* handle) {
uint32_t iso_sector_count(void* handle) {
if (!handle) return 0;
auto* reader = static_cast<PS1::ISOReader*>(handle);
return reader->GetSectorCount();
return mod_runtime_disc_sector_count(reader->GetSectorCount());
}
/* CD-track TOC accessors (multi-track / CD-DA support). track is 1-based. */
int iso_track_count(void* handle) {
if (!handle) return 1;
return static_cast<PS1::ISOReader*>(handle)->TrackCount();
return static_cast<PS1::ISOReader*>(handle)->TrackCount() + (mod_runtime_disc_extent_start()?1:0);
}
uint32_t iso_track_start_lba(void* handle, int track) {
if (!handle) return 0;
return static_cast<PS1::ISOReader*>(handle)->TrackStartLBA(track);
auto* reader=static_cast<PS1::ISOReader*>(handle);
if (track==reader->TrackCount()+1 && mod_runtime_disc_extent_start()) return mod_runtime_disc_extent_start();
return reader->TrackStartLBA(track);
}
uint32_t iso_track_pregap_lba(void* handle, int track) {
auto* reader = static_cast<PS1::ISOReader*>(handle);
if (reader && track==reader->TrackCount()+1 && mod_runtime_disc_extent_start()) return mod_runtime_disc_extent_start();
return reader ? reader->TrackPregapLBA(track) : 0;
}
+42 -2
View File
@@ -39,6 +39,8 @@ struct RegisteredPlugin {
/* Generated-function entry hooks, keyed by guest address. One id may
* observe several functions. */
std::vector<std::pair<uint32_t, PSXModFunctionEntryCallback>> function_entries;
std::vector<ModFunctionEntryHook> guest_functions;
std::vector<ModInstructionHook> instructions;
};
std::map<std::string, RegisteredPlugin>& registered_plugins() {
@@ -1475,7 +1477,45 @@ bool mod_plugin_registered(const std::string& id) {
const auto found = registered_plugins().find(id);
return found != registered_plugins().end() &&
(found->second.activation || found->second.vblank ||
!found->second.function_entries.empty());
!found->second.function_entries.empty() || !found->second.guest_functions.empty() ||
!found->second.instructions.empty());
}
bool mod_register_guest_function_plugin(const std::string& id, uint32_t address,
PSXModFunctionEntryCallback callback) {
const uint32_t key = address & 0x1FFFFFFFu;
if (!valid_id(id) || !callback || (address & 3u) ||
key < 0x0F000000u || key >= 0x10000000u || address >= 0xC0000000u)
return false;
for (const auto& plugin : registered_plugins())
for (const auto& function : plugin.second.guest_functions)
if (function.address == key) return false;
registered_plugins()[id].guest_functions.push_back({key, callback});
return true;
}
std::vector<ModFunctionEntryHook> mod_guest_functions(const std::string& id) {
const auto found = registered_plugins().find(id);
return found == registered_plugins().end()
? std::vector<ModFunctionEntryHook>{} : found->second.guest_functions;
}
bool mod_register_instruction_plugin(const std::string& id, uint32_t address,
uint32_t expected, PSXModFunctionEntryCallback callback) {
if (!valid_id(id) || !callback || (address & 3u) || address >= 0xC0000000u ||
(address & 0x1FFFFFFFu) >= 0x00800000u) return false;
auto& hooks = registered_plugins()[id].instructions;
const uint32_t key = address & 0x1FFFFFFFu;
for (const auto& hook : hooks)
if (hook.address == key) return false;
hooks.push_back({key, expected, callback});
return true;
}
std::vector<ModInstructionHook> mod_instruction_hooks(const std::string& id) {
const auto found = registered_plugins().find(id);
return found == registered_plugins().end()
? std::vector<ModInstructionHook>{} : found->second.instructions;
}
void mod_invoke_activation_plugin(const std::string& id) {
@@ -1507,7 +1547,7 @@ std::vector<std::string> mod_registered_plugin_ids() {
std::vector<std::string> ids;
for (const auto& entry : registered_plugins())
if (entry.second.activation || entry.second.vblank ||
!entry.second.function_entries.empty())
!entry.second.function_entries.empty() || !entry.second.guest_functions.empty())
ids.push_back(entry.first);
return ids; /* std::map keeps them sorted */
}
+151
View File
@@ -54,6 +54,8 @@ extern "C" int fntrace_is_game_started(void);
/* Declared in mod_plugins.h; see active_function_entry_hooks() below. */
uint32_t g_psx_mod_function_entry_hooks = 0;
uint32_t g_psx_mod_guest_functions = 0;
uint32_t g_psx_mod_instruction_hooks = 0;
/* Guest vblanks seen by the mod runtime: the plugin counters' time base. */
static uint64_t g_mod_vblanks = 0;
@@ -61,6 +63,10 @@ namespace PSXRecompV4 {
namespace {
struct RuntimeMods {
struct DiscExtent { uint32_t lba, count; const uint8_t* data; };
std::vector<DiscExtent> disc_extents;
uint32_t extent_start = 0;
bool activating = false;
ModPackageManager manager;
ModResolution plan;
ModResolution validation;
@@ -134,6 +140,21 @@ std::vector<ActiveFunctionEntryHook>& active_function_entry_hooks() {
return value;
}
std::vector<ActiveFunctionEntryHook>& active_guest_functions() {
static std::vector<ActiveFunctionEntryHook> value;
return value;
}
struct ActiveInstructionHook {
uint32_t key, expected;
PSXModFunctionEntryCallback callback;
const ModResolution::Plugin* plugin;
};
std::vector<ActiveInstructionHook>& active_instruction_hooks() {
static std::vector<ActiveInstructionHook> value;
return value;
}
inline uint32_t function_entry_key(uint32_t address) {
return address & 0x1FFFFFFFu;
}
@@ -141,6 +162,10 @@ inline uint32_t function_entry_key(uint32_t address) {
void clear_function_entry_hooks() {
active_function_entry_hooks().clear();
g_psx_mod_function_entry_hooks = 0;
active_guest_functions().clear();
g_psx_mod_guest_functions = 0;
active_instruction_hooks().clear();
g_psx_mod_instruction_hooks = 0;
}
void build_function_entry_hooks(const RuntimeMods& s) {
@@ -155,6 +180,20 @@ void build_function_entry_hooks(const RuntimeMods& s) {
[](const ActiveFunctionEntryHook& a,
const ActiveFunctionEntryHook& b) { return a.key < b.key; });
g_psx_mod_function_entry_hooks = (uint32_t)table.size();
auto& functions = active_guest_functions();
for (const auto& plugin : s.plan.plugins)
for (const auto& function : mod_guest_functions(plugin.id))
functions.push_back({function.address, function.callback, &plugin});
std::sort(functions.begin(), functions.end(),
[](const auto& a, const auto& b) { return a.key < b.key; });
g_psx_mod_guest_functions = (uint32_t)functions.size();
auto& instructions = active_instruction_hooks();
for (const auto& plugin : s.plan.plugins)
for (const auto& hook : mod_instruction_hooks(plugin.id))
instructions.push_back({hook.address, hook.expected, hook.callback, &plugin});
std::stable_sort(instructions.begin(), instructions.end(),
[](const auto& a, const auto& b) { return a.key < b.key; });
g_psx_mod_instruction_hooks = (uint32_t)instructions.size();
}
const ModPackage* selected_package(const std::string& id) {
@@ -1183,6 +1222,8 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
RuntimeMods& s = state();
s.manager.set_root({});
clear_function_entry_hooks();
s.disc_extents.clear();
s.extent_start = 0;
s.plan = {};
s.validation = {};
s.raw_disc_index.clear();
@@ -1237,6 +1278,8 @@ bool mod_runtime_clear_for_netplay(std::string* error) {
return true;
}
clear_function_entry_hooks();
s.disc_extents.clear();
s.extent_start = 0;
s.plan = {};
s.validation = {};
s.raw_disc_index.clear();
@@ -1310,6 +1353,8 @@ bool mod_runtime_commit(const std::filesystem::path& disc_path, std::string* err
/* Hooks follow activation, never a bare commit: a new plan runs none of
* its function-entry hooks until mod_runtime_activate_plugins(). */
clear_function_entry_hooks();
s.disc_extents.clear();
s.extent_start = 0;
s.plan = std::move(plan);
build_disc_index(s);
s.effective_disc_path = std::move(effective_disc);
@@ -1402,6 +1447,67 @@ extern "C" void mod_runtime_enable_disc_patches(void) {
PSXRecompV4::state().disc_enabled = true;
}
extern "C" int psx_mod_append_disc_extent(const char* resource_id,
uint64_t byte_offset, uint32_t sector_count, uint32_t* first_lba) {
using namespace PSXRecompV4;
if (first_lba) *first_lba = 0;
auto& s = state();
const uint8_t* data = nullptr;
uint64_t size = 0;
if (!s.activating || !first_lba || !sector_count ||
!psx_mod_current_resource_bytes(resource_id, &data, &size) ||
byte_offset > size || uint64_t(sector_count)*2336 > size-byte_offset) return 0;
try {
uint32_t start = s.extent_start;
if (s.disc_extents.empty()) {
PS1::ISOReader reader;
const auto& mount = s.effective_disc_path.empty() ? s.disc_path : s.effective_disc_path;
if (mount.empty() || !reader.Open(mount.string()) || reader.TrackCount() >= 99) return 0;
start = reader.GetSectorCount();
if (!start) return 0;
}
const uint32_t lba = s.disc_extents.empty() ? start :
s.disc_extents.back().lba + s.disc_extents.back().count;
if (uint64_t(lba)+sector_count+150 > 450000) return 0;
// Mode-2's duplicated subheader is part of the format contract.
for (uint32_t i=0; i<sector_count; ++i) {
const auto* sector = data+byte_offset+uint64_t(i)*2336;
if (std::memcmp(sector,sector+4,4)) return 0;
}
s.disc_extents.push_back({lba,sector_count,data+byte_offset});
s.extent_start = start;
*first_lba = lba;
return 1;
} catch (...) { return 0; }
}
extern "C" uint32_t mod_runtime_disc_extent_start(void) {
const auto& s=PSXRecompV4::state();
return s.disc_enabled && !s.disc_extents.empty() ? s.extent_start : 0;
}
extern "C" uint32_t mod_runtime_disc_sector_count(uint32_t base_count) {
const auto& s=PSXRecompV4::state();
return mod_runtime_disc_extent_start() ?
std::max(base_count,s.disc_extents.back().lba+s.disc_extents.back().count) : base_count;
}
extern "C" int mod_runtime_read_disc_extent(uint32_t lba, int raw,
uint8_t* bytes, uint32_t size) {
if (!bytes || size < (raw ? 2352u : 2048u) || !mod_runtime_disc_extent_start()) return 0;
const auto& extents=PSXRecompV4::state().disc_extents;
for (const auto& e:extents) if (lba>=e.lba && lba-e.lba<e.count) {
const auto* sector=e.data+uint64_t(lba-e.lba)*2336;
if (raw) {
std::memset(bytes,0,16);std::memset(bytes+1,0xff,10);
const uint32_t msf=lba+150;
const auto bcd=[](uint32_t v) {return uint8_t((v/10)*16+v%10);};
bytes[12]=bcd(msf/4500);bytes[13]=bcd(msf/75%60);bytes[14]=bcd(msf%75);bytes[15]=2;
std::memcpy(bytes+16,sector,2336);
} else std::memcpy(bytes,sector+8,2048);
return 1;
}
return 0;
}
extern "C" int psx_mod_read_disc_file(const char* path, void* buffer,
uint32_t capacity, uint32_t* size) {
using namespace PSXRecompV4;
@@ -1444,10 +1550,14 @@ extern "C" void mod_runtime_activate_plugins(void) {
RuntimeMods& s = state();
psx_ram_reset_size_request();
if (!s.initialized || !s.plan.ok) return;
s.disc_extents.clear();
s.extent_start = 0;
s.activating = true;
for (const ModResolution::Plugin& plugin : s.plan.plugins) {
PluginCallbackScope scope(s, &plugin);
mod_invoke_activation_plugin(plugin.id);
}
s.activating = false;
build_function_entry_hooks(s);
}
@@ -1670,6 +1780,47 @@ extern "C" int psx_mod_finish_function(CPUState* cpu) {
return 1;
}
extern "C" int psx_mod_register_guest_function_plugin(
const char* id, uint32_t address, PSXModFunctionEntryCallback callback) {
return id && PSXRecompV4::mod_register_guest_function_plugin(id, address, callback);
}
extern "C" int psx_mod_dispatch_guest_function(CPUState* cpu, uint32_t address) {
using namespace PSXRecompV4;
if (!g_psx_mod_guest_functions || !cpu || address >= 0xC0000000u) return 0;
const auto& functions = active_guest_functions();
const uint32_t key = function_entry_key(address);
auto it = std::lower_bound(functions.begin(), functions.end(), key,
[](const auto& function, uint32_t k) { return function.key < k; });
if (it == functions.end() || it->key != key) return 0;
PluginCallbackScope scope(state(), it->plugin, cpu);
it->callback(cpu, address);
cpu->pc = cpu->gpr[31];
return 1;
}
extern "C" int psx_mod_register_instruction_plugin(const char* id, uint32_t address,
uint32_t expected, PSXModFunctionEntryCallback callback) {
return id && PSXRecompV4::mod_register_instruction_plugin(id, address, expected, callback);
}
extern "C" void psx_mod_instruction(CPUState* cpu, uint32_t address, uint32_t instruction) {
using namespace PSXRecompV4;
if (!g_psx_mod_instruction_hooks || !cpu || address >= 0xC0000000u) return;
const auto& hooks = active_instruction_hooks();
const auto key = function_entry_key(address);
auto it = std::lower_bound(hooks.begin(), hooks.end(), key,
[](const auto& h, uint32_t k) { return h.key < k; });
for (; it != hooks.end() && it->key == key; ++it) {
if (it->expected != instruction || psx_mod_read_word(address) != instruction) continue;
PluginCallbackScope scope(state(), it->plugin, nullptr);
const uint32_t pc = cpu->pc;
it->callback(cpu, address);
if (cpu->pc != pc) std::abort();
cpu->gpr[0] = 0;
}
}
extern "C" int psx_mod_function_entry(CPUState* cpu, uint32_t address) {
using namespace PSXRecompV4;
if (!g_psx_mod_function_entry_hooks || !cpu) return 0;
+2
View File
@@ -2474,6 +2474,8 @@ static void init_callbacks(void) {
extern int psx_mod_function_entry(CPUState *cpu, uint32_t address);
s_callbacks.ws_screen_x_bound = psx_ws_screen_x_bound;
s_callbacks.mod_function_entry = psx_mod_function_entry;
extern void psx_mod_instruction(CPUState*, uint32_t, uint32_t);
s_callbacks.mod_instruction = psx_mod_instruction;
}
/* ABI v14: GTE precision-store tracker — the emitter emits a direct
* gte_precision_store_word() call for every swc2 (GTE store-word),
@@ -6,6 +6,11 @@ from pathlib import Path
def main():
source = (Path(__file__).resolve().parents[1] /
'src/dirty_ram_interp.c').read_text(encoding='utf-8')
fetched = source[source.index('static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn,\n uint32_t *next_pc_out) {'):]
fetched = fetched[:fetched.index('const uint32_t ld_op')]
assert fetched.count('psx_mod_instruction(cpu, pc, insn)') == 1
assert fetched.index('if (s_ld_pend_armed') < fetched.index('psx_mod_instruction(cpu, pc, insn)')
assert 'if (g_psx_mod_instruction_hooks) psx_mod_instruction(cpu, pc, insn);' in fetched
start = source.index('static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {')
source = source[start:]
static_dispatch = source[source.index('/* B-2: statically-compiled'):]
+130 -2
View File
@@ -123,6 +123,30 @@ static void test_active_entry(CPUState* cpu, uint32_t address) {
static void test_disabled_entry(CPUState*, uint32_t) { disabled_entry_hits++; }
static void test_unselected_entry(CPUState*, uint32_t) { unselected_entry_hits++; }
static int guest_function_hits;
static int instruction_hits;
static void test_instruction(CPUState* cpu, uint32_t) {
instruction_hits++;
cpu->gpr[5] = 0x80301008u;
if (psx_mod_finish_function(cpu)) failures++;
}
static void test_guest_function(CPUState* cpu, uint32_t) {
guest_function_hits++;
const uint32_t ra = cpu->gpr[31];
CPUState nested{};
nested.gpr[31] = 0x80007000u;
const int mode = entry_test_mode;
entry_test_mode = 1;
if (!psx_mod_function_entry(&nested, 0x80003000u) || nested.pc != nested.gpr[31]) failures++;
entry_test_mode = mode;
interrupted_entry_cpu = cpu;
mod_runtime_on_vblank();
interrupted_entry_cpu = nullptr;
if (!psx_mod_finish_function(cpu)) failures++;
cpu->gpr[2] = cpu->gpr[4] + 7u;
if (cpu->gpr[31] != ra) failures++;
}
static void test_activation_plugin(void) {
activation_calls++;
}
@@ -130,11 +154,29 @@ static void test_activation_plugin(void) {
static const uint8_t* media_snapshot;
static uint64_t media_size;
static int media_access, foreign_media_access;
static uint32_t mounted_lba;
static int extent_ok, invalid_extent, foreign_extent;
static void test_media_plugin(void) {
media_access = psx_mod_current_resource_bytes("rom", &media_snapshot, &media_size);
const uint8_t* foreign = nullptr;
uint64_t size = 0;
foreign_media_access = psx_mod_current_resource_bytes("private", &foreign, &size);
extent_ok=psx_mod_append_disc_extent("xa",0,2,&mounted_lba);
uint32_t bad=99;
invalid_extent=psx_mod_append_disc_extent("xa",1,2,&bad);
foreign_extent=psx_mod_append_disc_extent("private",0,1,&bad);
}
extern "C" {
void* iso_open(const char*);
void iso_close(void*);
int iso_read_raw_sector(void*,uint32_t,uint8_t*,int);
int iso_read_sector(void*,uint32_t,uint8_t*,int);
int iso_read_subq(void*,uint32_t,uint8_t*,int,int*);
uint32_t iso_sector_count(void*);
int iso_track_count(void*);
uint32_t iso_track_start_lba(void*,int);
int iso_track_is_audio(void*,int);
}
static int big_ram_activations;
@@ -384,18 +426,52 @@ int main() {
"runtime.unselected-entry", 0x80003000u, test_unselected_entry) == 1,
"unselected function-entry hook must register");
CPUState entry_cpu{};
check(psx_mod_register_instruction_plugin("runtime.test-vblank", 0x80003004u,
0x90A30014u, test_instruction), "register guarded instruction callback");
check(!psx_mod_register_instruction_plugin("runtime.test-vblank", 0xA0003004u,
0x90A30014u, test_instruction) &&
!psx_mod_register_instruction_plugin("runtime.other", 0x80003005u,
0x90A30014u, test_instruction), "reject aliased duplicate and unaligned sites");
psx_mod_write_word(0x80003004u, 0x90A30014u);
check(psx_mod_register_guest_function_plugin(
"runtime.test-vblank", 0x8FFF0000u, test_guest_function),
"trusted callback registers outside the hardware map");
check(!psx_mod_register_guest_function_plugin(
"runtime.other", 0xAFFF0000u, test_guest_function) &&
!psx_mod_register_guest_function_plugin("runtime.other", 0x80003000u, test_guest_function) &&
!psx_mod_register_guest_function_plugin("runtime.other", 0x8FFF0001u, test_guest_function) &&
!psx_mod_register_guest_function_plugin("runtime.other", 0xCFFF0000u, test_guest_function),
"guest callbacks refuse alias collisions, game addresses, misalignment and KSEG2");
check(psx_mod_register_guest_function_plugin(
"runtime.unselected-entry", 0x8FFF0004u, test_unselected_entry),
"unselected callback implementation can register");
check(PSXRecompV4::mod_runtime_initialize(
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(cue_path, &error),
"CUE and its data-track BIN must have the same mod target identity");
/* Committed but not yet activated: no hook may run. */
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
check(instruction_hits == 0 && !g_psx_mod_instruction_hooks, "instruction hooks await activation");
check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) &&
g_psx_mod_guest_functions == 0,
"committed guest functions remain unavailable until activation");
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 0,
"function-entry hooks must not run before plugin activation");
mod_runtime_activate_plugins();
check(activation_calls == 1,
"resolved trusted plugin must activate before runtime startup");
entry_cpu.pc = 0x80004000u;
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
psx_mod_instruction(&entry_cpu, 0xA0003004u, 0x90A30014u);
check(instruction_hits == 2 && entry_cpu.gpr[5] == 0x80301008u &&
entry_cpu.pc == 0x80004000u, "instruction aliases preserve control flow and edit registers");
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30015u);
psx_mod_write_word(0x80003004u, 0x90A30015u);
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
check(instruction_hits == 2, "fetched and live instruction guards reject changed code");
psx_mod_write_word(0x80003004u, 0x90A30014u);
mod_runtime_on_vblank();
check(plugin_calls == 1,
"resolved trusted plugin must run on guest VBlank");
@@ -416,6 +492,11 @@ int main() {
"hooks of plugins the plan does not activate must never run");
/* A replaced plan drops every hook until its own activation. */
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str());
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
check(instruction_hits == 2 && !g_psx_mod_instruction_hooks, "clearing removes instruction hooks");
check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) &&
g_psx_mod_guest_functions == 0,
"clearing the plan drops guest callback availability");
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 3,
"clearing the plan must drop its function-entry hooks");
@@ -450,6 +531,24 @@ int main() {
"entry completion survives VBlank callbacks during a native call");
entry_test_mode = 0;
entry_cpu.gpr[4] = 35u;
const uint32_t guest_sp = entry_cpu.gpr[29];
check(g_psx_mod_guest_functions == 1 &&
psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) &&
entry_cpu.gpr[2] == 42u && entry_cpu.pc == 0x80005000u &&
entry_cpu.gpr[29] == guest_sp,
"active callback returns its result through normal guest PC/stack state");
check(psx_mod_dispatch_guest_function(&entry_cpu, 0x0FFF0000u) &&
psx_mod_dispatch_guest_function(&entry_cpu, 0xAFFF0000u) &&
guest_function_hits == 3,
"guest callback aliases retain nested entry and VBlank scope");
check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0004u) &&
!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0001u) &&
!psx_mod_dispatch_guest_function(&entry_cpu, 0xCFFF0000u) &&
!psx_mod_dispatch_guest_function(nullptr, 0x8FFF0000u) &&
unselected_entry_hits == 0,
"unselected, unknown, misaligned and invalid callback requests fall through");
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 1; ram[0x1103] = 0;
@@ -784,6 +883,10 @@ int main() {
rom[32] = 0x5a;
const auto rom_path = media_root / "owner.z64";
write_bytes(rom_path, rom);
std::vector<uint8_t> xa(2336*2);
xa[2]=xa[6]=0x64;xa[8]=0x52;
xa[2336+2]=xa[2336+6]=0xE4;xa[2336+8]=0x63;
const auto xa_path=media_root/"owner.xa";write_bytes(xa_path,xa);
write_text(media_root / "packages/media.test/1.0.0/manifest.toml",
"format_version = 8\nid = \"media.test\"\nversion = \"1.0.0\"\nname = \"Media Test\"\n"
"[[target]]\ngame_id = \"READER\"\n"
@@ -791,15 +894,17 @@ int main() {
"[[feature]]\nid = \"private\"\nname = \"Private\"\n"
"[[resource]]\nfeature = \"active\"\nid = \"rom\"\nlabel = \"ROM\"\n"
"format = \"n64-rom\"\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n"
"[[resource]]\nfeature = \"active\"\nid = \"xa\"\nlabel = \"XA\"\n"
"format = \"file\"\nrequired = true\nsize = 4672\nsha256 = \"" + sha256_hex(xa) + "\"\n"
"[[resource]]\nfeature = \"private\"\nid = \"private\"\nlabel = \"Private\"\n"
"format = \"n64-rom\"\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n"
"[[plugin]]\nfeature = \"active\"\nid = \"media.test.plugin\"\n");
write_text(media_root / "state.toml",
"format_version = 2\n[[feature]]\npackage_id = \"media.test\"\nid = \"active\"\nenabled = true\n"
"[feature.resources]\nrom = \"" + rom_path.generic_string() + "\"\n");
"[feature.resources]\nrom = \"" + rom_path.generic_string() + "\"\nxa = \"" + xa_path.generic_string() + "\"\n");
check(psx_mod_register_activation_plugin("media.test.plugin", test_media_plugin), "register media consumer");
check(PSXRecompV4::mod_runtime_initialize(media_root, "READER", 0, {}, &error), "media initialize");
check(PSXRecompV4::mod_runtime_commit({}, &error), "verify and commit owner media");
check(PSXRecompV4::mod_runtime_commit(iso_path, &error), "verify and commit owner media");
const uint8_t* unavailable = nullptr;
uint64_t unavailable_size = 0;
check(!psx_mod_current_resource_bytes("rom", &unavailable, &unavailable_size), "bytes unavailable outside plugin callback");
@@ -807,11 +912,34 @@ int main() {
check(media_access && media_size == rom.size() &&
std::equal(rom.begin(), rom.end(), media_snapshot), "plugin receives exact verified bytes");
check(!foreign_media_access, "inactive feature cannot supply bytes to another feature");
check(extent_ok && mounted_lba==24 && !invalid_extent && !foreign_extent,"activation mounts only owned bounded resource extents");
uint32_t bad_lba=1;
check(!psx_mod_append_disc_extent("xa",0,1,&bad_lba)&&!bad_lba,"late mounting refused");
void* mounted=iso_open(iso_path.string().c_str());
check(mounted&&iso_sector_count(mounted)==24,"BIOS sees original disc before activation is enabled");
mod_runtime_enable_disc_patches();
std::array<uint8_t,12> subq{};int valid=0;
check(iso_sector_count(mounted)==26&&iso_track_count(mounted)==2&&
iso_track_start_lba(mounted,2)==24&&!iso_track_is_audio(mounted,2),"appended extents expose one data track and leadout");
check(iso_read_raw_sector(mounted,24,sector.data(),sector.size())&&sector[12]==0&&sector[13]==2&&sector[14]==0x24&&sector[15]==2&&
std::equal(xa.begin(),xa.begin()+2336,sector.begin()+16),"native raw sector header and exact XA subheader/audio bytes");
check(iso_read_sector(mounted,25,sector.data(),sector.size())&&sector[0]==0x63,"cooked access resolves the same extent");
check(!iso_read_sector(mounted,26,sector.data(),sector.size())&&
!iso_read_raw_sector(mounted,24,sector.data(),2351),"extent end and short raw buffers rejected");
check(iso_read_subq(mounted,24,subq.data(),subq.size(),&valid)&&valid&&subq[0]==0x41&&subq[1]==2&&subq[5]==0,"appended data track has native subchannel position");
check(iso_read_sector(mounted,22,sector.data(),sector.size())&&sector[1]==7,"base sectors unchanged");
xa[8]=0xff;write_bytes(xa_path,xa);
check(iso_read_raw_sector(mounted,24,sector.data(),sector.size())&&sector[24]==0x52,"streaming uses verified snapshot after external file changes");
mod_runtime_activate_plugins();
check(mounted_lba==24&&iso_sector_count(mounted)==26,"reactivation has stable LBAs and no duplicate extents");
rom[32] ^= 1;
write_bytes(rom_path, rom);
check(media_snapshot && media_snapshot[32] == 0x5a, "committed bytes survive owner file changes");
check(!PSXRecompV4::mod_runtime_commit({}, &error), "new commit rejects modified media");
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), "netplay clears donor plan");
check(iso_sector_count(mounted)==24&&iso_track_count(mounted)==1&&
!iso_read_raw_sector(mounted,24,sector.data(),sector.size()),"clearing plan removes all donor sectors and restores TOC");
iso_close(mounted);
fs::remove_all(root, ec);
if (failures) return 1;
std::cout << "mod runtime tests passed\n";