From c0ffd6d4c5f60fb9d717d84afb7c5722954eea7a Mon Sep 17 00:00:00 2001 From: Matt Stanley <1379tech@gmail.com> Date: Mon, 19 Oct 2026 16:00:11 -0700 Subject: [PATCH] Support guarded mod address adapters and atomic extended memory cards Extract the audited adapters and card transport onto master without the held selector/menu stack. Keep adapters and codecs tied to mod activation, use overlay ABI 33, retain rejected-load protection on codec registration, and refuse extended-card codecs in netplay before frame zero. Preserve default atomic save publication and guest-visible write failures. --- docs/MOD_MEMORY_ADDRESSES.md | 47 ++++ docs/MOD_PACKAGES.md | 23 ++ overlay_toolchain/include/overlay_api.h | 8 +- .../include/overlay_dispatch_preamble.c.inc | 4 + recompiler/include/code_generator.h | 27 +-- recompiler/src/code_generator.cpp | 110 +++++---- recompiler/src/pgxp_hook_emitter.cpp | 4 +- recompiler/src/pgxp_hook_emitter.h | 3 +- .../tests/draw_distance_codegen_test.cpp | 33 +++ runtime/CMakeLists.txt | 10 + runtime/include/memcard.h | 26 ++- runtime/include/mod_packages.h | 5 +- runtime/include/mod_plugins.h | 13 ++ runtime/include/mod_runtime.h | 2 + runtime/include/overlay_api.h | 8 +- .../include/overlay_dispatch_preamble.c.inc | 4 + runtime/src/debug_server.c | 8 +- runtime/src/dirty_ram_interp.c | 38 +++- runtime/src/main.cpp | 7 + runtime/src/memcard.c | 192 ++++++++++++++-- runtime/src/mod_packages.cpp | 15 ++ runtime/src/mod_runtime.cpp | 42 +++- runtime/src/overlay_loader.c | 2 + runtime/src/sio.c | 10 +- runtime/tests/test_draw_distance_interp.c | 57 ++++- runtime/tests/test_memcard_extensions.c | 214 ++++++++++++++++++ runtime/tests/test_mod_runtime.cpp | 41 ++++ runtime/tests/test_overlay_memory_address.c | 32 +++ runtime/tests/test_sio_card_protocol.c | 33 +++ runtime/tests/test_ws_cull_edge_interp.c | 3 + 30 files changed, 918 insertions(+), 103 deletions(-) create mode 100644 docs/MOD_MEMORY_ADDRESSES.md create mode 100644 runtime/tests/test_memcard_extensions.c create mode 100644 runtime/tests/test_overlay_memory_address.c diff --git a/docs/MOD_MEMORY_ADDRESSES.md b/docs/MOD_MEMORY_ADDRESSES.md new file mode 100644 index 00000000..1da16609 --- /dev/null +++ b/docs/MOD_MEMORY_ADDRESSES.md @@ -0,0 +1,47 @@ +# Guarded memory address adapters + +A trusted plugin can redirect the effective address of an audited guest memory +instruction with `psx_mod_register_memory_address_plugin(id, pc, expected, +callback)`. Add the PC to `[recompiler] mod_instruction_sites` and regenerate +static code. The callback receives a read-only CPU, PC, original instruction, +and computed effective address; return a replacement address or the input to +decline. No title-specific addresses or data layouts belong in this API. + +The active mod plan owns availability. Both the original instruction passed by +the backend and the live RAM word must equal the registered word. This prevents +an unrelated overlay at the same PC from being redirected. Registrations support +LB/LBU/LH/LHU/LW/LWL/LWR, SB/SH/SW/SWL/SWR, and LWC2/SWC2. PC segment aliases +share registrations. Duplicate plugin/PC/word address adapters are rejected. +Different overlay words at one PC are allowed. + +Adapters execute in plan order. Each receives the address returned by the +previous adapter. They cannot re-enter guest execution or change guest state. +The low two address bits must stay unchanged; a violating result is ignored +and counted as `mod.memory_address.bad_alignment`. This preserves alignment +faults and byte lanes for unaligned word merges. + +Static code computes the address once before the original load/store, keeping +the same value for PGXP. Source registers, store values, destinations, GTE +access, load interlocks, cycle helpers, and pending branch behavior remain in +the normal instruction implementation. Even a load whose destination aliases +its base can therefore be redirected without corrupting the base calculation. +An unconfigured native instruction has no new callback cost. + +Dirty-RAM interpretation passes the original fetched word through the decoder +and redirects the address in each memory-op case. Overlay DLLs forward through +`OverlayCallbacks.mod_memory_address`, flushing pending cycle charges just as +other mod callbacks do. ABI 33 rejects stale DLLs, including the earlier +experimental selector/adapter layout. The normal codegen cache fingerprint +also changes with the emitter. Package deactivation and netplay +plan clearing remove adapters with the existing instruction-hook table. + +Adapters are for access sites whose data ownership has been proved. A numerical +address resembling an extended array index is not sufficient: title callbacks +must validate their overlay/table/index context. Loop bounds, stack-sized table +copies, UI capacities, saves, and asset identities need separate title work. + +Validation extends the mod runtime tests (activation, aliases, instruction/live +RAM guards, CPU preservation, invalid alignment, plan clearing), generator tests +(all memory opcodes, PGXP, branch delay slots, immediate variants, load-to-zero), +and the interpreted-instruction fixture (all memory opcodes and unaligned lanes, +negative offsets, base/destination alias, GTE, and PGXP address parity). diff --git a/docs/MOD_PACKAGES.md b/docs/MOD_PACKAGES.md index 994a5e4b..cc6acac3 100644 --- a/docs/MOD_PACKAGES.md +++ b/docs/MOD_PACKAGES.md @@ -789,6 +789,29 @@ some selections of one feature is a `[[requirement]]`, not a dependency. ## Implicit requirements across packages +Game-owned save metadata can use the memory-card extension transport in +`memcard.h`. A trusted activation plugin registers both load and save callbacks +with `memcard_set_extension_codec()`. Initialization/reload passes the standard +128 KiB base plus its optional tail to the loader. A flush gives the encoder the +current base and publishes both parts together through an exclusive temporary +and atomic replacement. Tails are bounded to 128 KiB; rejected load/encode, +oversized tails and I/O errors leave the original file intact and writes dirty. +`memcard_last_flush_result()` and debug `mc_status` expose failure. +SIO reports a failed write to the guest instead of acknowledging a rejected +flush as saved. + +Each mod session clears the codec before activation. Without a codec, loaded +tails are preserved opaquely; their game-specific binding can become stale when +native saves change. Raw export exposes only the standard base, and explicit +raw import replaces the image and clears its tail. The title owns format +validation, save-slot identity, native fallback characters, and restore/rewind +synchronization. Transport alone does not make additional party members safe. + +Install codecs during plugin activation. Until extended metadata has a negotiated +transport, a session selecting a codec is refused before netplay starts. Ordinary +raw-card synchronization is unchanged; a package identity alone cannot synchronize +an opaque tail. + Trusted graphics plugins can call `psx_mod_set_internal_resolution(lines)` to request a rendering height for their session. Values from 120 through 8192 use the existing integer-scale resolver and backend allocation limits; zero clears diff --git a/overlay_toolchain/include/overlay_api.h b/overlay_toolchain/include/overlay_api.h index fc19550f..9f37b58e 100644 --- a/overlay_toolchain/include/overlay_api.h +++ b/overlay_toolchain/include/overlay_api.h @@ -124,8 +124,10 @@ * entry hooks, guarded pre-instruction callback. * v30: the two lines merged. OverlayCallbacks keeps the master layout, which * already holds every release-line callback; shards built against either - * v28 or v29 are rejected and recompiled. */ -#define PSX_OVERLAY_ABI_VERSION 30 + * v28 or v29 are rejected and recompiled. + * v31/v32: reserved for the experimental selector/adapter stack. + * v33: guarded load/store effective-address adapter on the master layout. */ +#define PSX_OVERLAY_ABI_VERSION 33 /* Process-lifetime overlay candidate capacity. Every accepted manifest F * record consumes one slot, even when another DLL carries an identical @@ -387,6 +389,8 @@ typedef struct OverlayCallbacks { 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); + /* Guarded load/store effective-address adapters (ABI v33). */ + uint32_t (*mod_memory_address)(const CPUState*, uint32_t, uint32_t, uint32_t); } OverlayCallbacks; #ifdef __cplusplus diff --git a/overlay_toolchain/include/overlay_dispatch_preamble.c.inc b/overlay_toolchain/include/overlay_dispatch_preamble.c.inc index bc7d1c64..05636328 100644 --- a/overlay_toolchain/include/overlay_dispatch_preamble.c.inc +++ b/overlay_toolchain/include/overlay_dispatch_preamble.c.inc @@ -266,6 +266,10 @@ 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); } +uint32_t psx_mod_memory_address(const CPUState *cpu, uint32_t pc, uint32_t word, uint32_t address) { + overlay_flush_cycles(); + return g_cbs.mod_memory_address ? g_cbs.mod_memory_address(cpu, pc, word, address) : address; +} 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; diff --git a/recompiler/include/code_generator.h b/recompiler/include/code_generator.h index bfb40bda..b58438a7 100644 --- a/recompiler/include/code_generator.h +++ b/recompiler/include/code_generator.h @@ -490,7 +490,8 @@ private: // Instruction translation std::string translate_instruction(uint32_t addr, uint32_t instr); - std::string translate_instruction_core(uint32_t addr, uint32_t instr); + std::string translate_instruction_core(uint32_t addr, uint32_t instr, + const std::string& memory_addr = "", uint32_t original_instruction = 0); // The [[draw_distance.clamp]] statement for (addr, instr), or "". std::string draw_distance_clamp_prefix(uint32_t addr, uint32_t instr) const; std::string mod_instruction_call(uint32_t addr, uint32_t instr) const; @@ -547,14 +548,14 @@ private: std::string translate_srav(uint32_t instr); // Load/store instructions - std::string translate_lw(uint32_t instr); - std::string translate_sw(uint32_t instr); - std::string translate_lb(uint32_t instr); - std::string translate_lbu(uint32_t instr); - std::string translate_lh(uint32_t instr); - std::string translate_lhu(uint32_t instr); - std::string translate_lwl(uint32_t instr); - std::string translate_lwr(uint32_t instr); + std::string translate_lw(uint32_t instr, const std::string& memory_addr = ""); + std::string translate_sw(uint32_t instr, const std::string& memory_addr = ""); + std::string translate_lb(uint32_t instr, const std::string& memory_addr = ""); + std::string translate_lbu(uint32_t instr, const std::string& memory_addr = ""); + std::string translate_lh(uint32_t instr, const std::string& memory_addr = ""); + std::string translate_lhu(uint32_t instr, const std::string& memory_addr = ""); + std::string translate_lwl(uint32_t instr, const std::string& memory_addr = ""); + std::string translate_lwr(uint32_t instr, const std::string& memory_addr = ""); // // LWL/LWR take their destination register as BOTH input (the merge base) // and output. Unlike every other load, they read that input late enough to @@ -584,10 +585,10 @@ private: } uint32_t lwlr_merge_reg_ = 0xFFFFFFFFu; std::string lwlr_merge_temp_; - std::string translate_swl(uint32_t instr); - std::string translate_swr(uint32_t instr); - std::string translate_sb(uint32_t instr); - std::string translate_sh(uint32_t instr); + std::string translate_swl(uint32_t instr, const std::string& memory_addr = ""); + std::string translate_swr(uint32_t instr, const std::string& memory_addr = ""); + std::string translate_sb(uint32_t instr, const std::string& memory_addr = ""); + std::string translate_sh(uint32_t instr, const std::string& memory_addr = ""); // Immediate instructions std::string translate_addiu(uint32_t instr); diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 8776a854..705e2643 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -223,11 +223,11 @@ std::string CodeGenerator::translate_lui(uint32_t instr) { reg_name(rt), imm, ((uint32_t)imm) << 16); } -std::string CodeGenerator::translate_lw(uint32_t instr) { +std::string CodeGenerator::translate_lw(uint32_t instr, const std::string& memory_addr) { uint32_t rs = get_rs(instr); uint32_t rt = get_rt(instr); int16_t offset = get_imm16(instr); - std::string addr = (offset == 0) ? reg_name(rs) + std::string addr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), offset); uint32_t mask = 1u << rs; /* GPR_DEP rs (load: dest rt armed via LDWhich) */ @@ -239,11 +239,12 @@ std::string CodeGenerator::translate_lw(uint32_t instr) { reg_name(rt), addr, rt, mask); } -std::string CodeGenerator::translate_sw(uint32_t instr) { +std::string CodeGenerator::translate_sw(uint32_t instr, const std::string& memory_addr) { uint32_t rs = get_rs(instr); uint32_t rt = get_rt(instr); int16_t offset = get_imm16(instr); + if (!memory_addr.empty()) return fmt::format("psx_store_cycle_barrier(); cpu->write_word({}, {});", memory_addr, reg_name(rt)); if (offset == 0) { return fmt::format("psx_store_cycle_barrier(); cpu->write_word({}, {});", reg_name(rs), reg_name(rt)); } else { @@ -385,11 +386,11 @@ std::string CodeGenerator::translate_sub(uint32_t instr) { return translate_subu(instr); } -std::string CodeGenerator::translate_lb(uint32_t instr) { +std::string CodeGenerator::translate_lb(uint32_t instr, const std::string& memory_addr) { uint32_t rs = get_rs(instr); uint32_t rt = get_rt(instr); int16_t offset = get_imm16(instr); - std::string addr = (offset == 0) ? reg_name(rs) + std::string addr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), offset); uint32_t mask = 1u << rs; @@ -400,11 +401,11 @@ std::string CodeGenerator::translate_lb(uint32_t instr) { reg_name(rt), addr, rt, mask); } -std::string CodeGenerator::translate_lbu(uint32_t instr) { +std::string CodeGenerator::translate_lbu(uint32_t instr, const std::string& memory_addr) { uint32_t rs = get_rs(instr); uint32_t rt = get_rt(instr); int16_t offset = get_imm16(instr); - std::string addr = (offset == 0) ? reg_name(rs) + std::string addr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), offset); uint32_t mask = 1u << rs; @@ -415,11 +416,11 @@ std::string CodeGenerator::translate_lbu(uint32_t instr) { reg_name(rt), addr, rt, mask); } -std::string CodeGenerator::translate_lh(uint32_t instr) { +std::string CodeGenerator::translate_lh(uint32_t instr, const std::string& memory_addr) { uint32_t rs = get_rs(instr); uint32_t rt = get_rt(instr); int16_t offset = get_imm16(instr); - std::string addr = (offset == 0) ? reg_name(rs) + std::string addr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), offset); uint32_t mask = 1u << rs; @@ -430,11 +431,11 @@ std::string CodeGenerator::translate_lh(uint32_t instr) { reg_name(rt), addr, rt, mask); } -std::string CodeGenerator::translate_lhu(uint32_t instr) { +std::string CodeGenerator::translate_lhu(uint32_t instr, const std::string& memory_addr) { uint32_t rs = get_rs(instr); uint32_t rt = get_rt(instr); int16_t offset = get_imm16(instr); - std::string addr = (offset == 0) ? reg_name(rs) + std::string addr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), offset); uint32_t mask = 1u << rs; @@ -445,19 +446,15 @@ std::string CodeGenerator::translate_lhu(uint32_t instr) { reg_name(rt), addr, rt, mask); } -std::string CodeGenerator::translate_lwl(uint32_t instr) { +std::string CodeGenerator::translate_lwl(uint32_t instr, const std::string& memory_addr) { uint32_t rs = get_rs(instr); uint32_t rt = get_rt(instr); int16_t offset = get_imm16(instr); - if (config_.optimize_zero_reg && rt == 0) { - return "/* nop: load to $zero */"; - } - // LWL: Load Word Left - merges high bytes from the aligned word into rt. // psx_lwl runs the full R3000A load interlock on the aligned address (GPR_DEP rs, // arm LDWhich=rt) and returns the merged value. - std::string addr = (offset == 0) ? reg_name(rs) + std::string addr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), (int32_t)offset); uint32_t mask = 1u << rs; if (config_.optimize_zero_reg && rt == 0) { @@ -467,17 +464,13 @@ std::string CodeGenerator::translate_lwl(uint32_t instr) { reg_name(rt), addr, lwlr_merge_operand(rt), rt, mask); } -std::string CodeGenerator::translate_lwr(uint32_t instr) { +std::string CodeGenerator::translate_lwr(uint32_t instr, const std::string& memory_addr) { uint32_t rs = get_rs(instr); uint32_t rt = get_rt(instr); int16_t offset = get_imm16(instr); - if (config_.optimize_zero_reg && rt == 0) { - return "/* nop: load to $zero */"; - } - // LWR: Load Word Right - merges low bytes from the aligned word into rt. - std::string addr = (offset == 0) ? reg_name(rs) + std::string addr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), (int32_t)offset); uint32_t mask = 1u << rs; if (config_.optimize_zero_reg && rt == 0) { @@ -487,13 +480,14 @@ std::string CodeGenerator::translate_lwr(uint32_t instr) { reg_name(rt), addr, lwlr_merge_operand(rt), rt, mask); } -std::string CodeGenerator::translate_swl(uint32_t instr) { +std::string CodeGenerator::translate_swl(uint32_t instr, const std::string& memory_addr) { uint32_t rs = get_rs(instr); uint32_t rt = get_rt(instr); int16_t offset = get_imm16(instr); // SWL: Store Word Left - stores rt into memory using addr, MSB side // Generate a helper call: psx_swl(cpu, addr, cpu->rt) + if (!memory_addr.empty()) return fmt::format("psx_swl(cpu, {}, {});", memory_addr, reg_name(rt)); if (offset == 0) { return fmt::format("psx_swl(cpu, {}, {});", reg_name(rs), reg_name(rt)); } else { @@ -502,13 +496,14 @@ std::string CodeGenerator::translate_swl(uint32_t instr) { } } -std::string CodeGenerator::translate_swr(uint32_t instr) { +std::string CodeGenerator::translate_swr(uint32_t instr, const std::string& memory_addr) { uint32_t rs = get_rs(instr); uint32_t rt = get_rt(instr); int16_t offset = get_imm16(instr); // SWR: Store Word Right - stores rt into memory using addr, LSB side // Generate a helper call: psx_swr(cpu, addr, cpu->rt) + if (!memory_addr.empty()) return fmt::format("psx_swr(cpu, {}, {});", memory_addr, reg_name(rt)); if (offset == 0) { return fmt::format("psx_swr(cpu, {}, {});", reg_name(rs), reg_name(rt)); } else { @@ -517,11 +512,12 @@ std::string CodeGenerator::translate_swr(uint32_t instr) { } } -std::string CodeGenerator::translate_sb(uint32_t instr) { +std::string CodeGenerator::translate_sb(uint32_t instr, const std::string& memory_addr) { uint32_t rs = get_rs(instr); uint32_t rt = get_rt(instr); int16_t offset = get_imm16(instr); + if (!memory_addr.empty()) return fmt::format("psx_store_cycle_barrier(); cpu->write_byte({}, (uint8_t){});", memory_addr, reg_name(rt)); if (offset == 0) { return fmt::format("psx_store_cycle_barrier(); cpu->write_byte({}, (uint8_t){});", reg_name(rs), reg_name(rt)); } else { @@ -530,11 +526,12 @@ std::string CodeGenerator::translate_sb(uint32_t instr) { } } -std::string CodeGenerator::translate_sh(uint32_t instr) { +std::string CodeGenerator::translate_sh(uint32_t instr, const std::string& memory_addr) { uint32_t rs = get_rs(instr); uint32_t rt = get_rt(instr); int16_t offset = get_imm16(instr); + if (!memory_addr.empty()) return fmt::format("psx_store_cycle_barrier(); cpu->write_half({}, (uint16_t){});", memory_addr, reg_name(rt)); if (offset == 0) { return fmt::format("psx_store_cycle_barrier(); cpu->write_half({}, (uint16_t){});", reg_name(rs), reg_name(rt)); } else { @@ -970,9 +967,20 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) return hook + config_.indent + clamp + "\n" + translate_instruction_core(addr, instr); } -std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t instr) { +std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t instr, + const std::string& memory_addr, uint32_t original_instruction) { uint32_t opcode = (instr >> 26) & 0x3F; uint32_t funct = instr & 0x3F; + const bool memory = (opcode >= 0x20 && opcode <= 0x26) || opcode == 0x28 || opcode == 0x29 || + opcode == 0x2a || opcode == 0x2b || opcode == 0x2e || opcode == 0x32 || opcode == 0x3a; + if (memory && memory_addr.empty() && !mod_instruction_call(addr, instr).empty()) { + const uint32_t guard = original_instruction ? original_instruction : instr; + const std::string raw = fmt::format("(uint32_t)({} + {})", reg_name(get_rs(instr)), (int)get_imm16(instr)); + return config_.indent + fmt::format( + "{{ uint32_t _mod_ea = psx_mod_memory_address(cpu, 0x{:08X}u, 0x{:08X}u, {});\n", + runtime_pc(addr), guard, raw) + + translate_instruction_core(addr, instr, "_mod_ea", guard) + "\n" + config_.indent + "}"; + } // Instruction comment (if enabled) std::string comment; @@ -1235,7 +1243,7 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in config_.ws_cull_bias_lower_sites.count(addr) ? "-" : "+", margin); // Preserve the ordinary ALU shadow update, including source capture // before an in-place add. Margin zero must also be a PGXP identity. - PSXRecomp::append_pgxp_hooks(instr, code); + PSXRecomp::append_pgxp_hooks(instr, code, memory_addr); return config_.indent + code + comment; } else if (!config_.overlay_mode) { fmt::print(stderr, "ERROR: [widescreen.cull] bias site 0x{:08X} is not " @@ -1285,7 +1293,7 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in if (opcode == 0x23) { // lw uint32_t rs = get_rs(instr), rt = get_rt(instr); int16_t offset = get_imm16(instr); - std::string laddr = (offset == 0) + std::string laddr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), (int)offset); uint32_t mask = 1u << rs; @@ -1310,7 +1318,7 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in if (opcode == 0x23) { // lw uint32_t rs = get_rs(instr), rt = get_rt(instr); int16_t offset = get_imm16(instr); - std::string laddr = (offset == 0) + std::string laddr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), (int)offset); uint32_t mask = 1u << rs; @@ -1340,7 +1348,7 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in get_rt(instr) != 0) { uint32_t rs = get_rs(instr), rt = get_rt(instr); int16_t offset = get_imm16(instr); - std::string laddr = (offset == 0) + std::string laddr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), (int)offset); uint32_t mask = 1u << rs; @@ -1376,7 +1384,7 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in if (opcode == 0x23) { // lw uint32_t rs = get_rs(instr), rt = get_rt(instr); int16_t offset = get_imm16(instr); - std::string laddr = (offset == 0) + std::string laddr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), (int)offset); uint32_t mask = 1u << rs; @@ -1560,6 +1568,9 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in uint32_t rs = get_rs(instr), rt = get_rt(instr); int16_t offset = get_imm16(instr); std::string store_pc = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)); + if (!memory_addr.empty()) + return store_pc + fmt::format("psx_store_cycle_barrier(); cpu->write_half({}, (uint16_t)psx_ws_backdrop_x((int16_t){}));{}", + memory_addr, reg_name(rt), comment); if (offset == 0) return store_pc + fmt::format("psx_store_cycle_barrier(); cpu->write_half({}, (uint16_t)psx_ws_backdrop_x((int16_t){}));{}", reg_name(rs), reg_name(rt), comment); @@ -1695,7 +1706,7 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in if (config_.persist_init_store_sites.count(addr)) { uint32_t rs = get_rs(instr), rt = get_rt(instr); int16_t offset = get_imm16(instr); - std::string aexpr = (offset != 0) + std::string aexpr = !memory_addr.empty() ? memory_addr : (offset != 0) ? fmt::format("({} + {})", reg_name(rs), offset) : std::string(reg_name(rs)); std::string store_pc = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)); @@ -1858,23 +1869,23 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in } } break; - case 0x20: code = translate_lb(instr); break; // lb - case 0x21: code = translate_lh(instr); break; // lh - case 0x22: code = translate_lwl(instr); break; // lwl - case 0x23: code = translate_lw(instr); break; // lw - case 0x24: code = translate_lbu(instr); break; // lbu - case 0x25: code = translate_lhu(instr); break; // lhu - case 0x26: code = translate_lwr(instr); break; // lwr + case 0x20: code = translate_lb(instr, memory_addr); break; // lb + case 0x21: code = translate_lh(instr, memory_addr); break; // lh + case 0x22: code = translate_lwl(instr, memory_addr); break; // lwl + case 0x23: code = translate_lw(instr, memory_addr); break; // lw + case 0x24: code = translate_lbu(instr, memory_addr); break; // lbu + case 0x25: code = translate_lhu(instr, memory_addr); break; // lhu + case 0x26: code = translate_lwr(instr, memory_addr); break; // lwr // Stores: stamp the EXACT executing store PC before the write so // wtrace/readtrace producer attribution is correct for game code. // (The BIOS emitter does the same in strict_translator.cpp; the game // emitter previously left g_debug_last_store_pc holding a STALE pc — // the last BIOS store — which mis-attributed game writes to BIOS.) - case 0x28: code = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)) + translate_sb(instr); break; // sb - case 0x29: code = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)) + translate_sh(instr); break; // sh - case 0x2A: code = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)) + translate_swl(instr); break; // swl - case 0x2B: code = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)) + translate_sw(instr); break; // sw - case 0x2E: code = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)) + translate_swr(instr); break; // swr + case 0x28: code = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)) + translate_sb(instr, memory_addr); break; // sb + case 0x29: code = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)) + translate_sh(instr, memory_addr); break; // sh + case 0x2A: code = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)) + translate_swl(instr, memory_addr); break; // swl + case 0x2B: code = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)) + translate_sw(instr, memory_addr); break; // sw + case 0x2E: code = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)) + translate_swr(instr, memory_addr); break; // swr case 0x2F: // CACHE - cache op (no-op for static recomp) code = "/* cache (no-op: static recompilation has no I/D cache) */"; break; @@ -1890,7 +1901,7 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in // (op 0x32 is non-load), the GTE deadline stall via psx_gte_stall, // then psx_cyc_lwc2_read does the ReadMemory timing (completion +1, // no LDWhich arm — the dest is a GTE register). - std::string addr = (offset == 0) + std::string addr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), offset); bool special = PSXRecompGTERegisters::data_write_needs_helper(static_cast(rt)); @@ -1922,7 +1933,7 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in ? fmt::format("gte_read_data(cpu, {})", rt) : fmt::format("cpu->gte_data[{}]", rt); std::string swc2_store_pc = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", runtime_pc(addr)); - std::string swc2_addr = (offset == 0) + std::string swc2_addr = !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), offset); code = gte_stall + swc2_store_pc + fmt::format( @@ -1938,7 +1949,7 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in } } - PSXRecomp::append_pgxp_hooks(instr, code); + PSXRecomp::append_pgxp_hooks(instr, code, memory_addr); /* The trailing disassembly comment must not land ON a preprocessor * directive line -- it would be swallowed as extra tokens and dropped, * costing the annotation and emitting -Wendif-labels noise. Reachable @@ -3610,6 +3621,7 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const { 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 uint32_t psx_mod_memory_address(const CPUState*, uint32_t, 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 int psx_ws_masked_reject(uint32_t flags, uint32_t mask); /* guarded packed-coordinate reject */\n"; ss << "extern int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla);\n"; diff --git a/recompiler/src/pgxp_hook_emitter.cpp b/recompiler/src/pgxp_hook_emitter.cpp index 40a18df5..e611a305 100644 --- a/recompiler/src/pgxp_hook_emitter.cpp +++ b/recompiler/src/pgxp_hook_emitter.cpp @@ -47,14 +47,14 @@ bool emission_ends_on_preprocessor_directive(const std::string& code) { return false; /* blank final line -- appending is already safe */ } -void append_pgxp_hooks(uint32_t instr, std::string& code) { +void append_pgxp_hooks(uint32_t instr, std::string& code, const std::string& memory_addr) { const uint32_t opcode = (instr >> 26) & 0x3F; const uint32_t rs = get_rs(instr); const uint32_t rt = get_rt(instr); const uint32_t rd = get_rd(instr); const int16_t offset = get_imm16(instr); const auto addr_expr = [&]() { - return (offset == 0) ? reg_name(rs) + return !memory_addr.empty() ? memory_addr : (offset == 0) ? reg_name(rs) : fmt::format("{} + {}", reg_name(rs), offset); }; diff --git a/recompiler/src/pgxp_hook_emitter.h b/recompiler/src/pgxp_hook_emitter.h index c01f94ba..a0499023 100644 --- a/recompiler/src/pgxp_hook_emitter.h +++ b/recompiler/src/pgxp_hook_emitter.h @@ -42,7 +42,8 @@ namespace PSXRecomp { " + indent instead of a space when this returns true. */ bool emission_ends_on_preprocessor_directive(const std::string& code); -void append_pgxp_hooks(uint32_t instr, std::string& code); +void append_pgxp_hooks(uint32_t instr, std::string& code, + const std::string& memory_addr = ""); } // namespace PSXRecomp diff --git a/recompiler/tests/draw_distance_codegen_test.cpp b/recompiler/tests/draw_distance_codegen_test.cpp index b7c03d81..e509beec 100644 --- a/recompiler/tests/draw_distance_codegen_test.cpp +++ b/recompiler/tests/draw_distance_codegen_test.cpp @@ -347,6 +347,38 @@ void overlay_identity_unchanged() { "clamps do not enter the overlay-cache identity"); } +void memory_addresses() { + const uint32_t ops[] = {0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x28,0x29,0x2a,0x2b,0x2e,0x32,0x3a}; + for (bool overlay : {false, true}) { + PSXRecomp::CodeGenConfig config{}; + config.overlay_mode = overlay; + config.mod_instruction_sites.insert(kBase); + for (uint32_t op : ops) { + const uint32_t word = (op << 26) | (5u << 21) | (3u << 16) | 0xfffcu; + const auto code = generate({word, 0u}, config).full_code; + const auto callback = code.find("psx_mod_memory_address(cpu, 0x80010000u,"); + check(callback != std::string::npos, "configured memory opcode has an address adapter"); + check(code.find("psx_mod_memory_address(cpu,", callback + 1) == std::string::npos, + "guest access and PGXP evaluate the adapter only once"); + check(code.find("_pgxa = _mod_ea") != std::string::npos, + "PGXP and guest access share the redirected address"); + check(code.find("cpu->gpr[5] =") == std::string::npos, + "adaptation preserves the base register"); + } + config.mod_instruction_sites.clear(); + config.mod_instruction_sites.insert(kBase + 4); + check(generate({0x10000001u, 0x8ca30014u, 0u}, config).full_code.find( + "psx_mod_memory_address(cpu, 0x80010004u,") != std::string::npos, + "branch delay-slot memory access is adapted"); + check(generate({0u, 0x88a00000u, 0u}, config).full_code.find( + "(void)psx_lwl(cpu, _mod_ea") != std::string::npos, + "unaligned load to zero still performs its access"); + } + PSXRecomp::CodeGenConfig plain{}; + check(generate({0x8ca30014u, 0u}, plain).full_code.find("psx_mod_memory_address(cpu,") == std::string::npos, + "unconfigured native memory access has no adapter call"); +} + void runtime_value() { check(psx_draw_distance_clamp_value(0x1C0u, 0x1BF) == 0x1BFu, "clamps past max"); check(psx_draw_distance_clamp_value(0x7FFu, 0x1BF) == 0x1BFu, "clamps far past max"); @@ -366,6 +398,7 @@ int main(int argc, char** argv) { reads_classifier(); loader(); emission(); + memory_addresses(); guards(); shared_decls(); overlay_identity_unchanged(); diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 9ca3189e..382f0087 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -49,6 +49,9 @@ if(BUILD_TESTING) add_executable(video_texture_filter_test tests/test_video_texture_filter.c src/video_texture_filter.c) target_include_directories(video_texture_filter_test PRIVATE include) add_test(NAME video_texture_filter_test COMMAND video_texture_filter_test) + add_executable(overlay_memory_address_test tests/test_overlay_memory_address.c) + target_include_directories(overlay_memory_address_test PRIVATE include) + add_test(NAME overlay_memory_address_test COMMAND overlay_memory_address_test) add_executable(mod_netplay_test tests/test_mod_netplay.c src/mod_netplay.c src/netplay_state_digest.c src/crc32.c src/psx_sha256.c) target_include_directories(mod_netplay_test PRIVATE include) @@ -1657,6 +1660,13 @@ if(BUILD_TESTING) target_include_directories(mod_internal_resolution_test PRIVATE include) add_test(NAME mod_internal_resolution_test COMMAND mod_internal_resolution_test) + add_executable(memcard_extensions_test + tests/test_memcard_extensions.c src/memcard.c) + target_include_directories(memcard_extensions_test PRIVATE include) + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/memcard_extensions_scratch) + add_test(NAME memcard_extensions_test COMMAND memcard_extensions_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/memcard_extensions_scratch) + # Motion kit and per-frame driver for render-pass plugins: blending, # identity matching, limits and the crossfade fallback, on mock RAM. add_executable(render_pass_motion_test diff --git a/runtime/include/memcard.h b/runtime/include/memcard.h index 1fe62fc5..720432c9 100644 --- a/runtime/include/memcard.h +++ b/runtime/include/memcard.h @@ -10,6 +10,30 @@ extern "C" { #define MEMCARD_SIZE (128 * 1024) /* 128KB per card */ #define MEMCARD_SECTOR_SIZE 128 #define MEMCARD_SECTORS 1024 +#define MEMCARD_EXTENSION_MAX 131072u + +/* Optional game-owned metadata after the standard 128KiB image. The serial + * device and raw export see only the standard image. A missing codec preserves + * a loaded opaque tail unchanged; its title-specific binding may become stale. + * Callbacks run on the emulation thread and must not re-enter card I/O. + * load: zero accepts, negative blocks flushing until a successful reload. + * save: return tail byte count (zero = raw card), negative aborts the flush. + * Both callbacks are required. Reset before each mod session's activation. + * Selecting a codec blocks existing cards until initialization/reload has + * successfully validated them with that codec's load callback. + * The codec must define native save fallback, slot identity and restore policy; + * this transport cannot infer those from an opaque tail. */ +typedef int (*MemcardExtensionLoad)(int card, const uint8_t* base, + const uint8_t* tail, uint32_t tail_size); +typedef int (*MemcardExtensionSave)(int card, const uint8_t* base, + uint8_t* tail, uint32_t capacity); +void memcard_set_extension_codec(MemcardExtensionLoad load, + MemcardExtensionSave save); +/* Register codecs during activation. Extended-card transport is offline-only. */ +int memcard_extension_codec_active(void); +/* Last attempted disk flush: 0 success, -1 I/O failure, -2 extension rejected. + * On failure the card stays dirty and the original file stays intact. */ +int memcard_last_flush_result(int card); /* Initialize memcard subsystem. dir = directory for .mcd files (can be NULL). * Loads (or formats+creates) card1.mcd / card2.mcd in `dir` for both slots. */ @@ -31,7 +55,7 @@ void memcard_init_slots(const char* dir, const MemcardSlotConfig slots[2]); * .mcd file's directory frames without touching the live card array. */ typedef struct { int exists; /* file present and readable */ - int valid; /* 128KB and "MC" magic in frame 0 */ + int valid; /* complete 128KB base and "MC" magic in frame 0 */ int used_blocks; /* directory frames in use (0..15) */ int total_blocks; /* always 15 */ uint8_t block_used[15];/* per-block: 1 = occupied, 0 = free */ diff --git a/runtime/include/mod_packages.h b/runtime/include/mod_packages.h index 6dca140b..9f489169 100644 --- a/runtime/include/mod_packages.h +++ b/runtime/include/mod_packages.h @@ -640,11 +640,14 @@ std::vector mod_function_entry_hooks(const std::string& id std::vector mod_guest_functions(const std::string& id); struct ModInstructionHook { uint32_t address, expected; - PSXModFunctionEntryCallback callback; + PSXModFunctionEntryCallback callback = nullptr; + PSXModMemoryAddressCallback memory_address = nullptr; }; bool mod_register_instruction_plugin(const std::string& id, uint32_t address, uint32_t expected, PSXModFunctionEntryCallback callback); std::vector mod_instruction_hooks(const std::string& id); +bool mod_register_memory_address_plugin(const std::string& id, uint32_t address, + uint32_t expected, PSXModMemoryAddressCallback callback); void mod_clear_plugins_for_tests(); /* Every id a trusted implementation registered (activation, vblank or diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index fc7bb585..59493d55 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -58,6 +58,19 @@ 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; +/* Redirect one audited load/store's effective address without changing its + * base register, destination, data, branch state or timing. Registration and + * live RAM must match the complete instruction. Native code instruments sites + * listed in mod_instruction_sites; dirty-RAM execution uses the same table. + * Return the original address to decline. Callbacks run in plan order, cannot + * re-enter guest execution, and must preserve the low two address bits (also + * preserving unaligned merge lanes). The CPU argument is read-only. */ +typedef uint32_t (*PSXModMemoryAddressCallback)(const struct CPUState* cpu, + uint32_t pc, uint32_t instruction, uint32_t effective_address); +int psx_mod_register_memory_address_plugin(const char* id, uint32_t pc, + uint32_t expected, PSXModMemoryAddressCallback callback); +uint32_t psx_mod_memory_address(const struct CPUState* cpu, uint32_t pc, + uint32_t instruction, uint32_t effective_address); /* 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 diff --git a/runtime/include/mod_runtime.h b/runtime/include/mod_runtime.h index 0e857415..5ba40280 100644 --- a/runtime/include/mod_runtime.h +++ b/runtime/include/mod_runtime.h @@ -108,6 +108,8 @@ void mod_runtime_on_dispatch(uint32_t target); * application. Reapply the already-validated main-EXE plan so the current * enabled mod selection remains authoritative after the restore. */ void mod_runtime_on_savestate_loaded(void); +/* Host-only heartbeat during long plugin disc reads; NULL clears it. */ +void mod_runtime_set_progress_hook(void (*hook)(void)); /* Invokes activation callbacks for the committed plan. Call after the final * launcher commit and before renderer/window initialization. */ void mod_runtime_activate_plugins(void); diff --git a/runtime/include/overlay_api.h b/runtime/include/overlay_api.h index fc19550f..9f37b58e 100644 --- a/runtime/include/overlay_api.h +++ b/runtime/include/overlay_api.h @@ -124,8 +124,10 @@ * entry hooks, guarded pre-instruction callback. * v30: the two lines merged. OverlayCallbacks keeps the master layout, which * already holds every release-line callback; shards built against either - * v28 or v29 are rejected and recompiled. */ -#define PSX_OVERLAY_ABI_VERSION 30 + * v28 or v29 are rejected and recompiled. + * v31/v32: reserved for the experimental selector/adapter stack. + * v33: guarded load/store effective-address adapter on the master layout. */ +#define PSX_OVERLAY_ABI_VERSION 33 /* Process-lifetime overlay candidate capacity. Every accepted manifest F * record consumes one slot, even when another DLL carries an identical @@ -387,6 +389,8 @@ typedef struct OverlayCallbacks { 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); + /* Guarded load/store effective-address adapters (ABI v33). */ + uint32_t (*mod_memory_address)(const CPUState*, uint32_t, uint32_t, uint32_t); } OverlayCallbacks; #ifdef __cplusplus diff --git a/runtime/include/overlay_dispatch_preamble.c.inc b/runtime/include/overlay_dispatch_preamble.c.inc index 4a059370..3ce89713 100644 --- a/runtime/include/overlay_dispatch_preamble.c.inc +++ b/runtime/include/overlay_dispatch_preamble.c.inc @@ -273,6 +273,10 @@ 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); } +uint32_t psx_mod_memory_address(const CPUState *cpu, uint32_t pc, uint32_t word, uint32_t address) { + overlay_flush_cycles(); + return g_cbs.mod_memory_address ? g_cbs.mod_memory_address(cpu, pc, word, address) : address; +} 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; diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 0f278728..903f2f97 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -7081,18 +7081,18 @@ static void handle_mc_status(int id, const char *json) json_escape_string(p1_json, sizeof(p1_json), p1); send_fmt("{\"id\":%d,\"ok\":true," "\"slot0\":{\"present\":%s,\"dirty\":%s,\"path\":\"%s\"," - "\"magic\":\"%c%c\",\"magic_hex\":\"%02X%02X\"}," + "\"magic\":\"%c%c\",\"magic_hex\":\"%02X%02X\",\"flush_result\":%d}," "\"slot1\":{\"present\":%s,\"dirty\":%s,\"path\":\"%s\"," - "\"magic\":\"%c%c\",\"magic_hex\":\"%02X%02X\"}}", + "\"magic\":\"%c%c\",\"magic_hex\":\"%02X%02X\",\"flush_result\":%d}}", id, pres0 ? "true" : "false", dirty0 ? "true" : "false", p0_json, (m0[0] >= 0x20 && m0[0] < 0x7F) ? m0[0] : '?', (m0[1] >= 0x20 && m0[1] < 0x7F) ? m0[1] : '?', - m0[0], m0[1], + m0[0], m0[1], memcard_last_flush_result(0), pres1 ? "true" : "false", dirty1 ? "true" : "false", p1_json, (m1[0] >= 0x20 && m1[0] < 0x7F) ? m1[0] : '?', (m1[1] >= 0x20 && m1[1] < 0x7F) ? m1[1] : '?', - m1[0], m1[1]); + m1[0], m1[1], memcard_last_flush_result(1)); } static void handle_spu_status(int id, const char *json) diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 346bd612..143a2a83 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -38,6 +38,7 @@ #include "fntrace.h" /* fntrace_is_game_started / fntrace_mark_game_started */ #include "psx_segment_miss.h" /* segment misses in static game code (§5.5) */ #include "draw_distance.h" /* [[draw_distance.clamp]] sites */ +#include "mod_plugins.h" /* instruction guards and effective-address adapters */ #include #include @@ -1490,7 +1491,7 @@ static void exec_delay_slot(CPUState *cpu, uint32_t pc) { * still writes gpr[rt] eagerly, and we roll that write back by one instruction * so the delay-slot instruction observes the architecturally-correct value. */ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, - uint32_t *next_pc_out); + uint32_t *next_pc_out, uint32_t original_instruction); static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t *next_pc_out) { @@ -1527,9 +1528,8 @@ 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); + const uint32_t original_instruction = 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. */ @@ -1565,7 +1565,7 @@ static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn, } #endif - const int rv = exec_one_fetched_inner(cpu, pc, insn, next_pc_out); + const int rv = exec_one_fetched_inner(cpu, pc, insn, next_pc_out, original_instruction); if (is_ld) { const uint32_t loaded = cpu->gpr[ld_rt]; @@ -1589,7 +1589,7 @@ static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn, } static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, - uint32_t *next_pc_out) { + uint32_t *next_pc_out, uint32_t original_instruction) { exec_pc_table_record(pc); uint32_t opc = op_field(insn); uint32_t rs = rs_field(insn); @@ -2368,6 +2368,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, * accessor (psx_cyc_load_* falls back to a plain read when cycles are off). */ case 0x20: { /* LB rt, simm(rs) */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); cpu->gpr[rt] = (uint32_t)(int32_t)(int8_t)psx_cyc_load_byte(cpu, addr, rt, 1u << rs); psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]); cpu->gpr[0] = 0; @@ -2375,6 +2377,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x21: { /* LH */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); if (addr & 1) return interp_exception(cpu, 4, addr, pc); /* LoadAddressError */ cpu->gpr[rt] = ws_clip_edge_load(pc, (uint32_t)(int32_t)(int16_t)psx_cyc_load_half(cpu, addr, rt, 1u << rs)); @@ -2384,6 +2388,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x22: { /* LWL */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); uint32_t word = psx_cyc_load_word(cpu, addr & ~3u, rt, 1u << rs); cpu->gpr[rt] = lwl_merge(addr, word, cpu->gpr[rt]); psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]); @@ -2392,6 +2398,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x23: { /* LW */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); if (addr & 3) return interp_exception(cpu, 4, addr, pc); /* LoadAddressError */ if (psx_ws_is_local_viewport_width_site(pc)) cpu->gpr[rt] = (uint32_t)psx_ws_local_viewport_width((int32_t)psx_cyc_load_word(cpu, addr, rt, 1u << rs)); @@ -2411,6 +2419,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x24: { /* LBU */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); cpu->gpr[rt] = (uint32_t)psx_cyc_load_byte(cpu, addr, rt, 1u << rs); psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]); cpu->gpr[0] = 0; @@ -2418,6 +2428,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x25: { /* LHU */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); if (addr & 1) return interp_exception(cpu, 4, addr, pc); /* LoadAddressError */ cpu->gpr[rt] = ws_clip_edge_load(pc, (uint32_t)psx_cyc_load_half(cpu, addr, rt, 1u << rs)); @@ -2427,6 +2439,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x26: { /* LWR */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); uint32_t word = psx_cyc_load_word(cpu, addr & ~3u, rt, 1u << rs); cpu->gpr[rt] = lwr_merge(addr, word, cpu->gpr[rt]); psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]); @@ -2435,6 +2449,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x28: { /* SB */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); g_debug_last_store_pc = pc; cpu->write_byte(addr, (uint8_t)cpu->gpr[rt]); psx_pgxp_store(cpu, insn, addr, cpu->gpr[rt] & 0xFFu); @@ -2442,6 +2458,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x29: { /* SH */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); if (addr & 1) return interp_exception(cpu, 5, addr, pc); /* StoreAddressError */ uint16_t val = (uint16_t)cpu->gpr[rt]; /* Widescreen backdrop screenX squash on the interpreter path: mirrors @@ -2458,6 +2476,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x2A: { /* SWL */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); g_debug_last_store_pc = pc; interp_swl(cpu, addr, cpu->gpr[rt]); psx_pgxp_store(cpu, insn, addr, cpu->gpr[rt]); @@ -2465,6 +2485,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x2B: { /* SW */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); if (addr & 3) return interp_exception(cpu, 5, addr, pc); /* StoreAddressError */ g_debug_last_store_pc = pc; cpu->write_word(addr, cpu->gpr[rt]); @@ -2473,6 +2495,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x2E: { /* SWR */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); g_debug_last_store_pc = pc; interp_swr(cpu, addr, cpu->gpr[rt]); psx_pgxp_store(cpu, insn, addr, cpu->gpr[rt]); @@ -2480,6 +2504,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x32: { /* LWC2 — §1+DO_LDS charged by exec_one's top step (mask 0) */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); #ifdef PSX_ENABLE_BLOCK_CYCLES psx_gte_stall(cpu); /* COP2 reg write stalls to GTE completion */ #endif @@ -2492,6 +2518,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x3A: { /* SWC2 */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; + if (g_psx_mod_instruction_hooks) + addr = psx_mod_memory_address(cpu, pc, original_instruction, addr); #ifdef PSX_ENABLE_BLOCK_CYCLES psx_gte_stall(cpu); /* COP2 reg read stalls to GTE completion */ #endif diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index cca6e711..b21fe616 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1955,6 +1955,7 @@ static PSXModSessionBaseline g_mod_owned_baseline; static void reset_mod_owned_presentation(void) { psx_mod_internal_resolution_reset(); + memcard_set_extension_codec(nullptr, nullptr); psx_local_mouse_clear(); psx_keybinds_clear_session_players(); PSXModSessionScalars live; @@ -19859,6 +19860,7 @@ int main(int argc, char** argv) { * precedes them either way. A netplay clear leaves no plan, so nothing * activates and the session stays vanilla. */ auto start_mod_session = [&](bool netplay) -> bool { + mod_runtime_set_progress_hook(starvation_watchdog_heartbeat); std::string save_scope, save_error; if (!PSXRecompV4::mod_runtime_savestate_scope(save_scope, &save_error) || !savestate_set_content_scope(save_scope.c_str())) { @@ -19900,6 +19902,11 @@ int main(int argc, char** argv) { gpu_ws_set_local_view_only( netplay && PSXRecompV4::mod_runtime_netplay_view_active() ? 1 : 0); mod_runtime_activate_plugins(); + if (netplay && memcard_extension_codec_active()) { + std::fprintf(stderr, + "psxrecomp: selected mod uses extended memory cards, which are unsupported in netplay\n"); + return false; + } const PSXModNetplayProfile* profile = psx_mod_netplay_profile(); if (netplay && profile && profile->fixed_aspect_mask) { const int index = psx_mod_netplay_aspect(); diff --git a/runtime/src/memcard.c b/runtime/src/memcard.c index c7c10afc..8ff08baa 100644 --- a/runtime/src/memcard.c +++ b/runtime/src/memcard.c @@ -12,13 +12,22 @@ * - No fake events found (clean) */ +#if !defined(_WIN32) && !defined(_POSIX_C_SOURCE) +#define _POSIX_C_SOURCE 200809L +#endif #include "memcard.h" #include #include #include #include +#include #ifdef _WIN32 #include +#include +#include +#include +#else +#include #endif #define MAX_CARDS 2 @@ -28,9 +37,132 @@ typedef struct { char filepath[512]; int present; int dirty; + uint8_t* extension; + uint32_t extension_size; + int extension_read_failed; + int extension_codec_failed; + int flush_result; } MemCard; static MemCard cards[MAX_CARDS]; +static MemcardExtensionLoad extension_load; +static MemcardExtensionSave extension_save; + +void memcard_set_extension_codec(MemcardExtensionLoad load, + MemcardExtensionSave save) { + extension_load = load && save ? load : NULL; + extension_save = load && save ? save : NULL; + /* A newly selected codec has not validated any currently mounted card. + * Registration must not turn a rejected load into permission to encode. + * Session initialization/reload runs the loader before publication. */ + for (int i = 0; i < MAX_CARDS; ++i) + cards[i].extension_codec_failed = extension_load && cards[i].present; +} + +int memcard_last_flush_result(int card) { + return card >= 0 && card < MAX_CARDS ? cards[card].flush_result : -1; +} + +int memcard_extension_codec_active(void) { + return extension_load != NULL && extension_save != NULL; +} + +static void discard_extension(MemCard* card) { + free(card->extension); + card->extension = NULL; + card->extension_size = 0; + card->extension_read_failed = card->extension_codec_failed = 0; +} + +static void notify_extension_load(int card) { + MemCard* c = &cards[card]; + c->extension_codec_failed = extension_load && + extension_load(card, c->data, c->extension, c->extension_size) < 0; +} + +/* Read one extra byte to distinguish an exactly bounded tail from overflow. + * Overflow/allocation/read failures block publication, rather than truncating + * bytes that the device cannot see but the owning game still needs. */ +static int load_card_image(int card, FILE* f) { + MemCard* c = &cards[card]; + int first; + discard_extension(c); + if (fread(c->data, 1, MEMCARD_SIZE, f) != MEMCARD_SIZE) return -1; + first = fgetc(f); + if (first != EOF) { + c->extension = (uint8_t*)malloc(MEMCARD_EXTENSION_MAX + 1u); + if (!c->extension) { c->extension_read_failed = 1; return 0; } + c->extension[0] = (uint8_t)first; + size_t count = 1u + fread(c->extension + 1, 1, MEMCARD_EXTENSION_MAX, f); + if (count > MEMCARD_EXTENSION_MAX || ferror(f)) { + discard_extension(c); + c->extension_read_failed = 1; + return 0; + } + c->extension_size = (uint32_t)count; + } else if (ferror(f)) { + c->extension_read_failed = 1; + return 0; + } + notify_extension_load(card); + return 0; +} + +/* Publish base and tail together. The exclusive temporary is beside the card, + * so rename never crosses filesystems. Failed writes never replace the card. */ +static int publish_card(const MemCard* c, const uint8_t* tail, uint32_t size) { + static unsigned sequence; + char temporary[576]; + FILE* f = NULL; + int descriptor = -1; + for (unsigned attempt = 0; attempt < 32u; ++attempt) { +#ifdef _WIN32 + snprintf(temporary, sizeof temporary, "%s.tmp.%lu.%u", c->filepath, + (unsigned long)_getpid(), ++sequence); + descriptor = _open(temporary, _O_WRONLY | _O_CREAT | _O_EXCL | _O_BINARY, + _S_IREAD | _S_IWRITE); +#else + snprintf(temporary, sizeof temporary, "%s.tmp.%lu.%u", c->filepath, + (unsigned long)getpid(), ++sequence); + descriptor = open(temporary, O_WRONLY | O_CREAT | O_EXCL, 0600); +#endif + if (descriptor >= 0) break; + } + if (descriptor < 0) return -1; +#ifdef _WIN32 + f = _fdopen(descriptor, "wb"); +#else + f = fdopen(descriptor, "wb"); +#endif + if (!f) { +#ifdef _WIN32 + _close(descriptor); +#else + close(descriptor); +#endif + remove(temporary); + return -1; + } + int ok = fwrite(c->data, 1, MEMCARD_SIZE, f) == MEMCARD_SIZE; + if (ok && size) ok = fwrite(tail, 1, size, f) == size; + if (fflush(f) != 0) ok = 0; +#ifdef _WIN32 + if (ok && _commit(_fileno(f)) != 0) ok = 0; +#else + if (ok && fsync(fileno(f)) != 0) ok = 0; +#endif + if (fclose(f) != 0) ok = 0; + if (ok) { +#ifdef _WIN32 + ok = MoveFileExA(temporary, c->filepath, + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; +#else + ok = rename(temporary, c->filepath) == 0; +#endif + } + if (!ok) remove(temporary); + return ok ? 0 : -1; +} /* XOR checksum of bytes 0x00-0x7E, stored at 0x7F. */ static uint8_t frame_checksum(const uint8_t *frame) { @@ -118,6 +250,7 @@ static void memcard_ensure_dir(const char* dir) { } void memcard_init_slots(const char* dir, const MemcardSlotConfig slots[2]) { + for (int i = 0; i < MAX_CARDS; ++i) discard_extension(&cards[i]); memset(cards, 0, sizeof(cards)); memcard_ensure_dir(dir); @@ -140,9 +273,9 @@ void memcard_init_slots(const char* dir, const MemcardSlotConfig slots[2]) { FILE* f = fopen(cards[i].filepath, "rb"); if (f) { - size_t n = fread(cards[i].data, 1, MEMCARD_SIZE, f); + int result = load_card_image(i, f); fclose(f); - if (n == MEMCARD_SIZE) { + if (result == 0) { cards[i].present = 1; } } else { @@ -154,6 +287,7 @@ void memcard_init_slots(const char* dir, const MemcardSlotConfig slots[2]) { int close_ok = (fclose(f) == 0); if (n == MEMCARD_SIZE && flush_ok && close_ok) { cards[i].present = 1; + notify_extension_load(i); } } } @@ -213,10 +347,16 @@ int memcard_summary_path(const char* path, MemcardSummary* out) { #ifdef _WIN32 struct _stat64 st; - if (_stat64(path, &st) == 0) out->mtime = (long long)st.st_mtime; + if (_stat64(path, &st) == 0) { + out->mtime = (long long)st.st_mtime; + out->size_bytes = (long long)st.st_size; + } #else struct stat st; - if (stat(path, &st) == 0) out->mtime = (long long)st.st_mtime; + if (stat(path, &st) == 0) { + out->mtime = (long long)st.st_mtime; + out->size_bytes = (long long)st.st_size; + } #endif return 0; } @@ -245,15 +385,37 @@ void memcard_flush(int card) { if (!cards[card].dirty) return; if (cards[card].filepath[0] == '\0') return; - FILE* f = fopen(cards[card].filepath, "wb"); - if (f) { - size_t n = fwrite(cards[card].data, 1, MEMCARD_SIZE, f); - int flush_ok = (fflush(f) == 0); - int close_ok = (fclose(f) == 0); - if (n == MEMCARD_SIZE && flush_ok && close_ok) { - cards[card].dirty = 0; + MemCard* c = &cards[card]; + uint8_t* updated = NULL; + const uint8_t* tail = c->extension; + uint32_t size = c->extension_size; + if (c->extension_read_failed || c->extension_codec_failed) { + c->flush_result = -2; + return; + } + if (extension_save) { + updated = (uint8_t*)malloc(MEMCARD_EXTENSION_MAX); + if (!updated) { c->flush_result = -1; return; } + int count = extension_save(card, c->data, updated, MEMCARD_EXTENSION_MAX); + if (count < 0 || (uint32_t)count > MEMCARD_EXTENSION_MAX) { + free(updated); + c->flush_result = -2; + return; + } + tail = updated; + size = (uint32_t)count; + } + c->flush_result = publish_card(c, tail, size); + if (c->flush_result == 0) { + c->dirty = 0; + if (updated) { + free(c->extension); + c->extension = updated; + c->extension_size = size; + updated = NULL; } } + free(updated); } void memcard_flush_all(void) { @@ -302,10 +464,12 @@ int memcard_import_raw(int card, const uint8_t *src) { if (card < 0 || card >= MAX_CARDS || !src) return -1; if (cards[card].filepath[0] == '\0') return -1; memcpy(cards[card].data, src, MEMCARD_SIZE); + discard_extension(&cards[card]); + notify_extension_load(card); cards[card].present = 1; cards[card].dirty = 1; memcard_flush(card); - return 0; + return cards[card].flush_result; } int memcard_rebind_dir(const char *dir) { @@ -365,17 +529,19 @@ int memcard_reload_bound(void) { for (i = 0; i < MAX_CARDS; i++) { FILE *f; if (cards[i].filepath[0] == '\0') { + discard_extension(&cards[i]); cards[i].present = 0; cards[i].dirty = 0; continue; } f = fopen(cards[i].filepath, "rb"); if (!f) { + discard_extension(&cards[i]); cards[i].present = 0; cards[i].dirty = 0; continue; } - if (fread(cards[i].data, 1, MEMCARD_SIZE, f) == MEMCARD_SIZE) { + if (load_card_image(i, f) == 0) { cards[i].present = 1; cards[i].dirty = 0; } else { diff --git a/runtime/src/mod_packages.cpp b/runtime/src/mod_packages.cpp index 43353cf6..3409c467 100644 --- a/runtime/src/mod_packages.cpp +++ b/runtime/src/mod_packages.cpp @@ -1580,6 +1580,21 @@ std::vector mod_instruction_hooks(const std::string& id) { ? std::vector{} : found->second.instructions; } +bool mod_register_memory_address_plugin(const std::string& id, uint32_t address, + uint32_t expected, PSXModMemoryAddressCallback callback) { + const uint32_t op = expected >> 26; + const bool memory = (op >= 0x20 && op <= 0x26) || op == 0x28 || op == 0x29 || + op == 0x2a || op == 0x2b || op == 0x2e || op == 0x32 || op == 0x3a; + if (!memory || !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 && hook.expected == expected && hook.memory_address) return false; + hooks.push_back({key, expected, nullptr, callback}); + return true; +} + bool mod_register_savestate_plugin(const std::string& id, void (*callback)(void)) { if (!valid_id(id) || !callback) return false; RegisteredPlugin& plugin = registered_plugins()[id]; diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 7512631a..acc93f70 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -84,6 +84,11 @@ 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; +static void (*g_progress_hook)(void) = nullptr; + +extern "C" void mod_runtime_set_progress_hook(void (*hook)(void)) { + g_progress_hook = hook; +} namespace PSXRecompV4 { namespace { @@ -295,6 +300,7 @@ struct ActiveInstructionHook { uint32_t key, expected; PSXModFunctionEntryCallback callback; const ModResolution::Plugin* plugin; + PSXModMemoryAddressCallback memory_address; }; std::vector& active_instruction_hooks() { static std::vector value; @@ -350,7 +356,7 @@ void build_function_entry_hooks(const RuntimeMods& s) { 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}); + instructions.push_back({hook.address, hook.expected, hook.callback, &plugin, hook.memory_address}); 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(); @@ -2842,10 +2848,15 @@ extern "C" int psx_mod_read_disc_file(const char* path, void* buffer, if (capacity < bytes) return 0; uint8_t sector[2048]; for (uint32_t offset = 0; offset < bytes; offset += 2048u) { + /* Plugin callbacks may read a large archive during gameplay. + * Account for host I/O just as the resident-sector reader does; + * otherwise its first guest instruction can trip the watchdog. */ + if (g_progress_hook && !(offset & ((2048u * 1024u) - 1u))) g_progress_hook(); if (!read_effective_user_sector(reader, lba + offset / 2048u, sector)) return 0; const uint32_t count = std::min(2048u, bytes - offset); std::memcpy(static_cast(buffer) + offset, sector, count); } + if (g_progress_hook) g_progress_hook(); *size = bytes; return 1; } catch (...) { return 0; } @@ -3384,7 +3395,7 @@ extern "C" void psx_mod_instruction(CPUState* cpu, uint32_t address, uint32_t in 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; + if (!it->callback || 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); @@ -3393,6 +3404,33 @@ extern "C" void psx_mod_instruction(CPUState* cpu, uint32_t address, uint32_t in } } +extern "C" int psx_mod_register_memory_address_plugin(const char* id, uint32_t pc, + uint32_t expected, PSXModMemoryAddressCallback callback) { + return id && PSXRecompV4::mod_register_memory_address_plugin(id, pc, expected, callback); +} + +extern "C" uint32_t psx_mod_memory_address(const CPUState* cpu, uint32_t pc, + uint32_t instruction, uint32_t address) { + using namespace PSXRecompV4; + if (!g_psx_mod_instruction_hooks || !cpu || pc >= 0xC0000000u || mod_plan_hooks_parked()) + return address; + const auto& hooks = active_instruction_hooks(); + const uint32_t key = function_entry_key(pc); + 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->memory_address || it->expected != instruction || psx_mod_read_word(pc) != instruction) continue; + PluginCallbackScope scope(state(), it->plugin, nullptr); + const uint32_t next = it->memory_address(cpu, pc, instruction, address); + if ((next ^ address) & 3u) { + psx_mod_counter_add("mod.memory_address.bad_alignment", 1); + continue; + } + address = next; + } + return address; +} + static int game_netplay_function_entry(CPUState* cpu, uint32_t address); /* Netplay host-output plugins ([[plugin]] netplay = "host_output") run their diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index 8427c61d..4a939b35 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -2634,6 +2634,8 @@ static void init_callbacks(void) { 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; + extern uint32_t psx_mod_memory_address(const CPUState*, uint32_t, uint32_t, uint32_t); + s_callbacks.mod_memory_address = psx_mod_memory_address; } /* ABI v14: GTE precision-store tracker — the emitter emits a direct * gte_precision_store_word() call for every swc2 (GTE store-word), diff --git a/runtime/src/sio.c b/runtime/src/sio.c index d5febbfb..2d82ca85 100644 --- a/runtime/src/sio.c +++ b/runtime/src/sio.c @@ -1914,9 +1914,13 @@ static void mc_process_byte(uint8_t tx_byte) { sio_stat |= SIO_STAT_ACK; if (tx_byte == expected && mc_sector < MEMCARD_SECTORS) { - memcard_write_sector(mc_slot, mc_sector, mc_data); - memcard_flush(mc_slot); - mc_checksum = 0x47; /* Good */ + if (memcard_write_sector(mc_slot, mc_sector, mc_data) == 0) { + memcard_flush(mc_slot); + mc_checksum = memcard_last_flush_result(mc_slot) == 0 + ? 0x47 : 0x4E; /* Good / write failed */ + } else { + mc_checksum = 0x4E; + } } else if (mc_sector >= MEMCARD_SECTORS) { mc_checksum = 0xFF; /* Bad sector */ } else { diff --git a/runtime/tests/test_draw_distance_interp.c b/runtime/tests/test_draw_distance_interp.c index 5ab4db85..a00a4a2e 100644 --- a/runtime/tests/test_draw_distance_interp.c +++ b/runtime/tests/test_draw_distance_interp.c @@ -51,6 +51,12 @@ uint32_t i_mask; uint64_t psx_cycle_count; uint32_t g_psx_mod_function_entry_hooks; uint32_t g_psx_mod_instruction_hooks; +static uint32_t address_calls, shadow_address; +uint32_t psx_mod_memory_address(const CPUState *cpu, uint32_t pc, uint32_t word, uint32_t address) { + (void)cpu; (void)pc; (void)word; + ++address_calls; + return address + 0x1000u; +} void psx_mod_instruction(CPUState *cpu, uint32_t pc, uint32_t instruction) { (void)cpu; (void)pc; (void)instruction; } @@ -76,9 +82,9 @@ int mdec_recently_active(uint32_t f) { (void)f; return 0; } void gte_execute(CPUState *cpu, uint32_t cmd) { (void)cpu; (void)cmd; } void gte_precision_store_word(uint32_t a, uint8_t r) { (void)a; (void)r; } uint32_t gte_read_ctrl(CPUState *cpu, uint8_t r) { (void)cpu; (void)r; return 0; } -uint32_t gte_read_data(CPUState *cpu, uint8_t r) { (void)cpu; (void)r; return 0; } +uint32_t gte_read_data(CPUState *cpu, uint8_t r) { return cpu->gte_data[r]; } void gte_write_ctrl(CPUState *cpu, uint8_t r, uint32_t v) { (void)cpu; (void)r; (void)v; } -void gte_write_data(CPUState *cpu, uint8_t r, uint32_t v) { (void)cpu; (void)r; (void)v; } +void gte_write_data(CPUState *cpu, uint8_t r, uint32_t v) { cpu->gte_data[r] = v; } int overlay_loader_call_native(CPUState *cpu, uint32_t a) { (void)cpu; (void)a; return 0; } void psx_bail_record(uint32_t ra, uint32_t sp, uint32_t pc, uint32_t gsp) { (void)ra; (void)sp; (void)pc; (void)gsp; @@ -97,17 +103,17 @@ void psx_pgxp_alu(struct CPUState *c, uint32_t i, uint32_t r, uint32_t a, uint32 (void)c; (void)i; (void)r; (void)a; (void)b; } void psx_pgxp_cop2(struct CPUState *c, uint32_t i, uint32_t v, uint32_t a) { - (void)c; (void)i; (void)v; (void)a; + (void)c; (void)i; (void)v; shadow_address = a; } void psx_pgxp_load(struct CPUState *c, uint32_t i, uint32_t a, uint32_t v) { - (void)c; (void)i; (void)a; (void)v; + (void)c; (void)i; (void)v; shadow_address = a; } void psx_pgxp_muldiv(struct CPUState *c, uint32_t i, uint32_t h, uint32_t l, uint32_t a, uint32_t b) { (void)c; (void)i; (void)h; (void)l; (void)a; (void)b; } void psx_pgxp_store(struct CPUState *c, uint32_t i, uint32_t a, uint32_t v) { - (void)c; (void)i; (void)a; (void)v; + (void)c; (void)i; (void)v; shadow_address = a; } void psx_rfe_mark_escape(void) {} int psx_syscall(CPUState *cpu, uint32_t code) { (void)cpu; (void)code; return 0; } @@ -312,11 +318,52 @@ static void test_interpreter(void) { CHECK(cpu.gpr[V0] == 0x900u && cpu.gpr[AT] == 0u, "off again: vanilla"); } +static void write_byte(uint32_t a, uint8_t v) { test_ram[a & 0x1fffffu] = v; } +static void write_half(uint32_t a, uint16_t v) { memcpy(test_ram + (a & 0x1fffffu), &v, 2); } +static void test_memory_addresses(void) { + const uint32_t ops[] = {0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x28,0x29,0x2a,0x2b,0x2e,0x32,0x3a}; + /* Compare real interpreted instructions on relocated memory against the + * same instructions on the original memory, including unaligned lanes. */ + for (unsigned i = 0; i < sizeof ops / sizeof ops[0]; ++i) { + for (unsigned lane = 0; lane < 4; ++lane) { + uint32_t op = ops[i]; + if (lane && op != 0x22 && op != 0x26 && op != 0x2a && op != 0x2e) continue; + CPUState vanilla = {0}, adapted = {0}; + vanilla.read_word = psx_read_word; vanilla.read_half = psx_read_half; + vanilla.read_byte = psx_read_byte; + vanilla.write_word = put_word; vanilla.write_half = write_half; vanilla.write_byte = write_byte; + vanilla.gpr[5] = 0x80008004u; + vanilla.gpr[3] = 0x87654321u; vanilla.gte_data[3] = 0x87654321u; + adapted = vanilla; + put_word(0x80008000u, 0xabcdef01u); put_word(0x80009000u, 0xabcdef01u); + const uint32_t word = (op << 26) | (5u << 21) | (3u << 16) | (0xfffcu + lane); + g_psx_mod_instruction_hooks = 0; + step(&vanilla, TEXT_LO, word); + const uint32_t expected = psx_read_word(0x80008000u); + address_calls = 0; g_psx_mod_instruction_hooks = 1; + step(&adapted, TEXT_LO, word); + CHECK(adapted.gpr[3] == vanilla.gpr[3] && adapted.gte_data[3] == vanilla.gte_data[3], + "adapted load/merge/GTE result op=%x lane=%u", op, lane); + CHECK(psx_read_word(0x80009000u) == expected && psx_read_word(0x80008000u) == expected, + "adapted store/merge result op=%x lane=%u", op, lane); + CHECK(adapted.gpr[5] == 0x80008004u && address_calls == 1 && shadow_address == 0x80009000u + lane, + "adapter preserves base and PGXP address op=%x lane=%u", op, lane); + } + } + CPUState cpu = {0}; cpu.read_word = psx_read_word; + cpu.gpr[5] = 0x80008000u; put_word(0x80009000u, 0x12345678u); + step(&cpu, TEXT_LO, 0x8ca50000u); /* lw a1,0(a1) */ + CHECK(cpu.gpr[5] == 0x12345678u && shadow_address == 0x80009000u, + "load destination may alias original base register"); + g_psx_mod_instruction_hooks = 0; +} + int main(int argc, char **argv) { if (argc > 1) s_capture_path = argv[1]; test_store(); test_switch(); test_interpreter(); + test_memory_addresses(); if (failures) { fprintf(stderr, "%d check(s) failed\n", failures); return 1; diff --git a/runtime/tests/test_memcard_extensions.c b/runtime/tests/test_memcard_extensions.c new file mode 100644 index 00000000..90ce68f4 --- /dev/null +++ b/runtime/tests/test_memcard_extensions.c @@ -0,0 +1,214 @@ +#include "memcard.h" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "line %d: %s\n", __LINE__, #x); exit(1); } } while (0) +#define PATH0 "card1.mcd" +#define PATH1 "card2.mcd" +#define SECTOR 400 +static uint8_t base[MEMCARD_SIZE], saved[MEMCARD_SIZE]; +static int loads, save_reject, load_reject, have_tail[2]; +static uint32_t large_size; + +static int large_load(int card, const uint8_t* raw, const uint8_t* tail, uint32_t size) { + CHECK(card == 0 || card == 1); + CHECK(raw[0] == 'M' && raw[1] == 'C'); + CHECK(size == (card == 0 ? large_size : 0)); + for (uint32_t i = 0; i < size; ++i) CHECK(tail[i] == 'X'); + return 0; +} + +static int large_save(int card, const uint8_t* raw, uint8_t* tail, uint32_t capacity) { + CHECK(raw[0] == 'M' && raw[1] == 'C'); + CHECK(capacity == MEMCARD_EXTENSION_MAX); + if (card != 0) return 0; + CHECK(large_size <= capacity); + memset(tail, 'X', large_size); + return (int)large_size; +} + +static long file_size(const char* path) { + FILE* f = fopen(path, "rb"); + CHECK(f && fseek(f, 0, SEEK_END) == 0); + long result = ftell(f); + CHECK(fclose(f) == 0); + return result; +} + +static void append_tail(uint32_t size) { + FILE* f = fopen(PATH0, "ab"); + CHECK(f); + for (uint32_t i = 0; i < size; ++i) CHECK(fputc('X', f) != EOF); + CHECK(fclose(f) == 0); +} + +static void read_base(uint8_t* out) { + FILE* f = fopen(PATH0, "rb"); + CHECK(f && fread(out, 1, MEMCARD_SIZE, f) == MEMCARD_SIZE); + CHECK(fclose(f) == 0); +} + +static int codec_load(int card, const uint8_t* raw, const uint8_t* tail, + uint32_t size) { + CHECK(card == 0 || card == 1); + CHECK(raw[0] == 'M' && raw[1] == 'C'); + CHECK(size == 0 || (size == 5 && tail)); + ++loads; + have_tail[card] = size != 0; + return load_reject ? -1 : 0; +} + +static int codec_save(int card, const uint8_t* raw, uint8_t* tail, + uint32_t capacity) { + if (save_reject) return save_reject == 1 ? -1 : (int)capacity + 1; + if (!have_tail[card]) return 0; + CHECK(capacity >= 5); + memcpy(tail, "TAIL", 4); + tail[4] = raw[SECTOR * MEMCARD_SECTOR_SIZE]; + return 5; +} + +static void write_sector(uint8_t value) { + uint8_t sector[MEMCARD_SECTOR_SIZE]; + memset(sector, value, sizeof sector); + CHECK(memcard_write_sector(0, SECTOR, sector) == 0); +} + +static int dirty(void) { + int value; + CHECK(memcard_debug_info(0, NULL, NULL, NULL, &value) == 0); + return value; +} + +int main(void) { + const MemcardSlotConfig slots[2] = {{PATH0, 1}, {PATH1, 1}}; + MemcardSummary summary; + CHECK(memcard_format_file(PATH0) == 0); + CHECK(memcard_format_file(PATH1) == 0); + append_tail(5); + + /* Inactive mods cannot silently truncate metadata, and the device/raw + * export must still expose exactly the ordinary card bytes. */ + memcard_set_extension_codec(NULL, NULL); + CHECK(!memcard_extension_codec_active()); + memcard_init_slots(NULL, slots); + CHECK(memcard_export_raw(0, base) == 0); + CHECK(base[0] == 'M' && base[1] == 'C'); + write_sector(17); + memcard_flush(0); + CHECK(memcard_last_flush_result(0) == 0 && !dirty()); + CHECK(file_size(PATH0) == MEMCARD_SIZE + 5); + FILE* f = fopen(PATH0, "rb"); + CHECK(f && fseek(f, MEMCARD_SIZE, SEEK_SET) == 0); + for (int i = 0; i < 5; ++i) CHECK(fgetc(f) == 'X'); + CHECK(fclose(f) == 0); + CHECK(memcard_summary_path(PATH0, &summary) == 0); + CHECK(summary.valid && summary.size_bytes == MEMCARD_SIZE + 5); + + /* A selected codec sees both slots and binds the extension to the base + * in the SAME publication, after the actual native writes. */ + memcard_set_extension_codec(codec_load, codec_save); + CHECK(memcard_extension_codec_active()); + memcard_init_slots(NULL, slots); + CHECK(loads == 2 && have_tail[0] && !have_tail[1]); + write_sector(39); + memcard_flush_all(); + CHECK(memcard_last_flush_result(0) == 0 && !dirty()); + CHECK(file_size(PATH0) == MEMCARD_SIZE + 5); + f = fopen(PATH0, "rb"); + CHECK(f && fseek(f, MEMCARD_SIZE, SEEK_SET) == 0); + char tail[5]; + CHECK(fread(tail, 1, 5, f) == 5); + CHECK(memcmp(tail, "TAIL", 4) == 0 && tail[4] == 39); + CHECK(fclose(f) == 0); + read_base(saved); + + /* Rejection or an impossible returned length must leave the original + * file intact and the buffered native write dirty for a later retry. */ + write_sector(54); + for (save_reject = 1; save_reject <= 2; ++save_reject) { + memcard_flush(0); + CHECK(memcard_last_flush_result(0) == -2 && dirty()); + read_base(base); + CHECK(memcmp(base, saved, sizeof base) == 0); + CHECK(file_size(PATH0) == MEMCARD_SIZE + 5); + } + save_reject = 0; + memcard_flush(0); + CHECK(memcard_last_flush_result(0) == 0 && !dirty()); + read_base(saved); + + load_reject = 1; + CHECK(memcard_reload_bound() == 0); + write_sector(71); + memcard_flush(0); + CHECK(memcard_last_flush_result(0) == -2 && dirty()); + read_base(base); + CHECK(memcmp(base, saved, sizeof base) == 0); + /* Re-registering the same codec cannot bypass its rejected load. */ + memcard_set_extension_codec(codec_load, codec_save); + memcard_flush(0); + CHECK(memcard_last_flush_result(0) == -2 && dirty()); + read_base(base); + CHECK(memcmp(base, saved, sizeof base) == 0); + load_reject = 0; + CHECK(memcard_reload_bound() == 0); + + /* The former cap, its first new byte, and the exact new cap all survive + * both an absent codec and a codec refresh/reload, byte for byte. */ + const uint32_t sizes[] = {65536u, 65537u, MEMCARD_EXTENSION_MAX}; + for (unsigned i = 0; i < sizeof sizes / sizeof sizes[0]; ++i) { + large_size = sizes[i]; + memcard_set_extension_codec(NULL, NULL); + CHECK(memcard_format_file(PATH0) == 0); + append_tail(large_size); + CHECK(memcard_reload_bound() == 0); + write_sector(83); + memcard_flush(0); + CHECK(memcard_last_flush_result(0) == 0 && !dirty()); + CHECK(file_size(PATH0) == (long)(MEMCARD_SIZE + large_size)); + memcard_set_extension_codec(large_load, large_save); + CHECK(memcard_reload_bound() == 0); + write_sector(84); + memcard_flush(0); + CHECK(memcard_last_flush_result(0) == 0 && !dirty()); + CHECK(file_size(PATH0) == (long)(MEMCARD_SIZE + large_size)); + CHECK(memcard_reload_bound() == 0); + } + + /* Oversized tails cannot be accidentally replaced with a shorter image. */ + memcard_set_extension_codec(NULL, NULL); + CHECK(memcard_format_file(PATH0) == 0); + append_tail(MEMCARD_EXTENSION_MAX + 1u); + CHECK(memcard_reload_bound() == 0); + write_sector(91); + memcard_flush(0); + CHECK(memcard_last_flush_result(0) == -2 && dirty()); + CHECK(file_size(PATH0) == MEMCARD_SIZE + MEMCARD_EXTENSION_MAX + 1u); + + /* Raw import is explicit replacement, so previous extension ownership + * does not leak into a different standard card (e.g. netplay sandbox). */ + CHECK(memcard_import_raw(0, saved) == 0); + CHECK(file_size(PATH0) == MEMCARD_SIZE); + + /* Publication failure leaves dirty data and does not touch the old card. */ +#ifdef _WIN32 + (void)_mkdir("blocked_target"); +#else + (void)mkdir("blocked_target", 0700); +#endif + CHECK(memcard_rebind_path(0, "blocked_target") == 0); + write_sector(105); + memcard_flush(0); + CHECK(memcard_last_flush_result(0) == -1 && dirty()); + read_base(base); + CHECK(memcmp(base, saved, sizeof base) == 0); + memcard_set_extension_codec(NULL, NULL); + puts("memcard extensions: preservation, refresh, rejection, raw import and atomic failure passed"); + return 0; +} diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index de8ce399..0a6cd56d 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -238,6 +238,12 @@ static int test_inactive_filter(CPUState*, uint32_t) { } static int guest_function_hits; static int instruction_hits; +static int memory_hits; +static uint32_t memory_delta = 0x1000u; +static uint32_t test_memory_address(const CPUState*, uint32_t, uint32_t, uint32_t address) { + ++memory_hits; + return address + memory_delta; +} static void test_instruction(CPUState* cpu, uint32_t) { instruction_hits++; cpu->gpr[5] = 0x80301008u; @@ -663,6 +669,17 @@ int main(int argc, char** argv) { !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_memory_address_plugin("runtime.test-vblank", 0x80003008u, + 0x8CA30014u, test_memory_address), "register effective address callback"); + check(!psx_mod_register_memory_address_plugin("runtime.test-vblank", 0xA0003008u, + 0x8CA30014u, test_memory_address) && + !psx_mod_register_memory_address_plugin("runtime.test-vblank", 0x80003009u, + 0x8CA30014u, test_memory_address) && + !psx_mod_register_memory_address_plugin("runtime.test-vblank", 0x8000300cu, + 0x24020001u, test_memory_address), "reject duplicate, unaligned and non-memory adapters"); + check(psx_mod_register_memory_address_plugin("runtime.unselected-entry", 0x80003008u, + 0x8CA30014u, test_memory_address), "register inactive adapter"); + psx_mod_write_word(0x80003008u, 0x8CA30014u); check(psx_mod_register_guest_function_plugin( "runtime.test-vblank", 0x8FFF0000u, test_guest_function), "trusted callback registers outside the hardware map"); @@ -689,6 +706,8 @@ int main(int argc, char** argv) { /* 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_memory_address(&entry_cpu, 0x80003008u, 0x8CA30014u, 0x80004000u) == 0x80004000u && + !memory_hits, "address adapters await activation"); check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) && g_psx_mod_guest_functions == 0, "committed guest functions remain unavailable until activation"); @@ -699,6 +718,21 @@ int main(int argc, char** argv) { check(activation_calls == 1, "resolved trusted plugin must activate before runtime startup"); entry_cpu.pc = 0x80004000u; + const CPUState memory_before = entry_cpu; + check(psx_mod_memory_address(&entry_cpu, 0x80003008u, 0x8CA30014u, 0x80004000u) == 0x80005000u && + psx_mod_memory_address(&entry_cpu, 0xA0003008u, 0x8CA30014u, 0x80004003u) == 0x80005003u && + memory_hits == 2 && !std::memcmp(&entry_cpu, &memory_before, sizeof entry_cpu), + "active adapter handles aliases and preserves CPU and merge lanes"); + check(psx_mod_memory_address(&entry_cpu, 0x80003008u, 0x8CA30015u, 0x80004000u) == 0x80004000u, + "address adapter rejects wrong emitted word"); + psx_mod_write_word(0x80003008u, 0x8CA30015u); + check(psx_mod_memory_address(&entry_cpu, 0x80003008u, 0x8CA30014u, 0x80004000u) == 0x80004000u && + memory_hits == 2, "address adapter rejects changed overlay word"); + psx_mod_write_word(0x80003008u, 0x8CA30014u); + memory_delta = 1; + check(psx_mod_memory_address(&entry_cpu, 0x80003008u, 0x8CA30014u, 0x80004000u) == 0x80004000u, + "misaligned adapter result is ignored"); + memory_delta = 0x1000u; psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u); psx_mod_instruction(&entry_cpu, 0xA0003004u, 0x90A30014u); check(instruction_hits == 2 && entry_cpu.gpr[5] == 0x80301008u && @@ -730,6 +764,8 @@ int main(int argc, char** argv) { 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_memory_address(&entry_cpu, 0x80003008u, 0x8CA30014u, 0x80004000u) == 0x80004000u, + "clearing removes address adapters"); check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) && g_psx_mod_guest_functions == 0, "clearing the plan drops guest callback availability"); @@ -1219,11 +1255,16 @@ int main(int argc, char** argv) { check(PSXRecompV4::mod_runtime_initialize(disc_root,"READER",0,{},&error),"reader initialize"); check(PSXRecompV4::mod_runtime_commit(iso_path,&error),"reader mount original ISO"); uint32_t bytes=0; + static unsigned host_read_progress = 0; + mod_runtime_set_progress_hook([] { ++host_read_progress; }); check(psx_mod_read_disc_file("S0/LEVEL.NSF",nullptr,0,&bytes) && bytes==3000,"query original nested file size"); std::vector result(3000); check(!psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),2999,&bytes) && bytes==0,"undersized destination rejected"); + check(host_read_progress == 0, "size query and rejected destination do not report reading"); check(psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),(uint32_t)result.size(),&bytes) && bytes==3000 && std::equal(result.begin(),result.end(),iso.begin()+22*2048),"complete original file bytes"); + check(host_read_progress >= 2, "whole-file plugin reads report progress and completion"); + mod_runtime_set_progress_hook(nullptr); check(!psx_mod_read_disc_file("S0/MISSING.NSF",nullptr,0,&bytes) && bytes==0,"missing file explicit"); check(!psx_mod_read_disc_file("S0",nullptr,0,&bytes),"directory rejected"); std::vector raw_iso(24*2352); diff --git a/runtime/tests/test_overlay_memory_address.c b/runtime/tests/test_overlay_memory_address.c new file mode 100644 index 00000000..7f82942a --- /dev/null +++ b/runtime/tests/test_overlay_memory_address.c @@ -0,0 +1,32 @@ +#define PSX_OVERLAY_DLL_BUILD 1 +#include "overlay_dispatch_preamble.c.inc" +#include +#include +#include +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "line %d: %s\n", __LINE__, #x); exit(1); } } while (0) + +static uint32_t charged, calls; +static CPUState* expected_cpu; +static void charge(uint32_t n) { charged += n; } +static uint32_t redirect(const CPUState* cpu, uint32_t pc, uint32_t word, uint32_t address) { + CHECK(cpu == expected_cpu && pc == 0x80010000u && word == 0x8ca30014u); + CHECK(charged == 7u); /* Pending DLL cycles publish before the host callback. */ + ++calls; + return address + 0x1000u; +} +int main(void) { + CPUState cpu = {0}; expected_cpu = &cpu; + OverlayCallbacks callbacks = {0}; callbacks.cpu_charge = charge; + callbacks.mod_memory_address = redirect; + overlay_init(&callbacks); + psx_cpu_charge(7); + CHECK(psx_mod_memory_address(&cpu, 0x80010000u, 0x8ca30014u, 0x80002003u) == 0x80003003u); + CHECK(calls == 1 && charged == 7); + overlay_flush_cycles(); CHECK(charged == 7); + callbacks.mod_memory_address = NULL; + overlay_init(&callbacks); + CHECK(psx_mod_memory_address(&cpu, 0x80010000u, 0x8ca30014u, 0x80002003u) == 0x80002003u); + CHECK(calls == 1 && overlay_abi() == PSX_OVERLAY_ABI_TAG); + puts("overlay address callback and cycle publication: passed"); + return 0; +} diff --git a/runtime/tests/test_sio_card_protocol.c b/runtime/tests/test_sio_card_protocol.c index 36fdd289..62fec5d2 100644 --- a/runtime/tests/test_sio_card_protocol.c +++ b/runtime/tests/test_sio_card_protocol.c @@ -322,6 +322,37 @@ static void test_flag_clears_on_write(void) { EXPECT_EQ("post_write.flag_clear", 0x00, card_xchg(0x52, 0)); } +static int accept_extension(int card, const uint8_t* base, const uint8_t* tail, + uint32_t size) { + (void)card; (void)base; (void)tail; (void)size; + return 0; +} +static int refuse_extension_save(int card, const uint8_t* base, uint8_t* tail, + uint32_t capacity) { + (void)card; (void)base; (void)tail; (void)capacity; + return -1; +} + +static void test_rejected_save_reports_failure(void) { + uint8_t before[128], after[128], data[128], rx[140]; + sio_init(); + memcard_set_extension_codec(accept_extension, refuse_extension_save); + memcard_init("test_dir"); + FILE* f = fopen("test_dir/card1.mcd", "rb"); + EXPECT_EQ("rejected.before_read", 128, f ? fread(before, 1, 128, f) : 0); + if (f) fclose(f); + memset(data, 0x92, sizeof data); + EXPECT_EQ("rejected.count", 138, run_card_write(0, 0, data, rx)); + EXPECT_EQ("rejected.end", 0x4E, rx[137]); + EXPECT_EQ("rejected.flush_result", -2, memcard_last_flush_result(0)); + f = fopen("test_dir/card1.mcd", "rb"); + EXPECT_EQ("rejected.after_read", 128, f ? fread(after, 1, 128, f) : 0); + if (f) fclose(f); + EXPECT_EQ("rejected.disk_unchanged", 0, memcmp(before, after, 128)); + memcard_set_extension_codec(NULL, NULL); + memcard_init("test_dir"); +} + /* Test 3: pad-poll mid-card-read — does our SIO model preserve card state? */ static void test_pad_poll_during_card_read(void) { sio_init(); @@ -499,6 +530,8 @@ int main(int argc, char **argv) { (g_checks - prev_checks) - (g_failures - prev_fails), g_checks - prev_checks); + test_rejected_save_reports_failure(); + prev_checks = g_checks; prev_fails = g_failures; test_pad_poll_during_card_read(); fprintf(stderr, "test_pad_poll_during_card_read: %d/%d ok\n", diff --git a/runtime/tests/test_ws_cull_edge_interp.c b/runtime/tests/test_ws_cull_edge_interp.c index b1e01206..50d662c0 100644 --- a/runtime/tests/test_ws_cull_edge_interp.c +++ b/runtime/tests/test_ws_cull_edge_interp.c @@ -40,6 +40,9 @@ uint32_t g_debug_last_store_pc; uint32_t g_dirty_ram_code_gen; uint64_t g_dispatch_static_hits; uint32_t g_psx_mod_instruction_hooks; +uint32_t psx_mod_memory_address(const CPUState *cpu, uint32_t pc, uint32_t word, uint32_t address) { + (void)cpu; (void)pc; (void)word; return address; +} void psx_mod_instruction(CPUState *cpu, uint32_t pc, uint32_t instruction) { (void)cpu; (void)pc; (void)instruction; abort(); }