mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge pull request #767 from RetroPortingToolKit/integrate/marios-hat-master-20261024
Support trusted mod PCM on the guest audio clock and prompt snapshots
This commit is contained in:
@@ -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);
|
||||
@@ -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,
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(). */
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
#include "interrupts.h"
|
||||
#include "mod_runtime.h"
|
||||
#include "savestate.h"
|
||||
#include "psx_memory.h"
|
||||
#include "sio.h"
|
||||
#include "timers.h"
|
||||
@@ -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. */
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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
|
||||
|
||||
+19
-3
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,12 @@ 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); }
|
||||
@@ -12,6 +18,26 @@ static void check(int ok, const char* message) {
|
||||
|
||||
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();
|
||||
|
||||
@@ -50,6 +50,11 @@ static int plugin_calls;
|
||||
static int restore_calls;
|
||||
static int option_renderer_activations;
|
||||
static void option_renderer_activate(void) { ++option_renderer_activations; }
|
||||
static PSXModPCMCallback pcm_callback;
|
||||
extern "C" void spu_set_pcm_overlay(PSXModPCMCallback callback) { pcm_callback = callback; }
|
||||
static void test_pcm(int16_t* samples, uint32_t frames) {
|
||||
for (uint32_t i = 0; i < frames * 2; ++i) samples[i] += 12;
|
||||
}
|
||||
static PsxLobbyMatchCaps test_match_caps;
|
||||
extern "C" const PsxLobbyMatchCaps* psx_lobby_match_caps(void) {
|
||||
return &test_match_caps;
|
||||
@@ -752,6 +757,12 @@ int main(int argc, char** argv) {
|
||||
|
||||
std::string error;
|
||||
PSXRecompV4::mod_clear_plugins_for_tests();
|
||||
check(psx_mod_register_pcm_plugin("runtime.test-vblank", test_pcm) == 1,
|
||||
"active PCM plugin registers");
|
||||
check(psx_mod_register_pcm_plugin("runtime.test-disabled-entry", test_pcm) == 1,
|
||||
"disabled PCM plugin registers");
|
||||
check(!psx_mod_register_pcm_plugin("runtime.test-vblank", test_pcm),
|
||||
"duplicate PCM callback is rejected");
|
||||
check(PSXRecompV4::mod_register_activation_plugin(
|
||||
"runtime.test-vblank", test_activation_plugin),
|
||||
"runtime test activation hook must register");
|
||||
@@ -854,6 +865,11 @@ int main(int argc, char** argv) {
|
||||
mod_runtime_on_vblank();
|
||||
check(plugin_calls == 1,
|
||||
"resolved trusted plugin must run on guest VBlank");
|
||||
int16_t overlay_pcm[4] = {};
|
||||
check(pcm_callback != nullptr, "active plan binds the SPU overlay");
|
||||
if (pcm_callback) pcm_callback(overlay_pcm, 2);
|
||||
check(overlay_pcm[0] == 12 && overlay_pcm[3] == 12,
|
||||
"only active PCM plugins mix every stereo sample");
|
||||
check(g_psx_mod_function_entry_hooks == 1,
|
||||
"activation must flatten exactly the active plan's entry hooks");
|
||||
psx_mod_function_entry(&entry_cpu, 0x80003000u);
|
||||
@@ -900,6 +916,7 @@ int main(int argc, char** argv) {
|
||||
check(activation_calls == 2 && plugin_calls == 1 &&
|
||||
g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 4,
|
||||
"a netplay session must stay vanilla: no activation, VBlank or hook");
|
||||
check(!pcm_callback, "an empty session unbinds PCM overlays");
|
||||
check(PSXRecompV4::mod_runtime_commit(cue_path, &error), error.c_str());
|
||||
mod_runtime_activate_plugins();
|
||||
check(activation_calls == 3,
|
||||
|
||||
@@ -129,9 +129,32 @@ static void test_end_with_repeat_keeps_playing(void) {
|
||||
"END with REPEAT records a loop event");
|
||||
}
|
||||
|
||||
static uint32_t overlay_frames;
|
||||
static void test_overlay(int16_t* stereo, uint32_t frames) {
|
||||
overlay_frames += frames;
|
||||
for (uint32_t i = 0; i < frames * 2; ++i) stereo[i] = 1234;
|
||||
}
|
||||
static void test_pcm_overlay_paths(void) {
|
||||
int16_t output[14];
|
||||
spu_init();
|
||||
spu_set_pcm_overlay(test_overlay);
|
||||
spu_render_block(output, 7); /* disabled SPU / general path */
|
||||
CHECK(overlay_frames == 7 && output[0] == 1234 && output[13] == 1234,
|
||||
"overlay plays even with disabled native SPU");
|
||||
spu_regs[reg_index(0x1F801DAAu)] = 0x8000;
|
||||
spu_render_block(output, 7); /* enabled SPU without voices / fast path */
|
||||
CHECK(overlay_frames == 14 && output[13] == 1234,
|
||||
"voice-free SPU path also advances the overlay");
|
||||
spu_set_pcm_overlay(NULL);
|
||||
spu_render_block(output, 7);
|
||||
CHECK(overlay_frames == 14 && output[0] == 0 && output[13] == 0,
|
||||
"unbinding preserves native silence without advancing playback");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_end_without_repeat_is_silent();
|
||||
test_end_with_repeat_keeps_playing();
|
||||
test_pcm_overlay_paths();
|
||||
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -122,10 +122,51 @@ static void test_gate_and_rewind(void) {
|
||||
"a rewound guest clock re-anchors without rendering");
|
||||
}
|
||||
|
||||
static uint32_t pcm_frames;
|
||||
static void pcm_overlay_test(int16_t* stereo, uint32_t frames) {
|
||||
pcm_frames += frames;
|
||||
for (uint32_t i = 0; i < frames * 2u; ++i) stereo[i] = 1234;
|
||||
}
|
||||
|
||||
static void test_pcm_overlay_uses_guest_clock(void) {
|
||||
g_cycle = 0;
|
||||
spu_init();
|
||||
spu_set_pcm_overlay(pcm_overlay_test);
|
||||
spu_catch_up();
|
||||
g_cycle += 768u * 3u;
|
||||
spu_catch_up();
|
||||
CHECK(pcm_frames == 3u && spu_output_available() == 3u,
|
||||
"PCM overlay advances with guest samples while the native SPU is off");
|
||||
int16_t output[8];
|
||||
CHECK(spu_output_pop(output, 4u) == 3u && output[0] == 1234 &&
|
||||
output[5] == 1234 && pcm_frames == 3u,
|
||||
"host output consumption neither skips nor advances mod playback");
|
||||
spu_set_output_gate(suppressed);
|
||||
s_suppress = 1;
|
||||
g_cycle += 768u * 2u;
|
||||
spu_catch_up();
|
||||
CHECK(pcm_frames == 5u && spu_output_available() == 0u,
|
||||
"suppressed output still advances deterministic mod PCM state");
|
||||
s_suppress = 0;
|
||||
spu_set_output_gate(NULL);
|
||||
spu_write(0x1F801DAAu, 0x8000u);
|
||||
g_cycle += 768u * 4u;
|
||||
spu_catch_up();
|
||||
CHECK(pcm_frames == 9u && spu_output_available() == 4u,
|
||||
"native voice-free fast path advances the same mod PCM clock");
|
||||
spu_set_pcm_overlay(NULL);
|
||||
spu_output_flush();
|
||||
g_cycle += 768u;
|
||||
spu_catch_up();
|
||||
CHECK(pcm_frames == 9u && spu_output_pop(output, 4u) == 1u && output[0] == 0,
|
||||
"unbinding restores native silence without advancing mod playback");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_envelope_rises_between_accesses();
|
||||
test_snapshot_keeps_clock_phase();
|
||||
test_gate_and_rewind();
|
||||
test_pcm_overlay_uses_guest_clock();
|
||||
if (failures) { fprintf(stderr, "%d failure(s)\n", failures); return 1; }
|
||||
puts("spu_guest_clock_test: ok");
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user