Files
psxrecomp/runtime/tests/test_mod_runtime.cpp
T

820 lines
36 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;
static std::array<uint8_t, 2 * 1024 * 1024> ram;
static int failures;
static int activation_calls;
static int plugin_calls;
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);
}
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" 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) {}
static CPUState* interrupted_entry_cpu;
static void test_vblank_plugin(void) {
plugin_calls++;
if (interrupted_entry_cpu && psx_mod_finish_function(interrupted_entry_cpu)) failures++;
}
/* Function-entry hooks: one owned by the plan's active plugin, one by a
* plugin whose feature is disabled, one by an id no package selects. */
static int active_entry_hits;
static int disabled_entry_hits;
static int unselected_entry_hits;
static uint32_t active_entry_last;
static int entry_test_mode, nested_result;
static uint32_t nested_pc;
static void test_active_entry(CPUState* cpu, uint32_t address) {
active_entry_hits++;
active_entry_last = address;
if (entry_test_mode == 1) {
CPUState unrelated{};
if (psx_mod_finish_function(&unrelated) || !psx_mod_finish_function(cpu)) failures++;
cpu->gpr[2] = 0x12345678u;
} else if (entry_test_mode == 2 || entry_test_mode == 3) {
const int mode = entry_test_mode;
if (mode == 3 && !psx_mod_finish_function(cpu)) failures++;
CPUState nested{}; nested.gpr[31] = 0x80004000u;
entry_test_mode = mode == 2 ? 1 : 0;
nested_result = psx_mod_function_entry(&nested, address);
nested_pc = nested.pc;
entry_test_mode = mode;
} else if (entry_test_mode == 4) {
/* A native call made by a hook can cross a guest VBlank. Its
* callbacks must neither inherit nor discard the interrupted hook. */
interrupted_entry_cpu = cpu;
mod_runtime_on_vblank();
interrupted_entry_cpu = nullptr;
if (!psx_mod_finish_function(cpu)) failures++;
cpu->gpr[2] = 0x87654321u;
}
}
static void test_disabled_entry(CPUState*, uint32_t) { disabled_entry_hits++; }
static void test_unselected_entry(CPUState*, uint32_t) { unselected_entry_hits++; }
static void test_activation_plugin(void) {
activation_calls++;
}
static const uint8_t* media_snapshot;
static uint64_t media_size;
static int media_access, foreign_media_access;
static void test_media_plugin(void) {
media_access = psx_mod_current_resource_bytes("rom", &media_snapshot, &media_size);
const uint8_t* foreign = nullptr;
uint64_t size = 0;
foreign_media_access = psx_mod_current_resource_bytes("private", &foreign, &size);
}
static int big_ram_activations;
static void test_big_ram_activation(void) {
big_ram_activations++;
}
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 void write_bytes(const fs::path& path, const std::vector<uint8_t>& bytes) {
fs::create_directories(path.parent_path());
std::ofstream out(path, std::ios::binary);
out.write((const char*)bytes.data(), (std::streamsize)bytes.size());
}
static std::string sha256_hex(const std::vector<uint8_t>& bytes) {
uint8_t digest[32];
psx_sha256_compute(bytes.data(), bytes.size(), digest);
static const char hex[] = "0123456789abcdef";
std::string out(64, '0');
for (size_t i = 0; i < 32; ++i) {
out[i * 2] = hex[digest[i] >> 4];
out[i * 2 + 1] = hex[digest[i] & 15];
}
return out;
}
int main() {
const fs::path root = fs::temp_directory_path() / "psxrecomp-mod-runtime-test";
std::error_code ec;
fs::remove_all(root, ec);
const std::vector<uint8_t> stock(8 * 2352, 0);
std::vector<uint8_t> overlay(3000);
for (size_t i = 0; i < overlay.size(); ++i)
overlay[i] = (uint8_t)(i * 17u + 3u);
const fs::path stock_path = root / "stock.bin";
write_bytes(stock_path, stock);
write_bytes(root / "audio.bin", std::vector<uint8_t>(2 * 2352, 0x5a));
const fs::path cue_path = root / "stock.cue";
write_text(cue_path,
"FILE \"stock.bin\" BINARY\n"
" TRACK 01 MODE2/2352\n"
" INDEX 01 00:00:00\n"
"FILE \"audio.bin\" BINARY\n"
" TRACK 02 AUDIO\n"
" INDEX 01 00:00:00\n");
write_bytes(root / "packages/runtime.test/1.0.0/assets/overlay.bin",
overlay);
write_text(root / "packages/runtime.test/1.0.0/manifest.toml",
"format_version = 5\n"
"id = \"runtime.test\"\n"
"version = \"1.0.0\"\n"
"name = \"Runtime Test\"\n"
"[[target]]\n"
"game_id = \"SLUS-RUNTIME\"\n"
"disc_sha256 = \"" + sha256_hex(stock) + "\"\n"
"[[feature]]\n"
"id = \"main-code\"\n"
"name = \"Main Code\"\n"
"[[feature]]\n"
"id = \"disc-byte\"\n"
"name = \"Disc Byte\"\n"
"[[feature]]\n"
"id = \"asset-overlay\"\n"
"name = \"Asset Overlay\"\n"
"[[feature]]\n"
"id = \"user-byte\"\n"
"name = \"User Byte\"\n"
"[[feature]]\n"
"id = \"dynamic-main\"\n"
"name = \"Dynamic Main\"\n"
"[[feature]]\n"
"id = \"sparse-main\"\n"
"name = \"Sparse Main\"\n"
"[[feature]]\n"
"id = \"sparse-flag\"\n"
"name = \"Sparse Flag\"\n"
"[[feature]]\n"
"id = \"sparse-disc\"\n"
"name = \"Sparse Disc\"\n"
"[[feature]]\n"
"id = \"vblank-plugin\"\n"
"name = \"VBlank Plugin\"\n"
"[[option]]\n"
"feature = \"dynamic-main\"\n"
"id = \"count\"\n"
"label = \"Count\"\n"
"type = \"integer\"\n"
"min = 0\n"
"max = 254\n"
"default = 42\n"
"[[option]]\n"
"feature = \"sparse-main\"\n"
"id = \"frames\"\n"
"label = \"Frames\"\n"
"type = \"integer\"\n"
"min = 0\n"
"max = 99\n"
"default = 2\n"
"[[patch]]\n"
"feature = \"main-code\"\n"
"target = \"main_exe\"\n"
"address = 2147487744\n"
"expected = \"01020304\"\n"
"replace = \"a1a2a3a4\"\n"
"[[patch]]\n"
"feature = \"disc-byte\"\n"
"target = \"disc_raw\"\n"
"offset = 4714\n"
"expected = \"aa\"\n"
"replace = \"bb\"\n"
"[[patch]]\n"
"feature = \"user-byte\"\n"
"target = \"disc_user\"\n"
"offset = 6154\n"
"expected = \"cc\"\n"
"replace = \"dd\"\n"
"[[patch]]\n"
"feature = \"dynamic-main\"\n"
"target = \"main_exe\"\n"
"address = 2147488000\n"
"expected = \"0000\"\n"
"replace_from = { option = \"count\", encoding = \"u16le\" }\n"
"[[patch]]\n"
"feature = \"dynamic-main\"\n"
"target = \"main_exe\"\n"
"address = 2147488002\n"
"expected = \"0100\"\n"
"replace_from = { option = \"count\", encoding = \"u16le\", addend = 1 }\n"
"[[patch]]\n"
"feature = \"sparse-main\"\n"
"target = \"main_exe\"\n"
"address = 2147488256\n"
"expected = \"02000132\"\n"
"fields = [{ offset = 0, option = \"frames\", encoding = \"u8\" }]\n"
"when_integer = { option = \"frames\", op = \"gt\", value = 0 }\n"
"[[patch]]\n"
"feature = \"sparse-main\"\n"
"target = \"main_exe\"\n"
"address = 2147488256\n"
"expected = \"02000132\"\n"
"fields = [{ offset = 0, replace = \"01\" }, "
"{ offset = 2, replace = \"00\" }]\n"
"when_integer = { option = \"frames\", op = \"eq\", value = 0 }\n"
"[[patch]]\n"
"feature = \"sparse-flag\"\n"
"target = \"main_exe\"\n"
"address = 2147488256\n"
"expected = \"02000132\"\n"
"fields = [{ offset = 1, replace = \"42\" }]\n"
"[[patch]]\n"
"feature = \"sparse-disc\"\n"
"target = \"disc_user\"\n"
"offset = 6164\n"
"expected = \"11223344\"\n"
"fields = [{ offset = 0, replace = \"aa\" }, "
"{ offset = 2, replace = \"bb\" }]\n"
"[[overlay]]\n"
"feature = \"asset-overlay\"\n"
"target = \"disc_raw\"\n"
"offset = 11408\n"
"file = \"assets/overlay.bin\"\n"
"sha256 = \"" + sha256_hex(overlay) + "\"\n"
"expected_sha256 = \"" +
sha256_hex(std::vector<uint8_t>(overlay.size(), 0)) + "\"\n"
"[[plugin]]\n"
"feature = \"vblank-plugin\"\n"
"id = \"runtime.test-vblank\"\n"
"[[feature]]\n"
"id = \"entry-disabled\"\n"
"name = \"Disabled Entry\"\n"
"[[plugin]]\n"
"feature = \"entry-disabled\"\n"
"id = \"runtime.test-disabled-entry\"\n");
write_text(root / "state.toml",
"format_version = 2\n"
"[[package]]\n"
"id = \"runtime.test\"\n"
"version = \"1.0.0\"\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"main-code\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"disc-byte\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"asset-overlay\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"user-byte\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"dynamic-main\"\n"
"enabled = true\n"
"[feature.values]\n"
"count = 42\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"sparse-main\"\n"
"enabled = true\n"
"[feature.values]\n"
"frames = 0\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"sparse-flag\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"sparse-disc\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"vblank-plugin\"\n"
"enabled = true\n");
std::string error;
PSXRecompV4::mod_clear_plugins_for_tests();
check(PSXRecompV4::mod_register_activation_plugin(
"runtime.test-vblank", test_activation_plugin),
"runtime test activation hook must register");
check(PSXRecompV4::mod_register_vblank_plugin(
"runtime.test-vblank", test_vblank_plugin),
"runtime test plugin must register");
check(psx_mod_register_function_entry_plugin(
"runtime.test-vblank", 0x80003000u, test_active_entry) == 1,
"active plugin's function-entry hook must register");
check(psx_mod_register_function_entry_plugin(
"runtime.test-disabled-entry", 0x80003000u, test_disabled_entry) == 1,
"disabled feature's function-entry hook must register");
check(psx_mod_register_function_entry_plugin(
"runtime.unselected-entry", 0x80003000u, test_unselected_entry) == 1,
"unselected function-entry hook must register");
CPUState entry_cpu{};
check(PSXRecompV4::mod_runtime_initialize(
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(cue_path, &error),
"CUE and its data-track BIN must have the same mod target identity");
/* Committed but not yet activated: no hook may run. */
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 0,
"function-entry hooks must not run before plugin activation");
mod_runtime_activate_plugins();
check(activation_calls == 1,
"resolved trusted plugin must activate before runtime startup");
mod_runtime_on_vblank();
check(plugin_calls == 1,
"resolved trusted plugin must run on guest VBlank");
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);
check(active_entry_hits == 1 && active_entry_last == 0x80003000u,
"active plan's function-entry hook must run at its address");
psx_mod_function_entry(&entry_cpu, 0x00003000u);
psx_mod_function_entry(&entry_cpu, 0xA0003000u);
check(active_entry_hits == 3 && active_entry_last == 0xA0003000u,
"function-entry hooks must match every segment alias of the code address");
psx_mod_function_entry(&entry_cpu, 0x80003004u);
psx_mod_function_entry(&entry_cpu, 0x80203000u);
check(active_entry_hits == 3,
"function-entry hooks must not run for other addresses or RAM mirrors");
check(disabled_entry_hits == 0 && unselected_entry_hits == 0,
"hooks of plugins the plan does not activate must never run");
/* A replaced plan drops every hook until its own activation. */
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str());
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 3,
"clearing the plan must drop its function-entry hooks");
check(PSXRecompV4::mod_runtime_commit(cue_path, &error), error.c_str());
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(active_entry_hits == 3,
"a re-committed plan must not run hooks before activation");
mod_runtime_activate_plugins();
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(active_entry_hits == 4 && disabled_entry_hits == 0 &&
unselected_entry_hits == 0,
"re-activation restores exactly the active plan's hooks");
entry_cpu.gpr[31] = 0x80005000u;
check(!psx_mod_finish_function(&entry_cpu) && !psx_mod_function_entry(nullptr, 0x80003000u),
"completion outside an entry callback or with no CPU is refused");
entry_test_mode = 1;
check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 &&
entry_cpu.pc == 0x80005000u && entry_cpu.gpr[2] == 0x12345678u,
"explicit completion returns handled and publishes guest return PC/results");
entry_test_mode = 2; entry_cpu.pc = 0;
check(!psx_mod_function_entry(&entry_cpu, 0x80003000u) && entry_cpu.pc == 0 &&
nested_result == 1 && nested_pc == 0x80004000u,
"inner completion cannot finish an ordinary outer callback");
entry_test_mode = 3;
check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 &&
entry_cpu.pc == 0x80005000u && nested_result == 0,
"outer completion survives a nested ordinary callback");
entry_test_mode = 4;
check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 &&
entry_cpu.pc == 0x80005000u && entry_cpu.gpr[2] == 0x87654321u,
"entry completion survives VBlank callbacks during a native call");
entry_test_mode = 0;
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 1; ram[0x1103] = 0;
ram[0x1200] = 2; ram[0x1201] = 0;
ram[0x1202] = 1; ram[0x1203] = 0x32;
mod_runtime_on_dispatch(0x80001000);
check(ram[0x1000] == 1, "patch must wait for the configured entry point");
mod_runtime_on_dispatch(0x80002000);
check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4,
"main-EXE patch must apply before entry execution");
check(ram[0x1100] == 42 && ram[0x1101] == 0 &&
ram[0x1102] == 43 && ram[0x1103] == 0,
"dynamic main-EXE patches must encode all sites before entry");
check(ram[0x1200] == 1 && ram[0x1201] == 0x42 &&
ram[0x1202] == 0 && ram[0x1203] == 0x32,
"adjacent sparse fields must compose while preserving guard-only "
"bytes");
/* A full-machine savestate replaces main RAM after the entry-point plan
* has already applied. Loading a stock checkpoint must not silently turn
* the currently enabled main-EXE features back off. */
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 1; ram[0x1103] = 0;
ram[0x1200] = 2; ram[0x1201] = 0;
ram[0x1202] = 1; ram[0x1203] = 0x32;
mod_runtime_on_savestate_loaded();
check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4 &&
ram[0x1100] == 42 && ram[0x1102] == 43 &&
ram[0x1200] == 1 && ram[0x1201] == 0x42 &&
ram[0x1202] == 0 && ram[0x1203] == 0x32,
"savestate restore must reapply the complete enabled main plan");
std::array<uint8_t, 2352> sector{};
sector[10] = 0xaa;
mod_runtime_patch_disc_sector(2, 1, sector.data(), (uint32_t)sector.size());
check(sector[10] == 0xaa, "disc overlay must stay off during reference reads");
mod_runtime_enable_disc_patches();
mod_runtime_patch_disc_sector(2, 1, sector.data(), (uint32_t)sector.size());
check(sector[10] == 0xbb, "raw disc overlay must patch matching sectors");
std::array<uint8_t, 2352> overlay_sector{};
mod_runtime_patch_disc_sector(
4, 1, overlay_sector.data(), (uint32_t)overlay_sector.size());
check(overlay_sector[1999] == 0 &&
overlay_sector[2000] == overlay[0] &&
overlay_sector[2351] == overlay[351],
"file overlay must patch the tail of its first sector");
overlay_sector.fill(0);
mod_runtime_patch_disc_sector(
5, 1, overlay_sector.data(), (uint32_t)overlay_sector.size());
check(overlay_sector.front() == overlay[352] &&
overlay_sector.back() == overlay[2703],
"file overlay must patch complete middle sectors");
overlay_sector.fill(0);
mod_runtime_patch_disc_sector(
6, 1, overlay_sector.data(), (uint32_t)overlay_sector.size());
check(overlay_sector[0] == overlay[2704] &&
overlay_sector[295] == overlay[2999] &&
overlay_sector[296] == 0,
"file overlay must patch the head of its final sector");
std::array<uint8_t, 2352> mode2_sector{};
mode2_sector[15] = 2;
mode2_sector[18] = 0;
mode2_sector[24 + 10] = 0xcc;
mode2_sector[24 + 20] = 0x11;
mode2_sector[24 + 21] = 0x22;
mode2_sector[24 + 22] = 0x33;
mode2_sector[24 + 23] = 0x44;
mod_runtime_patch_disc_sector(
3, 1, mode2_sector.data(), (uint32_t)mode2_sector.size());
check(mode2_sector[24 + 10] == 0xdd,
"disc_user operations must apply to raw Mode2 Form1 user data");
check(mode2_sector[24 + 20] == 0xaa &&
mode2_sector[24 + 21] == 0x22 &&
mode2_sector[24 + 22] == 0xbb &&
mode2_sector[24 + 23] == 0x44,
"sparse disc writes must validate a complete guard and modify only "
"owned fields");
std::array<uint8_t, 2352> audio_sector{};
audio_sector[24 + 10] = 0xcc;
mod_runtime_patch_disc_sector(
3, 1, audio_sector.data(), (uint32_t)audio_sector.size());
check(audio_sector[24 + 10] == 0xcc,
"disc_user operations must not modify CDDA/non-data sectors");
check(PSXRecompV4::mod_runtime_initialize(
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str());
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 2; ram[0x1103] = 0; /* second dynamic guard is wrong */
mod_runtime_on_dispatch(0x80002000);
check(ram[0x1000] == 1 && ram[0x1003] == 4 &&
ram[0x1100] == 0 && ram[0x1101] == 0,
"one failed generated guard must leave the complete main plan untouched");
check(PSXRecompV4::mod_runtime_initialize(
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str());
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 1; ram[0x1103] = 0;
ram[0x1200] = 2; ram[0x1201] = 0;
ram[0x1202] = 1; ram[0x1203] = 0x33; /* guard-only byte is wrong */
mod_runtime_on_dispatch(0x80002000);
check(ram[0x1000] == 1 && ram[0x1003] == 4 &&
ram[0x1200] == 2 && ram[0x1201] == 0 &&
ram[0x1202] == 1 && ram[0x1203] == 0x33,
"a failed sparse guard-only byte must leave the complete main plan "
"untouched");
/* Loading can be requested while the boot executable is still running,
* before the configured game entry point has dispatched. The restored
* checkpoint itself supplies the bytes used to validate and apply the
* plan in that case. */
check(PSXRecompV4::mod_runtime_initialize(
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str());
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 1; ram[0x1103] = 0;
ram[0x1200] = 2; ram[0x1201] = 0;
ram[0x1202] = 1; ram[0x1203] = 0x32;
mod_runtime_on_savestate_loaded();
check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4 &&
ram[0x1100] == 42 && ram[0x1102] == 43 &&
ram[0x1200] == 1 && ram[0x1201] == 0x42 &&
ram[0x1202] == 0 && ram[0x1203] == 0x32,
"pre-entry savestate restore must validate and apply the main plan");
mod_runtime_enable_disc_patches();
std::array<uint8_t, 2352> bad_sparse_disc{};
bad_sparse_disc[15] = 2;
bad_sparse_disc[18] = 0;
bad_sparse_disc[24 + 10] = 0xcc;
bad_sparse_disc[24 + 20] = 0x11;
bad_sparse_disc[24 + 21] = 0x99; /* guard-only byte is wrong */
bad_sparse_disc[24 + 22] = 0x33;
bad_sparse_disc[24 + 23] = 0x44;
mod_runtime_patch_disc_sector(
3, 1, bad_sparse_disc.data(),
(uint32_t)bad_sparse_disc.size());
check(bad_sparse_disc[24 + 10] == 0xcc &&
bad_sparse_disc[24 + 20] == 0x11 &&
bad_sparse_disc[24 + 22] == 0x33,
"a failed sparse disc guard must leave every write in the sector "
"untouched");
/* Implicit requirement through the real commit/activation path: the
* hidden required feature's activation plugin runs, the state.toml commit
* writes never gains the derived activation (it is not a choice),
* and a requirement on an absent package makes the launch commit fail
* rather than boot the requiring build without it. */
{
const fs::path req_root = root / "requirement-runtime";
write_text(req_root / "bundled/runtime.ram/1.0.0/manifest.toml",
"format_version = 5\n"
"id = \"runtime.ram\"\n"
"version = \"1.0.0\"\n"
"name = \"Runtime RAM\"\n"
"[[target]]\n"
"game_id = \"*\"\n"
"[[feature]]\n"
"id = \"big-ram\"\n"
"name = \"Big RAM\"\n"
"hidden = true\n"
"[[option]]\n"
"feature = \"big-ram\"\n"
"id = \"size\"\n"
"label = \"Size\"\n"
"type = \"choice\"\n"
"default = \"eight\"\n"
"[[option.choice]]\nvalue = \"eight\"\nlabel = \"8 MB\"\n"
"[[plugin]]\n"
"feature = \"big-ram\"\n"
"id = \"runtime.big-ram\"\n");
write_text(req_root / "bundled/runtime.mode/1.0.0/manifest.toml",
"format_version = 7\n"
"id = \"runtime.mode\"\n"
"version = \"1.0.0\"\n"
"name = \"Runtime Mode\"\n"
"[[target]]\n"
"game_id = \"SLUS-REQ\"\n"
"[[feature]]\n"
"id = \"mode\"\n"
"name = \"Mode\"\n"
"[[option]]\n"
"feature = \"mode\"\n"
"id = \"extras\"\n"
"label = \"Extras\"\n"
"type = \"choice\"\n"
"default = \"none\"\n"
"[[option.choice]]\nvalue = \"none\"\nlabel = \"None\"\n"
"[[option.choice]]\nvalue = \"full\"\nlabel = \"Full\"\n"
"[[requirement]]\n"
"feature = \"mode\"\n"
"package = \"runtime.ram\"\n"
"requires_feature = \"big-ram\"\n"
"when = { extras = \"full\" }\n");
const std::string state_text =
"format_version = 2\n"
"\n[[feature]]\npackage_id = \"runtime.mode\"\nid = \"mode\"\n"
"enabled = true\n"
"[feature.values]\nextras = \"full\"\n";
write_text(req_root / "state.toml", state_text);
const auto read_state = [&]() {
std::ifstream in(req_root / "state.toml", std::ios::binary);
return std::string((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
};
big_ram_activations = 0;
check(PSXRecompV4::mod_register_activation_plugin(
"runtime.big-ram", test_big_ram_activation),
"big-ram activation hook must register");
check(PSXRecompV4::mod_runtime_initialize(
req_root, "SLUS-REQ", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(stock_path, &error),
"a satisfied implicit requirement must commit");
mod_runtime_activate_plugins();
check(big_ram_activations == 1,
"the implicitly required feature's plugin must activate");
char value[32];
check(psx_mod_option_value("runtime.ram", "big-ram", "size", value,
sizeof(value)) == 1 &&
std::string(value) == "eight",
"an implicit feature's plugin must read its option values");
/* commit rewrites state.toml in canonical form; what matters is that
* the player's choice survives and the derived one is never added. */
const std::string after = read_state();
check(after.find("runtime.ram") == std::string::npos &&
after.find("extras = \"full\"") != std::string::npos,
"committing must not write the derived activation to state.toml");
/* Condition false: the required plugin stays off. */
write_text(req_root / "state.toml",
"format_version = 2\n"
"\n[[feature]]\npackage_id = \"runtime.mode\"\nid = \"mode\"\n"
"enabled = true\n"
"[feature.values]\nextras = \"none\"\n");
big_ram_activations = 0;
check(PSXRecompV4::mod_runtime_initialize(
req_root, "SLUS-REQ", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(stock_path, &error),
error.c_str());
mod_runtime_activate_plugins();
check(big_ram_activations == 0,
"a requirement whose condition is false must not activate");
/* Required package absent (e.g. the title excluded the builtin). */
write_text(req_root / "state.toml", state_text);
fs::remove_all(req_root / "bundled/runtime.ram", ec);
check(PSXRecompV4::mod_runtime_initialize(
req_root, "SLUS-REQ", 0x80002000, {}, &error),
error.c_str());
std::string commit_error;
check(!PSXRecompV4::mod_runtime_commit(stock_path, &commit_error) &&
commit_error.find("runtime.mode/mode requires "
"runtime.ram/big-ram") != std::string::npos,
"an unmet requirement must fail the launch commit loudly");
}
/*
* Display geometry passthrough. Ape Escape scans out 384 while its mode
* width is 368, which is precisely why a plugin cannot derive this from
* GPUSTAT: the horizontal range that makes the difference is write-only.
*/
g_test_display.width = 384;
g_test_display.height = 240;
check(psx_mod_display_width() == 384u,
"psx_mod_display_width must report the presenter's visible width, "
"not the coarse mode width");
check(psx_mod_display_height() == 240u,
"psx_mod_display_height must report the presenter's visible height");
g_test_display.width = 0;
g_test_display.height = 0;
check(psx_mod_display_width() == 0u && psx_mod_display_height() == 0u,
"unestablished display geometry must report zero so callers skip "
"drawing instead of guessing");
/* Source-owned ISO: nested file spanning two sectors, without a derived
* disc. Host reads must choose the original mount and leave sizes honest. */
std::vector<uint8_t> iso(24*2048);
auto le32 = [&](size_t at,uint32_t n) { for(unsigned i=0;i<4;++i) iso[at+i]=(uint8_t)(n>>(i*8)); };
auto record = [&](size_t at,uint32_t lba,uint32_t bytes,bool directory,const std::string& name) {
iso[at]=(uint8_t)((33+name.size()+1)&~size_t(1));
le32(at+2,lba); le32(at+10,bytes); iso[at+25]=directory?2:0;
iso[at+28]=1;iso[at+31]=1;iso[at+32]=(uint8_t)name.size();
std::copy(name.begin(),name.end(),iso.begin()+at+33);
};
iso[16*2048]=1;std::copy_n("CD001",5,iso.begin()+16*2048+1);iso[16*2048+6]=1;
record(16*2048+156,20,2048,true,std::string(1,'\0'));
record(20*2048,21,2048,true,"S0");
record(21*2048,22,3000,false,"LEVEL.NSF;1");
for(unsigned i=0;i<3000;++i)iso[22*2048+i]=(uint8_t)(i*7);
const auto disc_root=root/"host-reader";
const auto iso_path=disc_root/"original.iso";
write_bytes(iso_path,iso);
check(PSXRecompV4::mod_runtime_initialize(disc_root,"READER",0,{},&error),"reader initialize");
check(PSXRecompV4::mod_runtime_commit(iso_path,&error),"reader mount original ISO");
uint32_t bytes=0;
check(psx_mod_read_disc_file("S0/LEVEL.NSF",nullptr,0,&bytes) && bytes==3000,"query original nested file size");
std::vector<uint8_t> result(3000);
check(!psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),2999,&bytes) && bytes==0,"undersized destination rejected");
check(psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),(uint32_t)result.size(),&bytes) &&
bytes==3000 && std::equal(result.begin(),result.end(),iso.begin()+22*2048),"complete original file bytes");
check(!psx_mod_read_disc_file("S0/MISSING.NSF",nullptr,0,&bytes) && bytes==0,"missing file explicit");
check(!psx_mod_read_disc_file("S0",nullptr,0,&bytes),"directory rejected");
std::vector<uint8_t> raw_iso(24*2352);
for(unsigned i=0;i<24;++i) {
raw_iso[i*2352+15]=2;
std::copy_n(iso.begin()+i*2048,2048,raw_iso.begin()+i*2352+24);
}
const auto raw_path=disc_root/"original.bin";
write_bytes(raw_path,raw_iso);
check(PSXRecompV4::mod_runtime_commit(raw_path,&error),"reader mount raw disc");
check(psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),(uint32_t)result.size(),&bytes) &&
std::equal(result.begin(),result.end(),iso.begin()+22*2048),"raw and ISO reads identical");
/* Verified bytes are scoped to the committed feature, and do not depend
* on reopening an owner file after launch. No launcher is needed here. */
const auto media_root = root / "verified-media";
std::vector<uint8_t> rom(64, 0);
rom[0] = 0x80; rom[1] = 0x37; rom[2] = 0x12; rom[3] = 0x40;
rom[32] = 0x5a;
const auto rom_path = media_root / "owner.z64";
write_bytes(rom_path, rom);
write_text(media_root / "packages/media.test/1.0.0/manifest.toml",
"format_version = 8\nid = \"media.test\"\nversion = \"1.0.0\"\nname = \"Media Test\"\n"
"[[target]]\ngame_id = \"READER\"\n"
"[[feature]]\nid = \"active\"\nname = \"Active\"\ndefault_enabled = true\n"
"[[feature]]\nid = \"private\"\nname = \"Private\"\n"
"[[resource]]\nfeature = \"active\"\nid = \"rom\"\nlabel = \"ROM\"\n"
"format = \"n64-rom\"\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n"
"[[resource]]\nfeature = \"private\"\nid = \"private\"\nlabel = \"Private\"\n"
"format = \"n64-rom\"\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n"
"[[plugin]]\nfeature = \"active\"\nid = \"media.test.plugin\"\n");
write_text(media_root / "state.toml",
"format_version = 2\n[[feature]]\npackage_id = \"media.test\"\nid = \"active\"\nenabled = true\n"
"[feature.resources]\nrom = \"" + rom_path.generic_string() + "\"\n");
check(psx_mod_register_activation_plugin("media.test.plugin", test_media_plugin), "register media consumer");
check(PSXRecompV4::mod_runtime_initialize(media_root, "READER", 0, {}, &error), "media initialize");
check(PSXRecompV4::mod_runtime_commit({}, &error), "verify and commit owner media");
const uint8_t* unavailable = nullptr;
uint64_t unavailable_size = 0;
check(!psx_mod_current_resource_bytes("rom", &unavailable, &unavailable_size), "bytes unavailable outside plugin callback");
mod_runtime_activate_plugins();
check(media_access && media_size == rom.size() &&
std::equal(rom.begin(), rom.end(), media_snapshot), "plugin receives exact verified bytes");
check(!foreign_media_access, "inactive feature cannot supply bytes to another feature");
rom[32] ^= 1;
write_bytes(rom_path, rom);
check(media_snapshot && media_snapshot[32] == 0x5a, "committed bytes survive owner file changes");
check(!PSXRecompV4::mod_runtime_commit({}, &error), "new commit rejects modified media");
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), "netplay clears donor plan");
fs::remove_all(root, ec);
if (failures) return 1;
std::cout << "mod runtime tests passed\n";
return 0;
}