mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
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:
@@ -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).
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user