mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Netplay presentation packages: stay active in netplay, guarded; render passes on live netplay frames
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+34
-1
@@ -525,7 +525,8 @@ What a plugin sets up should last only for a session whose resolved plan
|
||||
activates it. Function-entry hooks follow that rule by construction: the commit
|
||||
and the netplay clear empty the hook table, and only activation rebuilds it, so
|
||||
a hook never runs in a session that did not activate its id (including a
|
||||
netplay session, which clears the plan). The host state a plugin changes
|
||||
netplay session, which clears the plan to its presentation packages; see
|
||||
*Netplay presentation packages*). The host state a plugin changes
|
||||
through the `psx_mod_*` setters is process-wide, so the runtime resets it at
|
||||
every session start instead.
|
||||
|
||||
@@ -627,6 +628,38 @@ bounded instant scheduler. Because this changes interrupt timing, packages
|
||||
should label it experimental and make it mutually exclusive with host-only
|
||||
load acceleration (normally as choices in one default-off feature).
|
||||
|
||||
### Netplay presentation packages
|
||||
|
||||
Netplay runs the simulation vanilla on both peers: the netplay clear drops
|
||||
every patch, overlay, derived disc and plugin. A package that only changes what
|
||||
the local player sees (a frame-rate or presentation plugin) can declare that at
|
||||
package level and stay active:
|
||||
|
||||
```toml
|
||||
netplay = "presentation" # default "off"
|
||||
```
|
||||
|
||||
Peers need not agree on these packages, so they can never change what either
|
||||
peer simulates. That is enforced, not trusted:
|
||||
|
||||
- The manifest may carry plugins and resources, but no `[[patch]]`,
|
||||
`[[overlay]]` or `[[derived_disc]]` (rejected at load).
|
||||
- In a netplay session (`g_psx_mod_netplay_presentation`) the API refuses every
|
||||
call that would change the simulation: `psx_mod_write_*`,
|
||||
`psx_mod_dispatch_guest_function` (outside a render pass, whose sandbox
|
||||
restores the machine), `psx_mod_write_code_word`, guest and GPU-DMA
|
||||
allocation, disc extents, `psx_mod_finish_function` and function filters
|
||||
(no skipping guest code), Skip FMVs, controller overrides and policies, disc
|
||||
speed, load acceleration, the 8 MB map and the native VBlank rate. Each
|
||||
refusal returns failure and counts `netplay.refused` (TCP `mod_counters`).
|
||||
- Every plugin callback (activation, VBlank, function entry, instruction) runs
|
||||
under a guard: a callback that leaves the CPU state changed is undone
|
||||
(`netplay.cpu_restored`), and one that stored to guest memory anyway is
|
||||
disabled for the rest of the session (`netplay.plugin_disabled`).
|
||||
|
||||
Render passes ([RENDER_PASSES.md](RENDER_PASSES.md)) are allowed on live
|
||||
netplay frames, never during a rollback resimulation.
|
||||
|
||||
## Native operations
|
||||
|
||||
`main_exe` writes use PSX guest virtual addresses. Expected bytes are checked
|
||||
|
||||
@@ -28,8 +28,10 @@ flip to frame n. For a PsyQ double-buffered loop that is the entry of the
|
||||
|
||||
Register that hook with `psx_mod_register_function_entry_plugin()` under the
|
||||
plugin's manifest `[[plugin]]` id (see [MOD_PACKAGES.md](MOD_PACKAGES.md)):
|
||||
it then runs only while the resolved mod plan activates the plugin, so never
|
||||
while the package is disabled or in netplay. Register once, for example from
|
||||
it then runs only while the resolved mod plan activates the plugin: never
|
||||
while the package is disabled, and in netplay only for a package declared
|
||||
`netplay = "presentation"` ([MOD_PACKAGES.md](MOD_PACKAGES.md), *Netplay
|
||||
presentation packages*). Register once, for example from
|
||||
the plugin's constructor; a repeated id and address returns 0. Entry hooks
|
||||
also fire for the guest functions a pass itself calls, the plugin's own
|
||||
included, so a hook that plans passes must ignore entries made inside one.
|
||||
@@ -61,7 +63,7 @@ budget aside (an empty plan while it says `READY` was shed for time):
|
||||
| `NO_PRESENTER` (1) | not OpenGL, interpolation off or suspended (FMV), or not the FLIP source | until the presenter changes |
|
||||
| `BACKEND` (2) | the renderer declines passes in its current mode | while that mode lasts |
|
||||
| `DISABLED` (3) | switched off after repeated faults | the session |
|
||||
| `SESSION` (4) | netplay, rollback, rewind, load/save replay, self-check resimulation | transient |
|
||||
| `SESSION` (4) | netplay rollback resimulation, rewind, load/save replay, self-check resimulation | transient |
|
||||
| `FAST_FORWARD` (5) | manual fast-forward, turbo-through-loads, FMV auto-skip, TCP turbo | transient |
|
||||
| `BUSY` (6) | inside an exception, a pass or a GPU DMA walk, or no frame captured since the presenter's history restarted (a display mode change) | transient |
|
||||
|
||||
@@ -223,8 +225,9 @@ After 8 faults (watchdog or refused writes) passes stay off for the session.
|
||||
|
||||
## Gates
|
||||
|
||||
The plan returns 0 in netplay, rollback resimulation, self-check, rewind,
|
||||
lockstep, an exception, while a GPU DMA list is in flight, during manual
|
||||
The plan returns 0 during a netplay rollback resimulation (live netplay
|
||||
frames may have passes: a pass leaves both peers' simulation untouched),
|
||||
self-check, rewind, lockstep, an exception, while a GPU DMA list is in flight, during manual
|
||||
fast-forward, turbo-through-loads, FMV auto-skip and TCP turbo, on anything
|
||||
but the OpenGL renderer with FLIP-source interpolation, while the presenter
|
||||
is suspended (FMV), after repeated faults, and when no VBlank was presented
|
||||
|
||||
@@ -272,6 +272,15 @@ struct ModPackage {
|
||||
std::string source_url;
|
||||
std::string resolver = "declarative";
|
||||
std::string save_compatibility = "shared";
|
||||
/* "off" (default): netplay runs vanilla without this package.
|
||||
* "presentation": the package only changes what the local player sees
|
||||
* (frame rate, presentation), never the simulation, so it stays active in
|
||||
* netplay. It may carry plugins and resources but no patches, overlays or
|
||||
* derived discs, and in a netplay session its plugins run under the
|
||||
* presentation guards (mod_runtime.cpp): guest writes and simulation
|
||||
* settings are refused, and a callback that changes guest state anyway is
|
||||
* undone or its plugin disabled. Peers need not agree on these packages. */
|
||||
std::string netplay = "off";
|
||||
/* Default channel for features that do not declare their own. */
|
||||
ModChannel channel = ModChannel::Stable;
|
||||
ModPackageOrigin origin = ModPackageOrigin::Installed;
|
||||
|
||||
@@ -49,6 +49,9 @@ extern uint32_t g_psx_ram_mask;
|
||||
uint32_t memory_get_ram_bytes(void);
|
||||
int psx_ram_8mb_active(void);
|
||||
void psx_ram_reset_size_request(void);
|
||||
/* Request the 8 MB map for the next memory_init() (0 = retail 2 MB);
|
||||
* plugins use psx_mod_set_main_ram_8mb (mod_runtime.cpp). */
|
||||
void psx_ram_request_8mb(int enabled);
|
||||
/* memory_init(): latch the requested geometry for this boot. */
|
||||
void psx_ram_apply_size_request(void);
|
||||
|
||||
|
||||
@@ -1586,8 +1586,13 @@ static std::string session_disc_path(const std::filesystem::path& stock_disc) {
|
||||
return mod_disc.string();
|
||||
}
|
||||
|
||||
/* mod_runtime.cpp: refuse a simulation-affecting call in a netplay
|
||||
* presentation session. */
|
||||
extern "C" int psx_mod_netplay_refuse(const char* api, int outside_pass_only);
|
||||
|
||||
extern "C" int psx_mod_set_native_vblank_rate(
|
||||
uint32_t frames_per_second) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_set_native_vblank_rate", 0)) return 0;
|
||||
if (frames_per_second != 0 &&
|
||||
(frames_per_second < 60 || frames_per_second > 1000)) {
|
||||
std::fprintf(stderr,
|
||||
@@ -1693,6 +1698,7 @@ extern "C" int psx_mod_set_frame_interpolation_source(uint32_t source) {
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_set_auto_skip_fmv(int enabled) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_set_auto_skip_fmv", 0)) return 0;
|
||||
if (enabled != 0 && enabled != 1) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: mod rejected invalid auto-skip-FMV value %d\n",
|
||||
@@ -1719,6 +1725,7 @@ extern "C" int psx_mod_set_bezel_artwork(const char* path) {
|
||||
|
||||
extern "C" int psx_mod_set_load_acceleration(
|
||||
uint32_t wall_clock_multiplier, uint32_t release_frames) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_set_load_acceleration", 0)) return 0;
|
||||
/* Host pacing only changes how fast wall-clock time is fed to a load; every
|
||||
* guest frame, CD deadline, interrupt and callback still happens, so a high
|
||||
* multiplier cannot desync the guest -- it just approaches "as fast as the
|
||||
@@ -1738,6 +1745,7 @@ extern "C" int psx_mod_set_load_acceleration(
|
||||
|
||||
extern "C" int psx_mod_set_disc_speed(
|
||||
uint32_t divisor, uint32_t instant_max_per_frame) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_set_disc_speed", 0)) return 0;
|
||||
/* Any positive divisor is arithmetically safe: cdrom.c divides the sector
|
||||
* delay by it and floors the result at CDROM_MIN_DELAY, and XA streaming
|
||||
* keeps authentic timing regardless. The old 2-or-4 allowlist was policy,
|
||||
@@ -1762,6 +1770,7 @@ extern "C" int psx_mod_set_disc_speed(
|
||||
|
||||
extern "C" int psx_mod_set_controller_mode_override(
|
||||
uint32_t player, uint32_t controller_mode) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_set_controller_mode_override", 0)) return 0;
|
||||
if (player >= PSX_MAX_PLAYERS ||
|
||||
(controller_mode != (uint32_t)PSXRecompV4::PAD_MODE_ANALOG &&
|
||||
controller_mode != (uint32_t)PSXRecompV4::PAD_MODE_DIGITAL)) {
|
||||
@@ -1780,6 +1789,7 @@ extern "C" int psx_mod_set_controller_presentation_policy(
|
||||
PSXModControllerPresentationCallback callback,
|
||||
uint32_t initial_mode,
|
||||
int config_capable) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_set_controller_presentation_policy", 0)) return 0;
|
||||
if (player >= PSX_MAX_PLAYERS || !callback ||
|
||||
(initial_mode != (uint32_t)PSXRecompV4::PAD_MODE_ANALOG &&
|
||||
initial_mode != (uint32_t)PSXRecompV4::PAD_MODE_DIGITAL)) {
|
||||
|
||||
@@ -1698,6 +1698,8 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
|
||||
cfg.contains("resolver") ? toml::find<std::string>(cfg, "resolver") : "declarative";
|
||||
out.save_compatibility = cfg.contains("save_compatibility")
|
||||
? toml::find<std::string>(cfg, "save_compatibility") : "shared";
|
||||
out.netplay = cfg.contains("netplay")
|
||||
? toml::find<std::string>(cfg, "netplay") : "off";
|
||||
/* Accepted at every format version: v5 manifests already carried a
|
||||
* package-level channel, enforced then only by a packaging grep over
|
||||
* the manifest text. Now it parses, and it seeds the per-feature
|
||||
@@ -1719,6 +1721,8 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
|
||||
throw std::runtime_error("resolver must be declarative or builtin:<id>");
|
||||
if (out.save_compatibility != "shared" && out.save_compatibility != "isolated")
|
||||
throw std::runtime_error("save_compatibility must be shared or isolated");
|
||||
if (out.netplay != "off" && out.netplay != "presentation")
|
||||
throw std::runtime_error("netplay must be off or presentation");
|
||||
|
||||
if (cfg.contains("target")) {
|
||||
for (const toml::value& v : toml::find(cfg, "target").as_array()) {
|
||||
@@ -2564,6 +2568,14 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
|
||||
out.derived_discs.push_back(std::move(derived));
|
||||
}
|
||||
}
|
||||
/* A netplay presentation package must not be able to change what
|
||||
* either peer simulates: code only, through the guarded plugin API. */
|
||||
if (out.netplay == "presentation" &&
|
||||
(!out.patches.empty() || !out.overlays.empty() ||
|
||||
!out.derived_discs.empty()))
|
||||
throw std::runtime_error(
|
||||
"a netplay = \"presentation\" package may not carry patches, "
|
||||
"overlays or derived discs");
|
||||
return true;
|
||||
} catch (const std::exception& ex) {
|
||||
set_error(error, path.string() + ": " + ex.what());
|
||||
|
||||
+165
-7
@@ -38,6 +38,12 @@
|
||||
#endif
|
||||
|
||||
extern "C" uint8_t psx_read_byte(uint32_t addr);
|
||||
extern "C" uint64_t g_guest_store_count;
|
||||
extern "C" int g_psx_render_pass_active;
|
||||
extern "C" int psx_mod_netplay_refuse(const char* api, int outside_pass_only);
|
||||
/* 1 while the committed plan is a netplay presentation plan. */
|
||||
extern "C" int g_psx_mod_netplay_presentation;
|
||||
int g_psx_mod_netplay_presentation = 0;
|
||||
extern "C" void psx_write_byte(uint32_t addr, uint8_t value);
|
||||
extern "C" uint16_t psx_read_half(uint32_t addr);
|
||||
extern "C" void psx_write_half(uint32_t addr, uint16_t value);
|
||||
@@ -89,6 +95,9 @@ struct RuntimeMods {
|
||||
const ModResolution::Plugin* current_plugin = nullptr;
|
||||
CPUState* current_function_cpu = nullptr;
|
||||
bool current_function_finished = false;
|
||||
/* Plugins of netplay = "presentation" packages that broke the
|
||||
* presentation contract this session (see NetplayPresentationGuard). */
|
||||
std::vector<const ModResolution::Plugin*> netplay_disabled;
|
||||
};
|
||||
|
||||
RuntimeMods& state() {
|
||||
@@ -123,6 +132,60 @@ public:
|
||||
PluginCallbackScope& operator=(const PluginCallbackScope&) = delete;
|
||||
};
|
||||
|
||||
/* Netplay presentation sessions (mod_runtime_clear_for_netplay keeps the
|
||||
* plugins of netplay = "presentation" packages). Each peer may run different
|
||||
* presentation packages, so none of them may change what the peer simulates.
|
||||
* The API refuses every simulation-affecting call (psx_mod_netplay_refuse),
|
||||
* and this guard wraps every plugin callback: a callback that leaves the CPU
|
||||
* changed is undone, and one that stored to guest memory (undoable only by
|
||||
* the plugin itself) disables its plugin for the session. Render passes are
|
||||
* sandboxed and restore both, so a callback that runs passes is unaffected. */
|
||||
bool plugin_netplay_disabled(const RuntimeMods& s,
|
||||
const ModResolution::Plugin* plugin) {
|
||||
return std::find(s.netplay_disabled.begin(), s.netplay_disabled.end(),
|
||||
plugin) != s.netplay_disabled.end();
|
||||
}
|
||||
|
||||
class NetplayPresentationGuard {
|
||||
RuntimeMods& runtime;
|
||||
const ModResolution::Plugin* plugin;
|
||||
CPUState* cpu;
|
||||
bool active;
|
||||
CPUState before{};
|
||||
uint64_t stores = 0;
|
||||
public:
|
||||
NetplayPresentationGuard(RuntimeMods& s, const ModResolution::Plugin* p,
|
||||
CPUState* c)
|
||||
: runtime(s), plugin(p), cpu(c),
|
||||
active(g_psx_mod_netplay_presentation != 0) {
|
||||
if (!active) return;
|
||||
if (cpu) before = *cpu;
|
||||
stores = g_guest_store_count;
|
||||
}
|
||||
~NetplayPresentationGuard() {
|
||||
if (!active) return;
|
||||
if (cpu && std::memcmp(cpu, &before, sizeof before) != 0) {
|
||||
*cpu = before;
|
||||
psx_mod_counter_add("netplay.cpu_restored", 1);
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: netplay: presentation plugin %s changed "
|
||||
"CPU state; undone\n",
|
||||
plugin ? plugin->id.c_str() : "?");
|
||||
}
|
||||
if (g_guest_store_count != stores && plugin &&
|
||||
!plugin_netplay_disabled(runtime, plugin)) {
|
||||
runtime.netplay_disabled.push_back(plugin);
|
||||
psx_mod_counter_add("netplay.plugin_disabled", 1);
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: netplay: presentation plugin %s wrote "
|
||||
"guest memory; disabled for this session\n",
|
||||
plugin->id.c_str());
|
||||
}
|
||||
}
|
||||
NetplayPresentationGuard(const NetplayPresentationGuard&) = delete;
|
||||
NetplayPresentationGuard& operator=(const NetplayPresentationGuard&) = delete;
|
||||
};
|
||||
|
||||
/* Function-entry hooks of the ACTIVE plan, flattened at plugin activation into
|
||||
* one table sorted by code key (address with the segment bits stripped, so a
|
||||
* KUSEG or KSEG1 PC reaches the same hook as KSEG0). The interpreter consults
|
||||
@@ -1226,6 +1289,8 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
|
||||
RuntimeMods& s = state();
|
||||
s.manager.set_root({});
|
||||
clear_function_entry_hooks();
|
||||
s.netplay_disabled.clear();
|
||||
g_psx_mod_netplay_presentation = 0;
|
||||
s.disc_extents.clear();
|
||||
s.extent_start = 0;
|
||||
s.plan = {};
|
||||
@@ -1294,12 +1359,71 @@ bool mod_runtime_clear_for_netplay(std::string* error) {
|
||||
s.main_applied = false;
|
||||
s.disc_enabled = false;
|
||||
s.disc_guard_failed = false;
|
||||
s.netplay_disabled.clear();
|
||||
g_psx_mod_netplay_presentation = 0;
|
||||
s.error.clear();
|
||||
if (error) error->clear();
|
||||
std::fprintf(stdout, "psxrecomp: mods cleared for netplay (vanilla session)\n");
|
||||
|
||||
/* The simulation stays vanilla on both peers. What survives is the
|
||||
* plugins (and their resources) of enabled netplay = "presentation"
|
||||
* packages: they change only what this player sees, so peers need not
|
||||
* agree on them, and the presentation guards hold them to that. A plan
|
||||
* that does not resolve keeps the session fully vanilla. */
|
||||
ModResolution full =
|
||||
s.manager.resolve(s.game_id, s.exe_sha256, s.disc_sha256);
|
||||
ModResolution kept;
|
||||
if (full.ok) {
|
||||
auto presentation = [&](const std::string& package_id) {
|
||||
const ModPackage* package = s.manager.selected_package(package_id);
|
||||
return package && package->netplay == "presentation";
|
||||
};
|
||||
for (const ModResolution::Plugin& plugin : full.plugins)
|
||||
if (presentation(plugin.package_id)) kept.plugins.push_back(plugin);
|
||||
for (const ModResolution::Resource& resource : full.resources)
|
||||
if (presentation(resource.package_id))
|
||||
kept.resources.push_back(resource);
|
||||
for (const auto& selection : full.selections)
|
||||
if (presentation(selection.first)) kept.selections.insert(selection);
|
||||
}
|
||||
if (kept.plugins.empty()) {
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: mods cleared for netplay (vanilla session)\n");
|
||||
return true;
|
||||
}
|
||||
kept.ok = true;
|
||||
kept.fingerprint = "netplay-presentation";
|
||||
s.plan = std::move(kept);
|
||||
g_psx_mod_netplay_presentation = 1;
|
||||
for (const ModResolution::Plugin& plugin : s.plan.plugins)
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: netplay keeps presentation plugin %s (%s/%s)\n",
|
||||
plugin.id.c_str(), plugin.package_id.c_str(),
|
||||
plugin.feature_id.c_str());
|
||||
std::fprintf(stdout, "psxrecomp: mods cleared for netplay (vanilla "
|
||||
"simulation; presentation plugins kept)\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
/* A simulation-affecting mod API call in a netplay presentation session.
|
||||
* `outside_pass_only`: guest memory access is sandboxed inside a render pass
|
||||
* (restored afterwards), so it is refused only outside one; settings are
|
||||
* never sandboxed and are always refused. */
|
||||
extern "C" int psx_mod_netplay_refuse(const char* api, int outside_pass_only) {
|
||||
if (!g_psx_mod_netplay_presentation) return 0;
|
||||
if (outside_pass_only && g_psx_render_pass_active) return 0;
|
||||
psx_mod_counter_add("netplay.refused", 1);
|
||||
static std::vector<std::string> named;
|
||||
const std::string name = api ? api : "?";
|
||||
if (std::find(named.begin(), named.end(), name) == named.end()) {
|
||||
named.push_back(name);
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: netplay: %s refused for a presentation "
|
||||
"plugin (it would change the simulation)\n",
|
||||
name.c_str());
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool mod_runtime_commit(const std::filesystem::path& disc_path, std::string* error) {
|
||||
RuntimeMods& s = state();
|
||||
if (!s.initialized) return true;
|
||||
@@ -1360,6 +1484,8 @@ bool mod_runtime_commit(const std::filesystem::path& disc_path, std::string* err
|
||||
s.disc_extents.clear();
|
||||
s.extent_start = 0;
|
||||
s.plan = std::move(plan);
|
||||
s.netplay_disabled.clear();
|
||||
g_psx_mod_netplay_presentation = 0;
|
||||
build_disc_index(s);
|
||||
s.effective_disc_path = std::move(effective_disc);
|
||||
s.main_applied = false;
|
||||
@@ -1453,6 +1579,7 @@ extern "C" void mod_runtime_enable_disc_patches(void) {
|
||||
|
||||
extern "C" int psx_mod_append_disc_extent(const char* resource_id,
|
||||
uint64_t byte_offset, uint32_t sector_count, uint32_t* first_lba) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_append_disc_extent", 0)) return 0;
|
||||
using namespace PSXRecompV4;
|
||||
if (first_lba) *first_lba = 0;
|
||||
auto& s = state();
|
||||
@@ -1559,6 +1686,7 @@ extern "C" void mod_runtime_activate_plugins(void) {
|
||||
s.activating = true;
|
||||
for (const ModResolution::Plugin& plugin : s.plan.plugins) {
|
||||
PluginCallbackScope scope(s, &plugin);
|
||||
NetplayPresentationGuard guard(s, &plugin, nullptr);
|
||||
mod_invoke_activation_plugin(plugin.id);
|
||||
}
|
||||
s.activating = false;
|
||||
@@ -1571,7 +1699,9 @@ extern "C" void mod_runtime_on_vblank(void) {
|
||||
RuntimeMods& s = state();
|
||||
if (!s.initialized || !s.plan.ok) return;
|
||||
for (const ModResolution::Plugin& plugin : s.plan.plugins) {
|
||||
if (plugin_netplay_disabled(s, &plugin)) continue;
|
||||
PluginCallbackScope scope(s, &plugin);
|
||||
NetplayPresentationGuard guard(s, &plugin, nullptr);
|
||||
mod_invoke_vblank_plugin(plugin.id);
|
||||
}
|
||||
}
|
||||
@@ -1651,6 +1781,7 @@ extern "C" int psx_mod_current_resource_bytes(const char* resource_id,
|
||||
}
|
||||
|
||||
extern "C" void psx_mod_write_byte(uint32_t address, uint8_t value) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_write_byte", 1)) return;
|
||||
psx_host_write_byte(address, value);
|
||||
}
|
||||
|
||||
@@ -1659,6 +1790,7 @@ extern "C" uint16_t psx_mod_read_half(uint32_t address) {
|
||||
}
|
||||
|
||||
extern "C" void psx_mod_write_half(uint32_t address, uint16_t value) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_write_half", 1)) return;
|
||||
psx_host_write_half(address, value);
|
||||
}
|
||||
|
||||
@@ -1667,21 +1799,31 @@ extern "C" uint32_t psx_mod_read_word(uint32_t address) {
|
||||
}
|
||||
|
||||
extern "C" void psx_mod_write_word(uint32_t address, uint32_t value) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_write_word", 1)) return;
|
||||
psx_host_write_word(address, value);
|
||||
}
|
||||
|
||||
extern "C" void psx_mod_write_code_word(uint32_t address, uint32_t value) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_write_code_word", 0)) return;
|
||||
psx_host_write_word(address, value);
|
||||
dirty_ram_mark_executable_range(address & 0x1FFFFFFFu, 4u);
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_set_main_ram_8mb(int enabled) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_set_main_ram_8mb", 0)) return 0;
|
||||
psx_ram_request_8mb(enabled);
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" uint32_t psx_mod_alloc_guest_memory(uint32_t size,
|
||||
uint32_t alignment) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_alloc_guest_memory", 0)) return 0;
|
||||
return psx_mod_memory_alloc(size, alignment);
|
||||
}
|
||||
|
||||
extern "C" uint32_t psx_mod_alloc_gpu_dma_memory(uint32_t size,
|
||||
uint32_t alignment) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_alloc_gpu_dma_memory", 0)) return 0;
|
||||
return psx_mod_gpu_dma_memory_alloc(size, alignment);
|
||||
}
|
||||
|
||||
@@ -1784,6 +1926,7 @@ extern "C" int psx_mod_register_function_filter_plugin(
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_finish_function(CPUState* cpu) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_finish_function", 0)) return 0;
|
||||
using namespace PSXRecompV4;
|
||||
RuntimeMods& s = state();
|
||||
if (!cpu || s.current_function_cpu != cpu || !s.current_plugin) return 0;
|
||||
@@ -1797,6 +1940,7 @@ extern "C" int psx_mod_register_guest_function_plugin(
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_dispatch_guest_function(CPUState* cpu, uint32_t address) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_dispatch_guest_function", 1)) return 0;
|
||||
using namespace PSXRecompV4;
|
||||
if (!g_psx_mod_guest_functions || !cpu || address >= 0xC0000000u) return 0;
|
||||
const auto& functions = active_guest_functions();
|
||||
@@ -1824,9 +1968,13 @@ extern "C" void psx_mod_instruction(CPUState* cpu, uint32_t address, uint32_t in
|
||||
[](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 (plugin_netplay_disabled(state(), it->plugin)) continue;
|
||||
PluginCallbackScope scope(state(), it->plugin, nullptr);
|
||||
const uint32_t pc = cpu->pc;
|
||||
it->callback(cpu, address);
|
||||
{
|
||||
NetplayPresentationGuard guard(state(), it->plugin, cpu);
|
||||
it->callback(cpu, address);
|
||||
}
|
||||
if (cpu->pc != pc) std::abort();
|
||||
cpu->gpr[0] = 0;
|
||||
}
|
||||
@@ -1842,13 +1990,23 @@ extern "C" int psx_mod_function_entry(CPUState* cpu, uint32_t address) {
|
||||
table.begin(), table.end(), key,
|
||||
[](const ActiveFunctionEntryHook& hook, uint32_t k) { return hook.key < k; });
|
||||
for (; it != table.end() && it->key == key; ++it) {
|
||||
if (plugin_netplay_disabled(s, it->plugin)) continue;
|
||||
PluginCallbackScope scope(s, it->plugin, cpu);
|
||||
++function_entry_depth;
|
||||
if (it->callback) it->callback(cpu, address);
|
||||
/* Either completion form skips the body: an entry callback that
|
||||
* called psx_mod_finish_function(), or a filter returning nonzero. */
|
||||
const bool handled = s.current_function_finished ||
|
||||
(it->filter && it->filter(cpu, address));
|
||||
bool handled;
|
||||
{
|
||||
NetplayPresentationGuard guard(s, it->plugin, cpu);
|
||||
if (it->callback) it->callback(cpu, address);
|
||||
/* Either completion form skips the body: an entry callback that
|
||||
* called psx_mod_finish_function(), or a filter returning
|
||||
* nonzero. Neither may skip guest code in a netplay presentation
|
||||
* session (psx_mod_finish_function refuses there). */
|
||||
const bool filtered = it->filter && it->filter(cpu, address);
|
||||
if (filtered && psx_mod_netplay_refuse("function filter", 0))
|
||||
handled = false;
|
||||
else
|
||||
handled = s.current_function_finished || filtered;
|
||||
}
|
||||
--function_entry_depth;
|
||||
if (handled) {
|
||||
cpu->pc = cpu->gpr[31];
|
||||
|
||||
@@ -54,7 +54,10 @@ const uint16_t* mod_texture_bank_pixels(uint16_t id, uint32_t* w, uint32_t* h) {
|
||||
return banks[id].pixels;
|
||||
}
|
||||
|
||||
int psx_mod_netplay_refuse(const char *api, int outside_pass_only);
|
||||
|
||||
uint32_t psx_mod_alloc_texture_packet_memory(uint32_t size, uint32_t alignment) {
|
||||
if (psx_mod_netplay_refuse("psx_mod_alloc_texture_packet_memory", 0)) return 0;
|
||||
uint32_t addr;
|
||||
if (arena_count == 16u || size < 40u) return 0;
|
||||
addr = psx_mod_gpu_dma_memory_alloc(size, alignment);
|
||||
|
||||
@@ -29,9 +29,10 @@ void psx_ram_reset_size_request(void) {
|
||||
s_ram_8mb_requested = 0;
|
||||
}
|
||||
|
||||
int psx_mod_set_main_ram_8mb(int enabled) {
|
||||
/* The request behind psx_mod_set_main_ram_8mb (mod_runtime.cpp owns the mod
|
||||
* API and its netplay guard). */
|
||||
void psx_ram_request_8mb(int enabled) {
|
||||
s_ram_8mb_requested = enabled ? 1 : 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void psx_ram_apply_size_request(void) {
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
* place (memory.c render_pass_store).
|
||||
*
|
||||
* Nothing here runs unless a trusted plugin calls the API, and the plan
|
||||
* refuses in netplay, rollback, rewind, fast-forward, self-check
|
||||
* refuses during rollback resimulation, rewind, fast-forward, self-check
|
||||
* resimulation, or without the OpenGL presenter's flip-aware interpolation
|
||||
* (psx_mod_render_pass_status says which). */
|
||||
* (psx_mod_render_pass_status says which). Live netplay frames may have
|
||||
* passes: they leave both peers' simulation untouched. */
|
||||
|
||||
#include "render_pass.h"
|
||||
|
||||
@@ -51,7 +52,6 @@ extern uint32_t interrupts_get_cycles_since_vblank(void);
|
||||
extern void interrupts_set_cycles_since_vblank(uint32_t v);
|
||||
extern void psx_irq_refresh_cause_ip2(void);
|
||||
extern int psx_get_in_exception(void);
|
||||
extern int psx_netplay_active(void);
|
||||
extern int psx_netplay_is_resimulating(void);
|
||||
extern int psx_selfcheck_resim_active(void);
|
||||
extern int psx_rewind_is_open(void);
|
||||
@@ -295,7 +295,12 @@ int psx_mod_set_render_pass_flip(uint32_t mode) {
|
||||
static uint32_t pass_status(void) {
|
||||
uint32_t gl;
|
||||
if (s_stats.disabled) return PSX_MOD_RENDER_PASS_DISABLED;
|
||||
if (psx_netplay_active() || psx_netplay_is_resimulating() ||
|
||||
/* Netplay itself is fine: a pass restores the whole guest machine, so
|
||||
* neither peer's simulation sees it, and its plugin runs under the
|
||||
* netplay presentation guards (mod_runtime.cpp). A rollback
|
||||
* resimulation replays ticks the player has already seen, so it never
|
||||
* draws. */
|
||||
if (psx_netplay_is_resimulating() ||
|
||||
psx_selfcheck_resim_active() || psx_rewind_is_open() ||
|
||||
g_ls_mode || g_ls_replay_active)
|
||||
return PSX_MOD_RENDER_PASS_SESSION;
|
||||
|
||||
@@ -76,6 +76,10 @@ extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) {
|
||||
return 0;
|
||||
}
|
||||
extern "C" void psx_ram_reset_size_request(void) {}
|
||||
static int ram_8mb_requests;
|
||||
extern "C" void psx_ram_request_8mb(int) { ram_8mb_requests++; }
|
||||
extern "C" { int g_psx_render_pass_active; uint64_t g_guest_store_count;
|
||||
extern int g_psx_mod_netplay_presentation; }
|
||||
extern "C" int psx_ws_x_margin(void) { return 0; }
|
||||
|
||||
/* Stand-in for the GPU's display geometry. psx_mod_display_width/height must
|
||||
@@ -177,6 +181,32 @@ static void test_guest_function(CPUState* cpu, uint32_t) {
|
||||
if (cpu->gpr[31] != ra) failures++;
|
||||
}
|
||||
|
||||
/* Netplay presentation package: what its plugin may and may not do. */
|
||||
static int pres_activations, sim_activations, pres_entries, pres_ram_ok;
|
||||
static uint32_t pres_alloc;
|
||||
static int pres_mode;
|
||||
static void test_pres_activation(void) {
|
||||
pres_activations++;
|
||||
pres_ram_ok = psx_mod_set_main_ram_8mb(1);
|
||||
pres_alloc = psx_mod_alloc_guest_memory(64, 4);
|
||||
}
|
||||
static void test_sim_activation(void) { sim_activations++; }
|
||||
static void test_pres_entry(CPUState* cpu, uint32_t) {
|
||||
pres_entries++;
|
||||
if (pres_mode == 0) { /* tries to change the simulation */
|
||||
cpu->gpr[2] = 0xDEADBEEFu;
|
||||
psx_mod_write_word(0x80001000u, 0x11111111u);
|
||||
(void)psx_mod_finish_function(cpu);
|
||||
} else if (pres_mode == 1) { /* sandboxed pass: writes allowed */
|
||||
g_psx_render_pass_active = 1;
|
||||
psx_mod_write_word(0x80001004u, 0x22222222u);
|
||||
g_psx_render_pass_active = 0;
|
||||
psx_mod_write_word(0x80001004u, 0u); /* the pass's restore */
|
||||
} else { /* a guest store behind the API */
|
||||
g_guest_store_count++;
|
||||
}
|
||||
}
|
||||
|
||||
static void test_activation_plugin(void) {
|
||||
activation_calls++;
|
||||
}
|
||||
@@ -1027,6 +1057,91 @@ int main() {
|
||||
check(iso_sector_count(mounted)==24&&iso_track_count(mounted)==1&&
|
||||
!iso_read_raw_sector(mounted,24,sector.data(),sector.size()),"clearing plan removes all donor sectors and restores TOC");
|
||||
iso_close(mounted);
|
||||
|
||||
/* Netplay presentation packages survive the netplay clear; everything
|
||||
* else stays vanilla, and the presentation plugin cannot touch the
|
||||
* simulation. */
|
||||
{
|
||||
const fs::path pres_root = root / "pres";
|
||||
write_text(pres_root / "packages/pres.test/1.0.0/manifest.toml",
|
||||
"format_version = 5\nid = \"pres.test\"\nversion = \"1.0.0\"\n"
|
||||
"name = \"Presentation\"\nnetplay = \"presentation\"\n"
|
||||
"[[target]]\ngame_id = \"PRES\"\n"
|
||||
"[[feature]]\nid = \"on\"\nname = \"On\"\ndefault_enabled = true\n"
|
||||
"[[plugin]]\nfeature = \"on\"\nid = \"pres.test.plugin\"\n");
|
||||
write_text(pres_root / "packages/sim.test/1.0.0/manifest.toml",
|
||||
"format_version = 5\nid = \"sim.test\"\nversion = \"1.0.0\"\n"
|
||||
"name = \"Simulation\"\n"
|
||||
"[[target]]\ngame_id = \"PRES\"\n"
|
||||
"[[feature]]\nid = \"on\"\nname = \"On\"\ndefault_enabled = true\n"
|
||||
"[[plugin]]\nfeature = \"on\"\nid = \"sim.test.plugin\"\n");
|
||||
write_text(pres_root / "state.toml", "format_version = 2\n");
|
||||
check(psx_mod_register_activation_plugin("pres.test.plugin",
|
||||
test_pres_activation) &&
|
||||
psx_mod_register_activation_plugin("sim.test.plugin",
|
||||
test_sim_activation) &&
|
||||
psx_mod_register_function_entry_plugin("pres.test.plugin",
|
||||
0x80006000u, test_pres_entry) == 1,
|
||||
"register presentation test plugins");
|
||||
check(PSXRecompV4::mod_runtime_initialize(pres_root, "PRES", 0, {}, &error),
|
||||
"presentation initialize");
|
||||
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str());
|
||||
check(g_psx_mod_netplay_presentation == 1,
|
||||
"a netplay session keeps an enabled presentation package");
|
||||
ram_8mb_requests = 0;
|
||||
mod_runtime_activate_plugins();
|
||||
check(pres_activations == 1 && sim_activations == 0,
|
||||
"netplay activates presentation plugins only");
|
||||
check(!pres_ram_ok && ram_8mb_requests == 0 && pres_alloc == 0,
|
||||
"netplay refuses simulation settings and guest allocation");
|
||||
CPUState pc{};
|
||||
pc.gpr[2] = 7; pc.gpr[31] = 0x80007000u; pc.pc = 0x80006000u;
|
||||
g_psx_mod_netplay_presentation = 0; /* the test's own setup write */
|
||||
psx_mod_write_word(0x80001000u, 0u);
|
||||
g_psx_mod_netplay_presentation = 1;
|
||||
pres_mode = 0;
|
||||
check(!psx_mod_function_entry(&pc, 0x80006000u) && pres_entries == 1,
|
||||
"a presentation hook cannot skip guest code in netplay");
|
||||
check(pc.gpr[2] == 7 && pc.pc == 0x80006000u,
|
||||
"a presentation hook's CPU change is undone in netplay");
|
||||
check(psx_read_word(0x80001000u) == 0u,
|
||||
"a presentation hook's guest write is refused outside a pass");
|
||||
pres_mode = 1;
|
||||
check(!psx_mod_function_entry(&pc, 0x80006000u) && pres_entries == 2,
|
||||
"a presentation hook may run a sandboxed pass");
|
||||
pres_mode = 2;
|
||||
psx_mod_function_entry(&pc, 0x80006000u);
|
||||
psx_mod_function_entry(&pc, 0x80006000u);
|
||||
check(pres_entries == 3,
|
||||
"a plugin that stores to guest memory is disabled for the session");
|
||||
check(PSXRecompV4::mod_runtime_commit({}, &error), error.c_str());
|
||||
check(g_psx_mod_netplay_presentation == 0,
|
||||
"an offline commit ends the presentation session");
|
||||
mod_runtime_activate_plugins();
|
||||
check(sim_activations == 1 && pres_activations == 2 && pres_ram_ok == 1,
|
||||
"offline, every enabled package activates with its full API");
|
||||
psx_mod_function_entry(&pc, 0x80006000u);
|
||||
check(pres_entries == 4, "offline, the disabled-in-netplay plugin runs again");
|
||||
|
||||
PSXRecompV4::ModPackage bad;
|
||||
write_text(pres_root / "bad/manifest.toml",
|
||||
"format_version = 5\nid = \"bad.test\"\nversion = \"1.0.0\"\n"
|
||||
"name = \"Bad\"\nnetplay = \"presentation\"\n"
|
||||
"[[target]]\ngame_id = \"PRES\"\n"
|
||||
"[[feature]]\nid = \"on\"\nname = \"On\"\n"
|
||||
"[[patch]]\nfeature = \"on\"\ntarget = \"main_exe\"\naddress = 2147549184\n"
|
||||
"expected = \"00 00 00 00\"\nreplace = \"01 00 00 00\"\n");
|
||||
check(!PSXRecompV4::ModPackageManager::read_manifest(
|
||||
pres_root / "bad/manifest.toml", bad),
|
||||
"a presentation package may not patch the game");
|
||||
write_text(pres_root / "bad2/manifest.toml",
|
||||
"format_version = 5\nid = \"bad.test\"\nversion = \"1.0.0\"\n"
|
||||
"name = \"Bad\"\nnetplay = \"sometimes\"\n"
|
||||
"[[target]]\ngame_id = \"PRES\"\n");
|
||||
check(!PSXRecompV4::ModPackageManager::read_manifest(
|
||||
pres_root / "bad2/manifest.toml", bad),
|
||||
"netplay must be off or presentation");
|
||||
}
|
||||
fs::remove_all(root, ec);
|
||||
if (failures) return 1;
|
||||
std::cout << "mod runtime tests passed\n";
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
/* Netplay presentation guard (mod_runtime.cpp): never in a netplay session here. */
|
||||
int psx_mod_netplay_refuse(const char *api, int outside_pass_only) { (void)api; (void)outside_pass_only; return 0; }
|
||||
static uint32_t memory[256], used, reads, resolves;
|
||||
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a) {
|
||||
(void)a; if(n>sizeof memory-used)return 0;
|
||||
|
||||
@@ -52,7 +52,7 @@ static int check(int cond, const char *label) {
|
||||
|
||||
static void set_geometry(int expanded) {
|
||||
psx_ram_reset_size_request();
|
||||
if (expanded) psx_mod_set_main_ram_8mb(1);
|
||||
if (expanded) psx_ram_request_8mb(1);
|
||||
psx_ram_apply_size_request();
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ int main(void) {
|
||||
check_helpers_everywhere(0, "memory_init without the mod stays retail");
|
||||
/* The request is inert until memory_init() applies it. */
|
||||
psx_ram_reset_size_request();
|
||||
check(psx_mod_set_main_ram_8mb(1) == 1, "8 MB request accepted");
|
||||
psx_ram_request_8mb(1);
|
||||
check_helpers_everywhere(0, "8 MB request is inert before memory_init");
|
||||
psx_ram_apply_size_request();
|
||||
check_helpers_everywhere(1, "8 MB request applies unique 8 MiB decode");
|
||||
|
||||
Reference in New Issue
Block a user