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.
This commit is contained in:
Matt Stanley
2026-10-19 16:00:11 -07:00
parent abf85e50a4
commit c0ffd6d4c5
30 changed files with 918 additions and 103 deletions
+47
View File
@@ -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).
+23
View File
@@ -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
+6 -2
View File
@@ -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
@@ -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;
+14 -13
View File
@@ -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);
+61 -49
View File
@@ -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<uint8_t>(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";
+2 -2
View File
@@ -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);
};
+2 -1
View File
@@ -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
@@ -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();
+10
View File
@@ -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
+25 -1
View File
@@ -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 */
+4 -1
View File
@@ -640,11 +640,14 @@ 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;
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<ModInstructionHook> 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
+13
View File
@@ -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
+2
View File
@@ -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);
+6 -2
View File
@@ -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
@@ -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;
+4 -4
View File
@@ -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)
+33 -5
View File
@@ -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 <stdint.h>
#include <stdlib.h>
@@ -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
+7
View File
@@ -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();
+179 -13
View File
@@ -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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#ifdef _WIN32
#include <direct.h>
#include <io.h>
#include <process.h>
#include <windows.h>
#else
#include <unistd.h>
#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 {
+15
View File
@@ -1580,6 +1580,21 @@ std::vector<ModInstructionHook> mod_instruction_hooks(const std::string& id) {
? std::vector<ModInstructionHook>{} : 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];
+40 -2
View File
@@ -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<ActiveInstructionHook>& active_instruction_hooks() {
static std::vector<ActiveInstructionHook> 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<uint8_t*>(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
+2
View File
@@ -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),
+7 -3
View File
@@ -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 {
+52 -5
View File
@@ -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;
+214
View File
@@ -0,0 +1,214 @@
#include "memcard.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <direct.h>
#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;
}
+41
View File
@@ -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<uint8_t> 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<uint8_t> raw_iso(24*2352);
@@ -0,0 +1,32 @@
#define PSX_OVERLAY_DLL_BUILD 1
#include "overlay_dispatch_preamble.c.inc"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}
+33
View File
@@ -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",
+3
View File
@@ -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();
}