mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Support guarded mod callbacks and verified disc extents for combined content
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'):]
|
||||
|
||||
@@ -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())&§or[12]==0&§or[13]==2&§or[14]==0x24&§or[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())&§or[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())&§or[1]==7,"base sectors unchanged");
|
||||
xa[8]=0xff;write_bytes(xa_path,xa);
|
||||
check(iso_read_raw_sector(mounted,24,sector.data(),sector.size())&§or[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";
|
||||
|
||||
Reference in New Issue
Block a user