Files
psxrecomp/runtime/tests/test_netplay_view_mods.cpp
T
Shokunin 33b842e97f netplay: per-player own-view mods, widescreen only inside the local view
A [[plugin]] may declare netplay = "local_view": it only changes what one
player sees. A netplay session now keeps the player's own features whose every
contribution is such a plugin (no writes, overlays or derived discs) instead of
clearing them, without saving state.toml. They activate as usual, but their
hooks run only inside psx_mod_render_local_view (psx_mod_local_view_scope) and
their vblank/savestate callbacks not at all. With such a plan the widescreen
cull margin is the stock 0 in the shared game and this peer's own margin only
inside the sandboxed own-view draw; squash is not used. The canonical frame,
snapshots and digests stay identical on every peer whatever each player chose,
so nothing is negotiated, and each keeps their own aspect.

Tests: netplay_view_mods_test (flag, plan filter, parked hooks, no vblank,
state.toml untouched), ws_local_view_margin_test (margin scope), render-pass
sandbox asserts the scope around a local view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:17:26 -07:00

226 lines
8.9 KiB
C++

#include "mod_runtime.h"
#include "mod_packages.h"
#include "mod_plugins.h"
#include "psx_sha256.h"
#include "gpu.h"
#include "cpu_state.h"
#include <array>
#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
namespace fs = std::filesystem;
/* Netplay own-view mods (mod_runtime_commit_netplay_view): a feature whose
* only contribution is a [[plugin]] with netplay = "local_view" stays on in a
* match, per player; its hooks run only inside the sandboxed own-view render
* (psx_mod_local_view_scope), never in the shared simulation; everything
* else is cleared; state.toml is never rewritten. */
static std::array<uint8_t, 2 * 1024 * 1024> ram;
static int failures;
static int test_netplay_active = 1;
static int test_scope;
extern "C" int psx_netplay_active(void) { return test_netplay_active; }
extern "C" int psx_mod_local_view_scope(void) { return test_scope; }
extern "C" uint8_t psx_read_byte(uint32_t address) {
return ram[address & 0x1fffffu];
}
extern "C" void psx_write_byte(uint32_t address, uint8_t value) {
ram[address & 0x1fffffu] = value;
}
extern "C" uint16_t psx_read_half(uint32_t address) {
const uint32_t offset = address & 0x1fffffu;
return (uint16_t)(ram[offset] | ((uint16_t)ram[offset + 1] << 8));
}
extern "C" void psx_write_half(uint32_t address, uint16_t value) {
const uint32_t offset = address & 0x1fffffu;
ram[offset] = (uint8_t)value;
ram[offset + 1] = (uint8_t)(value >> 8);
}
extern "C" uint32_t psx_read_word(uint32_t address) {
const uint32_t offset = address & 0x1fffffu;
return (uint32_t)ram[offset] |
((uint32_t)ram[offset + 1] << 8) |
((uint32_t)ram[offset + 2] << 16) |
((uint32_t)ram[offset + 3] << 24);
}
extern "C" void psx_write_word(uint32_t address, uint32_t value) {
const uint32_t offset = address & 0x1fffffu;
ram[offset] = (uint8_t)value;
ram[offset + 1] = (uint8_t)(value >> 8);
ram[offset + 2] = (uint8_t)(value >> 16);
ram[offset + 3] = (uint8_t)(value >> 24);
}
/* Mod writes are host stores (memory.c psx_host_write_*). */
extern "C" void psx_host_write_byte(uint32_t address, uint8_t value) {
psx_write_byte(address, value);
}
extern "C" void psx_host_write_half(uint32_t address, uint16_t value) {
psx_write_half(address, value);
}
extern "C" void psx_host_write_word(uint32_t address, uint32_t value) {
psx_write_word(address, value);
}
extern "C" uint32_t psx_mod_memory_alloc(uint32_t, uint32_t) { return 0; }
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) {}
extern "C" void psx_projection_reset_session(void) {}
extern "C" void gpu_ws_set_native_scene_predicate(int (*)(void)) {}
extern "C" int psx_ws_x_margin(void) { return 0; }
/* Stand-in for the GPU's display geometry. psx_mod_display_width/height must
* report exactly what the presenter reports -- a plugin drawing an overlay
* uses this as the screen edge, so a substituted or rounded value would put
* HUD elements in the wrong place. */
static GpuDisplayInfo g_test_display;
extern "C" void gpu_get_display_info(GpuDisplayInfo* out) {
*out = g_test_display;
}
extern "C" void dirty_ram_mark_executable_range(uint32_t, uint32_t) {}
extern "C" int fntrace_is_game_started(void) { return 1; }
/* Widescreen tag passthroughs mod_runtime forwards to the GPU; unused here. */
extern "C" void gpu_ws_tag_hud_primitive(uint32_t, int) {}
extern "C" void gpu_ws_tag_world_primitive(uint32_t, int) {}
extern "C" void gpu_ws_set_adaptive_backdrop_preload(int) {}
extern "C" int gpu_ws_configured_x_reveal(void) { return 0; }
extern "C" void gpu_ws_tag_hud_prim(uint32_t, int) {}
extern "C" void gpu_ws_tag_screen_mask_quad(uint32_t) {}
extern "C" void gpu_ws_tag_radial_screen_mask_quad(uint32_t, float) {}
static void check(bool value, const char* message) {
if (!value) {
std::cerr << "FAIL: " << message << "\n";
failures++;
}
}
static void write_text(const fs::path& path, const std::string& text) {
fs::create_directories(path.parent_path());
std::ofstream out(path);
out << text;
}
static std::string read_text(const fs::path& path) {
std::ifstream in(path);
return std::string(std::istreambuf_iterator<char>(in), {});
}
static int view_hits, sim_hits, view_activations, sim_activations, vblanks;
static void view_entry(CPUState*, uint32_t) { view_hits++; }
static void sim_entry(CPUState*, uint32_t) { sim_hits++; }
static void view_activate(void) { view_activations++; }
static void sim_activate(void) { sim_activations++; }
static void view_vblank(void) { vblanks++; }
static std::string package(const std::string& id, const std::string& feature,
const std::string& plugin, const std::string& extra) {
return "format_version = 7\n"
"id = \"" + id + "\"\nversion = \"1.0.0\"\nname = \"T\"\n"
"[[target]]\ngame_id = \"SLUS-VIEW\"\n"
"[[feature]]\nid = \"" + feature + "\"\nname = \"F\"\n"
"[[plugin]]\nfeature = \"" + feature + "\"\nid = \"" + plugin + "\"\n" +
extra;
}
int main() {
const fs::path root = fs::temp_directory_path() / "psx_netplay_view_mods_test";
fs::remove_all(root);
write_text(root / "bundled/view.pkg/1.0.0/manifest.toml",
package("view.pkg", "wide", "view.wide", "netplay = \"local_view\"\n"));
write_text(root / "bundled/sim.pkg/1.0.0/manifest.toml",
package("sim.pkg", "cheat", "sim.cheat", ""));
write_text(root / "bundled/bad.pkg/1.0.0/manifest.toml",
package("bad.pkg", "x", "bad.x", "netplay = \"everywhere\"\n"));
const std::string state =
"format_version = 2\n"
"[[feature]]\npackage_id = \"view.pkg\"\nid = \"wide\"\nenabled = true\n"
"[[feature]]\npackage_id = \"sim.pkg\"\nid = \"cheat\"\nenabled = true\n";
write_text(root / "state.toml", state);
check(psx_mod_register_function_entry_plugin("view.wide", 0x80003000u, view_entry),
"register view hook");
check(psx_mod_register_function_entry_plugin("sim.cheat", 0x80003100u, sim_entry),
"register sim hook");
check(psx_mod_register_activation_plugin("view.wide", view_activate) &&
psx_mod_register_activation_plugin("sim.cheat", sim_activate),
"register activations");
check(psx_mod_register_vblank_plugin("view.wide", view_vblank), "register vblank");
std::string error;
check(PSXRecompV4::mod_runtime_initialize(root, "SLUS-VIEW", 0x80002000u, {}, &error),
error.c_str());
/* Offline: both run in the game. */
check(PSXRecompV4::mod_runtime_commit({}, &error), error.c_str());
check(!PSXRecompV4::mod_runtime_netplay_view_active(), "offline plan is not own-view");
mod_runtime_activate_plugins();
CPUState cpu{};
psx_mod_function_entry(&cpu, 0x80003000u);
psx_mod_function_entry(&cpu, 0x80003100u);
check(view_hits == 1 && sim_hits == 1, "offline hooks run in the game");
/* Netplay: only the local-view feature remains, parked outside the scope. */
view_hits = sim_hits = view_activations = sim_activations = vblanks = 0;
const std::string saved = read_text(root / "state.toml");
check(PSXRecompV4::mod_runtime_commit_netplay_view({}, &error), error.c_str());
check(PSXRecompV4::mod_runtime_netplay_view_active(), "own-view plan active");
mod_runtime_activate_plugins();
check(view_activations == 1 && sim_activations == 0,
"only the local-view plugin activates in a match");
psx_mod_function_entry(&cpu, 0x80003000u);
psx_mod_function_entry(&cpu, 0x80003100u);
mod_runtime_on_vblank();
check(view_hits == 0 && sim_hits == 0 && vblanks == 0,
"the shared simulation runs no mod hook or vblank");
test_scope = 1;
psx_mod_function_entry(&cpu, 0x80003000u);
psx_mod_function_entry(&cpu, 0x80003100u);
test_scope = 0;
check(view_hits == 1 && sim_hits == 0,
"inside the own-view render only the local-view hook runs");
check(read_text(root / "state.toml") == saved, "state.toml untouched by a match");
/* A plan whose own-view feature also writes the game does not qualify. */
PSXRecompV4::ModResolution plan;
plan.plugins.push_back({"p", "a.pkg", "f", true});
plan.plugins.push_back({"q", "b.pkg", "g", true});
PSXRecompV4::ModResolution::Write w;
w.package_id = "b.pkg";
w.feature_id = "g";
plan.writes.push_back(w);
const auto keep = PSXRecompV4::mod_runtime_netplay_view_features(plan);
check(keep.size() == 1 && keep[0] == "a.pkg/f",
"a feature with a game write is not own-view");
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error) &&
!PSXRecompV4::mod_runtime_netplay_view_active(),
"a clear ends the own-view plan");
fs::remove_all(root);
if (failures) {
std::cerr << failures << " failure(s)\n";
return 1;
}
std::cout << "netplay_view_mods_test: ok\n";
return 0;
}