Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions overlay_toolchain/include/mod_plugins.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ extern "C" {

typedef void (*PSXModVBlankCallback)(void);
typedef void (*PSXModActivationCallback)(void);
/* Emulation-thread stereo S16 at 44100 Hz, after SPU/shadow synthesis, before
* capture/host mute. Called for every rendered block, including discarded
* turbo/rollback output. Add with saturation; never call guest code here.
* Keep mutable cursors in allocated guest memory for save/rewind/rollback.
* Host audio/device state must not affect sample generation. */
typedef void (*PSXModPCMCallback)(int16_t* stereo, uint32_t frames);
struct CPUState;
typedef void (*PSXModFunctionEntryCallback)(struct CPUState* cpu,
uint32_t address);
Expand All @@ -34,6 +40,7 @@ int psx_mod_register_activation_plugin(const char* id,
PSXModActivationCallback callback);
int psx_mod_register_vblank_plugin(const char* id,
PSXModVBlankCallback callback);
int psx_mod_register_pcm_plugin(const char* id, PSXModPCMCallback callback);
/* Runs after successful guest-state restore, before the restored PC resumes.
* Rebind host hooks here; do not advance guest gameplay as a VBlank would. */
int psx_mod_register_savestate_plugin(const char* id,
Expand Down Expand Up @@ -262,14 +269,14 @@ void psx_mod_set_world_scene_predicate(PSXModWorldScenePredicate predicate);
* player left it in the launcher (or the manifest default when untouched).
* Writes a NUL-terminated string into `out` and returns 1; returns 0 with
* out[0] = '\0' when the plan is not committed, the ids do not resolve, or the
* value does not fit — the caller then applies its own default rather than
* value does not fit — the caller then applies its own default rather than
* treating an empty string as a selection.
*
* Why this exists: the manifest schema already carries typed, validated,
* launcher-rendered, persisted options ([[option]] boolean/choice/integer), but
* an activation callback takes no arguments and had no way to read them, so a
* trusted plugin could only ever be an on/off switch. A parameterised feature
* then had to be modelled as one feature per value — and `constraint` only
* then had to be modelled as one feature per value — and `constraint` only
* expresses ordered_integer WITHIN a feature, so those pseudo-features could
* not even be made mutually exclusive. This closes that gap: one feature, one
* option, the plugin reads what was chosen.
Expand Down
2 changes: 2 additions & 0 deletions runtime/include/mod_packages.h
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,8 @@ bool mod_register_activation_plugin(const std::string& id, void (*callback)(void
bool mod_register_session_reset_plugin(const std::string& id, void (*callback)(void));
void mod_reset_plugin_sessions();
bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void));
bool mod_register_pcm_plugin(const std::string& id, PSXModPCMCallback callback);
PSXModPCMCallback mod_pcm_callback(const std::string& id);
bool mod_register_savestate_plugin(const std::string& id, void (*callback)(void));
void mod_invoke_savestate_plugin(const std::string& id);
/* A function-entry hook is a trusted implementation like the others: manifests
Expand Down
7 changes: 7 additions & 0 deletions runtime/include/mod_plugins.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ extern "C" {

typedef void (*PSXModVBlankCallback)(void);
typedef void (*PSXModActivationCallback)(void);
/* Emulation-thread stereo S16 at 44100 Hz, after SPU/shadow synthesis, before
* capture/host mute. Called for every rendered block, including discarded
* turbo/rollback output. Add with saturation; never call guest code here.
* Keep mutable cursors in allocated guest memory for save/rewind/rollback.
* Host audio/device state must not affect sample generation. */
typedef void (*PSXModPCMCallback)(int16_t* stereo, uint32_t frames);
struct CPUState;
/* A callback that runs guest code inside psx_mod_render_pass() can be
* abandoned by a watchdog longjmp; see the note at psx_mod_render_pass(). */
Expand All @@ -34,6 +40,7 @@ int psx_mod_register_session_reset_plugin(const char* id,
PSXModActivationCallback callback);
int psx_mod_register_vblank_plugin(const char* id,
PSXModVBlankCallback callback);
int psx_mod_register_pcm_plugin(const char* id, PSXModPCMCallback callback);
/* Runs after successful guest-state restore, before the restored PC resumes.
* Rebind host hooks here; do not advance guest gameplay as a VBlank would. */
int psx_mod_register_savestate_plugin(const char* id,
Expand Down
4 changes: 4 additions & 0 deletions runtime/include/savestate.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ int savestate_request_load_blob_protocol(const void* data, size_t size);
/* 1 while a staged save/load has not yet been consumed by savestate_poll. */
int savestate_pending(void);

/* Read-only hot-path hint for interrupt maintenance. Updated with staged
* requests; actual execution still passes every savestate_poll safety gate. */
extern int g_psx_savestate_pending;

/* Machine-readable lifecycle receipt for deterministic debug harnesses. */
void savestate_status_json(char* buf, size_t cap);
typedef struct PSXSavestateStatus {
Expand Down
3 changes: 3 additions & 0 deletions runtime/include/spu.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ int spu_shadow_tap_count(void);

uint32_t spu_snapshot_bytes(void);
int spu_snapshot_len_ok(uint32_t len);
/* Optional host-owned implementation, not serialized. The caller owns any
* mutable playback state and restores it with its guest snapshot. */
void spu_set_pcm_overlay(void (*callback)(int16_t* stereo, uint32_t frames));
void spu_snapshot_write(uint8_t *p);
int spu_snapshot_read(const uint8_t *p, uint32_t len);
uint8_t *spu_get_ram_ptr(void);
Expand Down
16 changes: 16 additions & 0 deletions runtime/src/interrupts.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

#include "interrupts.h"
#include "mod_runtime.h"
#include "savestate.h"
#include "psx_memory.h"
#include "sio.h"
#include "timers.h"
Expand Down Expand Up @@ -1118,6 +1119,15 @@ int psx_interrupt_delivery_needed(const CPUState* cpu) {
return 1;
}

static void poll_staged_user_snapshot(CPUState* cpu) {
extern uint32_t g_dirty_safe_resume_pc;
if (!in_exception && g_psx_savestate_pending) {
uint32_t pc = g_dirty_safe_resume_pc ? g_dirty_safe_resume_pc
: s_compiled_interrupt_resume_pc;
savestate_poll(cpu, pc);
}
}

void psx_check_interrupts(CPUState* cpu) {
/* Frozen guest time (psx_cycle_freeze.h): no delivery, and no
* check-boundary bookkeeping that the stock timeline would not do. */
Expand Down Expand Up @@ -1197,6 +1207,12 @@ void psx_check_interrupts(CPUState* cpu) {
}
}

/* Quiet native/HLE gameplay may execute far fewer than 0x4000 BB checks
* in a frame. A staged user snapshot must not wait for that amortized
* maintenance counter: short actions/voices can finish before capture.
* Keep exception, host-adapter and resume-PC gates in savestate_poll, and
* retain the canonical netplay edge gate above. Idle work stays amortized. */
poll_staged_user_snapshot(cpu);
s_irq_path_entry++;

/* MotK VLC / FMV hot edge: sticky unmasked I_STAT (CD/VBlank) while
Expand Down
24 changes: 22 additions & 2 deletions runtime/src/mod_packages.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ struct RegisteredPlugin {
PSXModActivationCallback activation = nullptr;
PSXModActivationCallback session_reset = nullptr;
PSXModVBlankCallback vblank = nullptr;
PSXModPCMCallback pcm = nullptr;
/* Generated-function entry hooks, keyed by guest address. One id may
* observe several functions. */
std::vector<ModFunctionEntryHook> function_entries;
Expand Down Expand Up @@ -1527,6 +1528,19 @@ bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void)) {
return true;
}

bool mod_register_pcm_plugin(const std::string& id, PSXModPCMCallback callback) {
if (!valid_id(id) || !callback) return false;
auto& plugin = registered_plugins()[id];
if (plugin.pcm) return false;
plugin.pcm = callback;
return true;
}

PSXModPCMCallback mod_pcm_callback(const std::string& id) {
const auto found = registered_plugins().find(id);
return found == registered_plugins().end() ? nullptr : found->second.pcm;
}

bool mod_register_function_entry_plugin(const std::string& id, uint32_t address,
PSXModFunctionEntryCallback callback) {
if (!valid_id(id) || !address || !callback) return false;
Expand Down Expand Up @@ -1559,7 +1573,8 @@ bool mod_register_function_override_marker(const std::string& id) {
bool mod_plugin_registered(const std::string& id) {
const auto found = registered_plugins().find(id);
return found != registered_plugins().end() &&
(found->second.activation || found->second.session_reset || found->second.vblank || found->second.savestate ||
(found->second.activation || found->second.session_reset || found->second.vblank ||
found->second.savestate || found->second.pcm ||
!found->second.function_entries.empty() || !found->second.guest_functions.empty() ||
!found->second.instructions.empty() || found->second.function_override);
}
Expand Down Expand Up @@ -1657,7 +1672,8 @@ std::vector<std::string> mod_registered_plugin_ids() {
std::vector<std::string> ids;
for (const auto& entry : registered_plugins())
if (entry.second.activation || entry.second.session_reset || entry.second.vblank ||
!entry.second.function_entries.empty() || !entry.second.guest_functions.empty())
entry.second.pcm || entry.second.savestate ||
!entry.second.function_entries.empty() || !entry.second.guest_functions.empty() || !entry.second.instructions.empty())
ids.push_back(entry.first);
return ids; /* std::map keeps them sorted */
}
Expand Down Expand Up @@ -4410,6 +4426,10 @@ extern "C" int psx_mod_register_vblank_plugin(
PSXRecompV4::mod_register_vblank_plugin(id, callback) ? 1 : 0;
}

extern "C" int psx_mod_register_pcm_plugin(const char* id, PSXModPCMCallback callback) {
return id && PSXRecompV4::mod_register_pcm_plugin(id, callback) ? 1 : 0;
}

extern "C" int psx_mod_register_activation_plugin(
const char* id, PSXModActivationCallback callback) {
return id &&
Expand Down
19 changes: 19 additions & 0 deletions runtime/src/mod_runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "host_launch_timing.h"
#include "host_overlay.h"
#include "cpu_state.h"
#include "spu.h"

#include "disc_path.h"
#include "host_file_identity.h"
Expand Down Expand Up @@ -308,6 +309,18 @@ std::vector<ActiveFunctionEntryHook>& active_function_entry_hooks() {
}
unsigned function_entry_depth;

struct ActivePCMHook {
PSXModPCMCallback callback;
const ModResolution::Plugin* plugin;
};
std::vector<ActivePCMHook> pcm_hooks;
void mix_plugin_pcm(int16_t* stereo, uint32_t frames) {
for (const auto& hook : pcm_hooks) {
PluginCallbackScope scope(state(), hook.plugin);
hook.callback(stereo, frames);
}
}

std::vector<ActiveFunctionEntryHook>& active_guest_functions() {
static std::vector<ActiveFunctionEntryHook> value;
return value;
Expand Down Expand Up @@ -342,6 +355,8 @@ void update_function_entry_count() {
}

void clear_function_entry_hooks() {
spu_set_pcm_overlay(nullptr);
pcm_hooks.clear();
active_function_entry_hooks().clear();
update_function_entry_count();
active_guest_functions().clear();
Expand Down Expand Up @@ -377,6 +392,10 @@ void build_function_entry_hooks(const RuntimeMods& s) {
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();
for (const auto& plugin : s.plan.plugins)
if (auto callback = mod_pcm_callback(plugin.id))
pcm_hooks.push_back({callback, &plugin});
if (!pcm_hooks.empty()) spu_set_pcm_overlay(mix_plugin_pcm);
}

/* Function overrides queue here at constructor time and are ARMED into the
Expand Down
22 changes: 19 additions & 3 deletions runtime/src/savestate.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,13 @@ static int s_content_scope_valid = 1;
static char s_content_token[71]; /* "_mods_" + complete SHA-256 + NUL */
static int s_save_pending = -1; /* slot, or -1 */
static int s_load_pending = -1;
int g_psx_savestate_pending = 0;
static int s_load_completed = 0;
static int s_load_failed = 0;
static uint32_t s_status_generation = 0;
static int s_status_pending = 0;
static uint32_t s_status_polls;
static const char* s_status_wait = "";
static int s_status_last_ok = 0;
static int s_status_last_load = 0;
static int s_status_last_slot = -1;
Expand Down Expand Up @@ -788,6 +791,9 @@ static int request_save_inner(int slot) {
s_last_save_pc = 0; /* block netplay transfer until this write stamps a PC */
s_save_defer_slot = -1;
s_save_pending = slot;
s_status_polls = 0;
s_status_wait = "boundary";
g_psx_savestate_pending = 1;
s_status_pending = 1;
s_status_last_load = 0;
s_status_last_slot = slot;
Expand All @@ -814,6 +820,9 @@ static int request_load_inner(int slot) {
s_load_failed = 0;
s_load_completed = 0;
s_load_pending = slot;
s_status_polls = 0;
s_status_wait = "boundary";
g_psx_savestate_pending = 1;
s_status_pending = 1;
s_status_last_load = 1;
s_status_last_slot = slot;
Expand Down Expand Up @@ -866,6 +875,7 @@ int savestate_request_load_blob_protocol(const void* data, size_t size) {
s_load_failed = 0;
s_load_completed = 0;
s_load_pending = 0; /* non-negative: poll will prefer the blob */
g_psx_savestate_pending = 1;
return 1;
}

Expand All @@ -886,10 +896,11 @@ void savestate_status_json(char* buf, size_t cap) {
if (!buf || cap == 0) return;
snprintf(buf, cap,
"\"generation\":%u,\"pending\":%d,\"last_ok\":%d,"
"\"last_op\":\"%s\",\"last_slot\":%d",
"\"last_op\":\"%s\",\"last_slot\":%d,"
"\"polls\":%u,\"wait\":\"%s\"",
(unsigned)s_status_generation, s_status_pending,
s_status_last_ok, s_status_last_load ? "load" : "save",
s_status_last_slot);
s_status_last_slot, s_status_polls, s_status_pending ? s_status_wait : "");
}

int savestate_take_load_completed(void) {
Expand Down Expand Up @@ -917,7 +928,9 @@ uint32_t savestate_last_save_pc(void) {
void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
if (s_save_pending < 0 && s_load_pending < 0) return; /* hot path: nothing staged */
extern int psx_mod_function_entry_active(void);
if (psx_mod_function_entry_active()) return;
++s_status_polls;
if (psx_mod_function_entry_active()) { s_status_wait = "mod_callback"; return; }
s_status_wait = "resume_pc";

if (s_save_pending >= 0) {
int slot = s_save_pending;
Expand Down Expand Up @@ -950,6 +963,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
if (now - s_save_defer_t0 < 2000.0)
return;
s_save_pending = -1;
g_psx_savestate_pending = s_load_pending >= 0;
s_save_defer_slot = -1;
s_last_save_pc = 0;
s_save_failed = 1;
Expand All @@ -970,6 +984,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
psx_frontend_on_savestate_notify(0, slot, 0);
} else {
s_save_pending = -1;
g_psx_savestate_pending = s_load_pending >= 0;
s_save_defer_slot = -1;
if (pc != resume_pc && resume_pc == 0u) {
fprintf(stderr,
Expand Down Expand Up @@ -1018,6 +1033,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
(s_load_blob && s_load_blob_len > 0) ? "blob" : "file";
#endif
s_load_pending = -1;
g_psx_savestate_pending = s_save_pending >= 0;
char path[600];
const double t_load0 = savestate_mono_ms();
double t_after_boot = t_load0;
Expand Down
7 changes: 7 additions & 0 deletions runtime/src/spu.c
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,11 @@ void spu_init(void) {
spu_output_flush();
}

static void (*pcm_overlay)(int16_t*, uint32_t);
void spu_set_pcm_overlay(void (*callback)(int16_t*, uint32_t)) {
pcm_overlay = callback;
}

static void spu_render_block(int16_t* out_stereo, int frames) {
if (!out_stereo || frames <= 0) return;

Expand Down Expand Up @@ -1248,6 +1253,7 @@ static void spu_render_block(int16_t* out_stereo, int frames) {
last_peak = block_peak;
if (block_peak > peak) peak = block_peak;
spu_shadow_process(out_stereo, frames);
if (pcm_overlay) pcm_overlay(out_stereo, (uint32_t)frames);
audio_trace_pcm(AUDIO_TAP_SPU_OUT, out_stereo, frames);
return;
}
Expand Down Expand Up @@ -1442,6 +1448,7 @@ static void spu_render_block(int16_t* out_stereo, int frames) {
* while proven. No-op (byte-identical) when disabled. The canon mix above
* stays the authoritative output AND the verify oracle. */
spu_shadow_process(out_stereo, frames);
if (pcm_overlay) pcm_overlay(out_stereo, (uint32_t)frames);

/* T1 tap: the SPU's final output block as handed to the host layer
* (post-shadow, pre host fade/mute). Placed here so every spu_render
Expand Down
26 changes: 26 additions & 0 deletions runtime/tests/test_host_snapshot_guard.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,39 @@ int g_cosim_dirty_pump_site;
int g_call_unit_depth;
int g_psx_dispatch_depth;
uint32_t g_dirty_safe_resume_pc;
int g_psx_savestate_pending;
static unsigned snapshot_polls;
static uint32_t snapshot_pc;
void savestate_poll(CPUState* cpu, uint32_t pc) {
(void)cpu; ++snapshot_polls; snapshot_pc = pc;
}

static void check(int ok, const char* message) {
if (!ok) { fprintf(stderr, "FAIL: %s\n", message); exit(1); }
}

int main(void) {
uint32_t pc = 0x80025070u;
CPUState cpu = {0};
s_compiled_interrupt_resume_pc = pc;
poll_staged_user_snapshot(&cpu);
check(snapshot_polls == 0, "idle snapshot work stays amortized");
g_psx_savestate_pending = 1;
poll_staged_user_snapshot(&cpu);
check(snapshot_polls == 1 && snapshot_pc == pc,
"staged request reaches the next native boundary");
in_exception = 1;
poll_staged_user_snapshot(&cpu);
check(snapshot_polls == 1, "exception cannot service a user snapshot");
in_exception = 0;
g_dirty_safe_resume_pc = pc + 4;
poll_staged_user_snapshot(&cpu);
check(snapshot_polls == 2 && snapshot_pc == pc + 4,
"staged dirty-code request uses its own resume boundary");
g_psx_savestate_pending = 0;
poll_staged_user_snapshot(&cpu);
check(snapshot_polls == 2, "completed request stops urgent polling");
g_dirty_safe_resume_pc = 0;
check(psx_irq_resume_context_snapshot_safe_at(pc), "native boundary admitted");
psx_snapshot_host_call_begin();
psx_snapshot_host_call_begin();
Expand Down
Loading
Loading