Files
psxrecomp/recompiler/tests/recompiler_patch_test.cpp
Matthew Stanley 4fca2d27d8 Allow explicit world axes for widescreen visibility cones
(cherry picked from commit 2c6e0a3a20)
2026-10-24 15:24:20 -07:00

1374 lines
62 KiB
C++

#include <array>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <functional>
#include <stdexcept>
#include <string>
#include <vector>
#include "code_generator.h"
#include "config_loader.h"
#include "control_flow.h"
#include "fmt/format.h"
#include "gte_register_classification.h"
#include "recompiler_patch.h"
namespace fs = std::filesystem;
using PSXRecompV4::RecompilerPatch;
namespace {
int failures = 0;
void check(bool condition, const std::string& name) {
if (condition) {
fmt::print("PASS {}\n", name);
} else {
fmt::print(stderr, "FAIL {}\n", name);
++failures;
}
}
template <typename Fn>
void check_throws(Fn&& fn, const std::string& needle,
const std::string& name) {
try {
fn();
check(false, name + " (did not throw)");
} catch (const std::exception& e) {
check(std::string(e.what()).find(needle) != std::string::npos,
name + " (" + e.what() + ")");
}
}
void append_word(std::vector<uint8_t>& bytes, uint32_t word) {
bytes.push_back(static_cast<uint8_t>(word));
bytes.push_back(static_cast<uint8_t>(word >> 8));
bytes.push_back(static_cast<uint8_t>(word >> 16));
bytes.push_back(static_cast<uint8_t>(word >> 24));
}
void write_word(PSXRecomp::PS1Executable& exe, uint32_t address,
uint32_t word) {
const size_t offset = static_cast<size_t>(address - exe.header.load_address);
exe.code_data[offset + 0] = static_cast<uint8_t>(word);
exe.code_data[offset + 1] = static_cast<uint8_t>(word >> 8);
exe.code_data[offset + 2] = static_cast<uint8_t>(word >> 16);
exe.code_data[offset + 3] = static_cast<uint8_t>(word >> 24);
}
std::string generate_first_instruction(uint32_t first_word,
std::vector<RecompilerPatch> patches,
bool overlay_mode,
PSXRecomp::CodeGenConfig config = {}) {
constexpr uint32_t base = 0x80010000u;
PSXRecomp::PS1Executable exe{};
exe.header.load_address = base;
exe.header.initial_pc = base;
exe.header.file_size = 20;
append_word(exe.code_data, first_word);
append_word(exe.code_data, 0x00000000u); // branch/nonbranch delay-slot nop
append_word(exe.code_data, 0x24030001u); // addiu v1, zero, 1
append_word(exe.code_data, 0x03E00008u); // jr ra
append_word(exe.code_data, 0x00000000u); // return delay-slot nop
PSXRecompV4::apply_recompiler_patches_to_executable(
exe, patches, overlay_mode);
PSXRecomp::Function function{};
function.start_addr = base;
function.end_addr = base + 20;
function.size = 20;
function.name = "patch_test";
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
const auto cfg = analyzer.analyze_function(function);
config.overlay_mode = overlay_mode;
PSXRecomp::CodeGenerator generator(exe, config);
return generator.generate_function(function, cfg).full_code;
}
std::string base_config() {
return R"toml([game]
name = "Patch Test"
id = "TEST-00000"
exe = "TEST.EXE"
load_address = "0x80010000"
entry_pc = "0x80010000"
text_size = "0x1000"
stack_base = "0x801FFFF0"
[recompiler]
seeds = "seeds.txt"
out_dir = "generated"
)toml";
}
fs::path write_config(const fs::path& root, const std::string& suffix,
const std::string& patch_tables) {
const fs::path path = root / ("game-" + suffix + ".toml");
std::ofstream file(path, std::ios::binary);
file << base_config() << patch_tables;
file.close();
return path;
}
void parser_tests(const fs::path& root) {
const auto valid = write_config(root, "valid", R"toml(
[[recompiler.patch]]
id = "gameplay-rate"
address = "0x80012340"
expected = "0x24020002"
replacement = "0x24020001"
note = "Test-only fixture"
)toml");
const auto config = PSXRecompV4::load_game_config(valid);
check(config.recompiler_patches.size() == 1,
"parser accepts one guarded patch");
check(config.recompiler_patches[0].id == "gameplay-rate" &&
config.recompiler_patches[0].address == 0x80012340u &&
config.recompiler_patches[0].expected == 0x24020002u &&
config.recompiler_patches[0].replacement == 0x24020001u,
"parser preserves patch fields");
const auto audio_buffer = write_config(root, "audio-buffer", R"toml(
[runtime]
[audio]
buffer_ms = 60
)toml");
const auto audio_config = PSXRecompV4::load_game_config(audio_buffer);
check(audio_config.runtime.audio_buffer_ms == 60,
"parser preserves per-game audio buffer target");
const auto controller_defaults = write_config(root, "controller-defaults", R"toml(
[controller]
p1_device = "keyboard"
p2_device = "gamepad"
p2_mode = "digital"
)toml");
const auto controller_defaults_config =
PSXRecompV4::load_game_config(controller_defaults);
check(controller_defaults_config.runtime.has_default_p1_device &&
controller_defaults_config.runtime.default_p1_device == "keyboard" &&
controller_defaults_config.runtime.has_default_p2_device &&
controller_defaults_config.runtime.default_p2_device == "gamepad" &&
controller_defaults_config.runtime.default_p2_mode ==
PSXRecompV4::PAD_MODE_DIGITAL,
"parser preserves per-game controller device defaults");
check(!controller_defaults_config.runtime.has_multitap_default,
"offline multitap keeps the framework default when unset");
const auto no_multitap = write_config(root, "no-multitap", R"toml(
[controller]
multitap = false
)toml");
const auto no_multitap_config = PSXRecompV4::load_game_config(no_multitap);
check(no_multitap_config.runtime.has_multitap_default &&
!no_multitap_config.runtime.multitap_default,
"parser preserves a title's offline multitap default");
const auto netplay_viewport = write_config(root, "netplay-viewport", R"toml(
[runtime]
[netplay]
local_viewport = "vertical_split"
local_viewport_aspect = "adaptive"
)toml");
const auto netplay_viewport_config =
PSXRecompV4::load_game_config(netplay_viewport);
check(netplay_viewport_config.netplay_local_viewport == "vertical_split",
"parser preserves netplay local viewport mode");
check(netplay_viewport_config.netplay_local_viewport_aspect == "adaptive",
"parser preserves netplay local viewport aspect");
const auto bad_audio_buffer = write_config(root, "bad-audio-buffer", R"toml(
[runtime]
[audio]
buffer_ms = 20
)toml");
check_throws(
[&] { (void)PSXRecompV4::load_game_config(bad_audio_buffer); },
"[audio] buffer_ms out of range (30..500)",
"parser rejects unsafe audio buffer target");
const auto duplicate_address = write_config(root, "duplicate-address", R"toml(
[[recompiler.patch]]
id = "first"
address = "0x80012340"
expected = "0x24020002"
replacement = "0x24020001"
[[recompiler.patch]]
id = "alias"
address = "0xA0012340"
expected = "0x24020002"
replacement = "0x24020001"
)toml");
check_throws(
[&] { (void)PSXRecompV4::load_game_config(duplicate_address); },
"physical address", "parser rejects duplicate aliased addresses");
const auto duplicate_id = write_config(root, "duplicate-id", R"toml(
[[recompiler.patch]]
id = "same-id"
address = "0x80012340"
expected = "0x24020002"
replacement = "0x24020001"
[[recompiler.patch]]
id = "same-id"
address = "0x80012344"
expected = "0x24030002"
replacement = "0x24030001"
)toml");
check_throws([&] { (void)PSXRecompV4::load_game_config(duplicate_id); },
"duplicate [[recompiler.patch]] id",
"parser rejects duplicate IDs");
const auto unaligned = write_config(root, "unaligned", R"toml(
[[recompiler.patch]]
id = "unaligned"
address = "0x80012342"
expected = "0x24020002"
replacement = "0x24020001"
)toml");
check_throws([&] { (void)PSXRecompV4::load_game_config(unaligned); },
"instruction-aligned", "parser rejects unaligned sites");
const auto negsub = write_config(root, "negsub", R"toml(
[widescreen.cull]
negsub_sites = ["0x80012340"]
)toml");
const auto negsub_config = PSXRecompV4::load_game_config(negsub);
check(negsub_config.ws_cull_negsub_sites ==
std::vector<uint32_t>{0x80012340u},
"parser preserves negsub cull sites");
const auto branch_keep = write_config(root, "branch-keep", R"toml(
[widescreen.cull]
branch_keep_sites = ["0x80012340"]
)toml");
const auto branch_keep_config = PSXRecompV4::load_game_config(branch_keep);
check(branch_keep_config.ws_cull_branch_keep_sites ==
std::vector<uint32_t>{0x80012340u},
"parser preserves branch keep sites");
const auto nclip_exact = write_config(root, "nclip-exact", R"toml(
[widescreen.cull]
nclip_exact_sites = ["0x80012340"]
)toml");
const auto nclip_exact_config = PSXRecompV4::load_game_config(nclip_exact);
check(nclip_exact_config.ws_cull_nclip_exact_sites ==
std::vector<uint32_t>{0x80012340u},
"parser preserves exact NCLIP branch sites");
const auto vxrange = write_config(root, "vxrange", R"toml(
[widescreen.cull]
vxrange_sites = ["0x80012340"]
)toml");
const auto vxrange_config = PSXRecompV4::load_game_config(vxrange);
check(vxrange_config.ws_cull_vxrange_sites ==
std::vector<uint32_t>{0x80012340u},
"parser preserves masked-u16 X-window sites");
const auto depth = write_config(root, "depth", R"toml(
[widescreen.cull]
depth_sites = ["0x80012340"]
)toml");
const auto depth_config = PSXRecompV4::load_game_config(depth);
check(depth_config.ws_cull_depth_sites ==
std::vector<uint32_t>{0x80012340u},
"parser preserves depth-bound sites");
const auto plane_nx = write_config(root, "plane-nx", R"toml(
[widescreen.cull]
plane_nx_sites = ["0x80012340"]
)toml");
const auto plane_nx_config = PSXRecompV4::load_game_config(plane_nx);
check(plane_nx_config.ws_cull_plane_nx_sites ==
std::vector<uint32_t>{0x80012340u},
"parser preserves side-plane nx load sites");
const auto xclip_load = write_config(root, "xclip-load", R"toml(
[widescreen.cull]
xclip_load_sites = ["0x80012340"]
)toml");
const auto xclip_load_config = PSXRecompV4::load_game_config(xclip_load);
check(xclip_load_config.ws_cull_xclip_load_sites ==
std::vector<uint32_t>{0x80012340u},
"parser preserves per-prim bound-load sites");
const auto keep = write_config(root, "cull-keep", R"toml(
[[widescreen.cull.keep]]
address = "0x8002B310"
expected = "0x28A21C01"
result = 1
[[widescreen.cull.keep]]
address = "0x8002B368"
expected = "0x0082202A"
result = 0
)toml");
const auto keep_config = PSXRecompV4::load_game_config(keep);
check(keep_config.ws_cull_keep_sites.size() == 2 &&
keep_config.ws_cull_keep_sites[0].address == 0x8002B310u &&
keep_config.ws_cull_keep_sites[0].expected == 0x28A21C01u &&
keep_config.ws_cull_keep_sites[0].result == 1u &&
keep_config.ws_cull_keep_sites[1].address == 0x8002B368u &&
keep_config.ws_cull_keep_sites[1].expected == 0x0082202Au &&
keep_config.ws_cull_keep_sites[1].result == 0u,
"parser preserves full-word-guarded maximal-participation sites");
const auto scale = write_config(root, "cull-scale", R"toml(
[[widescreen.cull.scale]]
address = "0x8002C734"
expected = "0x0062182A"
operand = "rt"
half_extent = 160
[[widescreen.cull.scale]]
address = "0x8002C758"
expected = "0x0043102A"
operand = "rs"
half_extent = 160
)toml");
const auto scale_config = PSXRecompV4::load_game_config(scale);
check(scale_config.ws_cull_scale_sites.size() == 2 &&
scale_config.ws_cull_scale_sites[0].address == 0x8002C734u &&
scale_config.ws_cull_scale_sites[0].expected == 0x0062182Au &&
scale_config.ws_cull_scale_sites[0].operand == 1u &&
scale_config.ws_cull_scale_sites[0].half_extent == 160u &&
scale_config.ws_cull_scale_sites[1].operand == 0u,
"parser preserves register-register frustum-edge scale sites");
const auto signed_bound = write_config(root, "signed-x-bound", R"toml(
[[widescreen.signed_x_bound]]
address = "0x8002D290"
expected = "0x2402FF00"
[[widescreen.signed_x_bound]]
address = "0x8002D298"
expected = "0x3C02FFF0"
)toml");
const auto signed_bound_config =
PSXRecompV4::load_game_config(signed_bound);
check(signed_bound_config.ws_signed_x_bound_sites.size() == 2 &&
signed_bound_config.ws_signed_x_bound_sites[0].address ==
0x8002D290u &&
signed_bound_config.ws_signed_x_bound_sites[0].expected ==
0x2402FF00u &&
signed_bound_config.ws_signed_x_bound_sites[1].address ==
0x8002D298u &&
signed_bound_config.ws_signed_x_bound_sites[1].expected ==
0x3C02FFF0u,
"parser preserves guarded signed pixel and Q16 X bounds");
const auto bad_keep = write_config(root, "cull-keep-bad", R"toml(
[[widescreen.cull.keep]]
address = "0x8002B310"
expected = "0x24820001"
result = 1
)toml");
check_throws([&] { (void)PSXRecompV4::load_game_config(bad_keep); },
"expected must be SLT/SLTU/SLTI/SLTIU",
"parser rejects non-comparison maximal-participation sites");
const auto range = write_config(root, "range-cull", R"toml(
[widescreen.cull]
range_sites = ["0x80012340"]
bias_lower_sites = ["0x80012348"]
activation_guard_pixels = 256
)toml");
const auto range_config = PSXRecompV4::load_game_config(range);
check(range_config.ws_cull_range_sites ==
std::vector<uint32_t>{0x80012340u} &&
range_config.ws_cull_bias_lower_sites ==
std::vector<uint32_t>{0x80012348u} &&
range_config.ws_cull_activation_guard_pixels == 256,
"parser preserves explicit range sites and activation guard");
auto changed_lower_config = range_config;
changed_lower_config.ws_cull_bias_lower_sites.clear();
check(PSXRecompV4::overlay_codegen_config_hash(range_config) !=
PSXRecompV4::overlay_codegen_config_hash(changed_lower_config),
"lower activation endpoint changes overlay cache identity");
const auto bad_activation_guard =
write_config(root, "range-cull-bad-activation-guard", R"toml(
[widescreen.cull]
activation_guard_pixels = 257
)toml");
check_throws(
[&] {
(void)PSXRecompV4::load_game_config(bad_activation_guard);
},
"activation_guard_pixels must be in [0, 256]",
"parser rejects an oversized activation-only guard");
const auto angle = write_config(root, "angle-cull", R"toml(
[[widescreen.cull.angle]]
address = "0x8013F138"
expected = "0x24020155"
)toml");
const auto angle_config = PSXRecompV4::load_game_config(angle);
check(angle_config.ws_cull_angle_sites.size() == 1 &&
angle_config.ws_cull_angle_sites[0].address ==
0x8013F138u &&
angle_config.ws_cull_angle_sites[0].expected ==
0x24020155u,
"parser preserves exact terrain-frustum angle sites");
auto changed_angle_config = angle_config;
changed_angle_config.ws_cull_angle_sites[0].expected =
0x24020156u;
check(PSXRecompV4::overlay_codegen_config_hash(angle_config) !=
PSXRecompV4::overlay_codegen_config_hash(
changed_angle_config),
"terrain angle sites change overlay cache identity");
const auto bad_angle = write_config(root, "angle-cull-bad", R"toml(
[[widescreen.cull.angle]]
address = "0x8013F138"
expected = "0x24420155"
)toml");
check_throws([&] { (void)PSXRecompV4::load_game_config(bad_angle); },
"expected must be ADDI/ADDIU rt,zero,imm",
"parser rejects non-constant terrain-angle instructions");
const std::string aspect_tables = R"toml(
[widescreen.cull.aspect_cone]
forward_addr = "0x1F8000E8"
object_type_offset = 12
object_reg = 19
x_reg = 16
z_reg = 17
y_reg = 18
hysteresis_pixels = 24
queue_reserve = 4
queue_count_addrs = ["0x1F800144", "0x1F800150", "0x1F80015C"]
queue_capacities = [24, 40, 28]
queue_type_masks = ["0x00000204", "0x00000010", "0x00000020"]
[[widescreen.cull.aspect_cone.sites]]
address = "0x80010000"
expected = "0x28620370"
[[widescreen.cull.aspect_cone.sites]]
address = "0x80010004"
expected = "0x0082202A"
cosine_threshold = 856
object_reg = 20
x_reg = 19
z_reg = 18
y_reg = 17
queue_guard = false
)toml";
const auto aspect_cone = write_config(root, "aspect-cone", aspect_tables);
const auto aspect_config =
PSXRecompV4::load_game_config(aspect_cone);
check(aspect_config.ws_aspect_cone.sites.size() == 2 &&
aspect_config.ws_aspect_cone.sites[0].address ==
0x80010000u &&
aspect_config.ws_aspect_cone.sites[0].expected ==
0x28620370u &&
aspect_config.ws_aspect_cone.sites[0].cosine_threshold ==
0x370u &&
aspect_config.ws_aspect_cone.sites[1].expected ==
0x0082202Au &&
aspect_config.ws_aspect_cone.sites[1].cosine_threshold ==
856u &&
aspect_config.ws_aspect_cone.sites[1].object_reg == 20u &&
aspect_config.ws_aspect_cone.sites[1].x_reg == 19u &&
aspect_config.ws_aspect_cone.sites[1].z_reg == 18u &&
aspect_config.ws_aspect_cone.sites[1].y_reg == 17u &&
!aspect_config.ws_aspect_cone.sites[1].queue_guard &&
aspect_config.ws_aspect_cone.forward_addr ==
0x1F8000E8u &&
aspect_config.ws_aspect_cone.forward_offsets ==
std::array<uint32_t, 3>{0u, 2u, 4u} &&
aspect_config.ws_aspect_cone.object_reg == 19u &&
aspect_config.ws_aspect_cone.x_reg == 16u &&
aspect_config.ws_aspect_cone.z_reg == 17u &&
aspect_config.ws_aspect_cone.y_reg == 18u &&
aspect_config.ws_aspect_cone.hysteresis_pixels == 24u &&
aspect_config.ws_aspect_cone.queue_reserve == 4u &&
aspect_config.ws_aspect_cone.queue_capacities ==
std::array<uint32_t, 3>{24u, 40u, 28u},
"parser preserves aspect-aware participation policy");
const auto aspect_hash = PSXRecompV4::overlay_codegen_config_hash(
aspect_config);
auto axis_tables = aspect_tables;
axis_tables.insert(axis_tables.find("forward_addr"),
"forward_offsets = [0, 4, 2]\n");
const auto axis_config = PSXRecompV4::load_game_config(
write_config(root, "aspect-cone-axis-order", axis_tables));
check(axis_config.ws_aspect_cone.forward_offsets ==
std::array<uint32_t, 3>{0u, 4u, 2u},
"parser maps a middle-vertical forward vector explicitly");
check(aspect_hash != PSXRecompV4::overlay_codegen_config_hash(axis_config),
"forward component order changes overlay cache identity");
axis_tables.replace(axis_tables.find("[0, 4, 2]"), 9, "[0, 2, 2]");
const auto bad_axis_config =
write_config(root, "aspect-cone-repeated-axis", axis_tables);
check_throws([&] { (void)PSXRecompV4::load_game_config(bad_axis_config); },
"forward_offsets must be a permutation",
"parser rejects a duplicated forward component");
auto changed_aspect_config = aspect_config;
changed_aspect_config.ws_aspect_cone.hysteresis_pixels++;
check(aspect_hash != PSXRecompV4::overlay_codegen_config_hash(
changed_aspect_config),
"aspect participation policy changes overlay cache identity");
changed_aspect_config = aspect_config;
changed_aspect_config.ws_cull_guard_pixels++;
check(aspect_hash != PSXRecompV4::overlay_codegen_config_hash(
changed_aspect_config),
"widescreen guard changes overlay cache identity");
changed_aspect_config = aspect_config;
changed_aspect_config.ws_cull_activation_guard_pixels++;
check(aspect_hash != PSXRecompV4::overlay_codegen_config_hash(
changed_aspect_config),
"activation-only guard changes overlay cache identity");
changed_aspect_config = aspect_config;
changed_aspect_config.ws_aspect_cone.sites[1].queue_guard = true;
check(aspect_hash != PSXRecompV4::overlay_codegen_config_hash(
changed_aspect_config),
"per-site aspect participation policy changes cache identity");
const auto bad_aspect_opcode =
write_config(root, "aspect-cone-bad-opcode", R"toml(
[widescreen.cull.aspect_cone]
forward_addr = "0x1F8000E8"
object_type_offset = 12
object_reg = 19
x_reg = 16
z_reg = 17
y_reg = 18
queue_count_addrs = ["0x1F800144", "0x1F800150", "0x1F80015C"]
queue_capacities = [24, 40, 28]
queue_type_masks = ["0x00000204", "0x00000010", "0x00000020"]
[[widescreen.cull.aspect_cone.sites]]
address = "0x80010000"
expected = "0x2C620370"
)toml");
check_throws(
[&] {
(void)PSXRecompV4::load_game_config(bad_aspect_opcode);
},
"expected must be signed SLTI or SLT",
"parser rejects unsigned aspect-cone guards");
const auto bad_aspect_slt_threshold =
write_config(root, "aspect-cone-bad-slt-threshold", R"toml(
[widescreen.cull.aspect_cone]
forward_addr = "0x1F8000E8"
object_type_offset = 12
object_reg = 19
x_reg = 16
z_reg = 17
y_reg = 18
queue_count_addrs = ["0x1F800144", "0x1F800150", "0x1F80015C"]
queue_capacities = [24, 40, 28]
queue_type_masks = ["0x00000204", "0x00000010", "0x00000020"]
[[widescreen.cull.aspect_cone.sites]]
address = "0x80010000"
expected = "0x0082202A"
)toml");
check_throws(
[&] {
(void)PSXRecompV4::load_game_config(
bad_aspect_slt_threshold);
},
"cosine_threshold must be in [1, 1023]",
"parser requires an explicit Q10 threshold for SLT cones");
const auto bad_aspect_arrays =
write_config(root, "aspect-cone-bad-arrays", R"toml(
[widescreen.cull.aspect_cone]
forward_addr = "0x1F8000E8"
object_type_offset = 12
object_reg = 19
x_reg = 16
z_reg = 17
y_reg = 18
queue_count_addrs = ["0x1F800144", "0x1F800150"]
queue_capacities = [24, 40, 28]
queue_type_masks = ["0x00000204", "0x00000010", "0x00000020"]
[[widescreen.cull.aspect_cone.sites]]
address = "0x80010000"
expected = "0x28620370"
)toml");
check_throws(
[&] {
(void)PSXRecompV4::load_game_config(bad_aspect_arrays);
},
"must contain exactly three values",
"parser rejects incomplete aspect-cone queue metadata");
}
void capture_history_config_tests(const fs::path& root) {
const auto valid = write_config(root, "capture-history", R"toml(
[runtime]
overlay_cache = true
overlay_capture_history = true
overlay_capture_persist_dir = ".aot_capture_history/TEST-00000"
)toml");
const auto cfg = PSXRecompV4::load_game_config(valid);
check(cfg.runtime.overlay_capture_history,
"parser enables durable overlay capture history");
check(cfg.runtime.overlay_capture_persist_dir ==
".aot_capture_history/TEST-00000",
"parser preserves project-relative capture history directory");
const auto escaping = write_config(root, "capture-history-escape", R"toml(
[runtime]
overlay_capture_persist_dir = "../outside"
)toml");
check_throws([&] { (void)PSXRecompV4::load_game_config(escaping); },
"must stay inside the project",
"parser rejects escaping capture history directory");
const auto absolute = write_config(root, "capture-history-absolute", R"toml(
[runtime]
overlay_capture_persist_dir = "C:/outside"
)toml");
check_throws([&] { (void)PSXRecompV4::load_game_config(absolute); },
"must be project-relative",
"parser rejects absolute capture history directory");
// A game.toml travels between hosts, so each of these must be rejected on
// every host, not only on the one whose path grammar it is written in.
// TOML literal strings ('...') are used because a backslash is an escape
// inside a TOML basic string. Cases marked POSIX-visible are the ones a
// host-native std::filesystem check silently accepted under POSIX.
struct RejectedDir {
const char* value; // as written in game.toml
const char* needle; // expected message fragment
const char* name;
};
static constexpr RejectedDir rejected[] = {
{"'/outside'", "must be project-relative",
"parser rejects rooted capture history directory"},
{R"('\outside')", "must be project-relative",
"parser rejects backslash-rooted capture history directory"},
{R"('C:\outside')", "must be project-relative",
"parser rejects backslash drive-qualified capture history directory"},
{"'C:outside'", "must be project-relative",
"parser rejects drive-relative capture history directory"},
{R"('\\server\share\outside')", "must be project-relative",
"parser rejects UNC capture history directory"},
{R"('..\outside')", "must stay inside the project",
"parser rejects backslash-escaping capture history directory"},
{R"('nested\..\..\outside')", "must stay inside the project",
"parser rejects nested backslash-escaping capture history directory"},
{"'nested/..'", "must stay inside the project",
"parser rejects a trailing .. component"},
// A basic string, not a literal one: TOML forbids a raw NUL byte but
// allows the \u0000 escape that produces it. The runtime passes this
// value on as a C string, so a NUL would truncate it after validation.
{R"("..\u0000keep")", "must not contain a NUL byte",
"parser rejects an embedded NUL escape"},
};
for (size_t i = 0; i < std::size(rejected); ++i) {
const auto& c = rejected[i];
const auto cfg_path = write_config(
root, fmt::format("capture-history-rejected-{}", i),
fmt::format("[runtime]\noverlay_capture_persist_dir = {}\n",
c.value));
check_throws([&] { (void)PSXRecompV4::load_game_config(cfg_path); },
c.needle, c.name);
}
// A dotfile is not a `..` component; the guard must not over-reject.
const auto dotted = write_config(root, "capture-history-dotted", R"toml(
[runtime]
overlay_capture_history = true
overlay_capture_persist_dir = ".aot_capture_history/..keep/TEST-00000"
)toml");
const auto dotted_cfg = PSXRecompV4::load_game_config(dotted);
check(dotted_cfg.runtime.overlay_capture_persist_dir ==
".aot_capture_history/..keep/TEST-00000",
"parser accepts a capture history component that merely starts with dots");
}
void codegen_tests() {
constexpr uint32_t original = 0x24020002u; // addiu v0, zero, 2
constexpr uint32_t replacement = 0x24020001u; // addiu v0, zero, 1
const RecompilerPatch physical_alias{
"gameplay-rate", 0x00010000u, original, replacement, ""};
const std::string applied =
generate_first_instruction(original, {physical_alias}, false);
check(applied.find("0x24020001") != std::string::npos &&
applied.find("cpu->gpr[2] = 1;") != std::string::npos,
"codegen applies exact patch through physical alias");
check_throws(
[&] { (void)generate_first_instruction(0x24020003u,
{physical_alias}, false); },
"wrong game revision or stale patch",
"main codegen fails on stale opcode guard");
const std::string overlay =
generate_first_instruction(0x24020003u, {physical_alias}, true);
check(overlay.find("0x24020003") != std::string::npos &&
overlay.find("0x24020001") == std::string::npos,
"overlay nonmatching variant remains unchanged");
constexpr uint32_t beq_v0_zero = 0x10400002u;
constexpr uint32_t bne_v0_zero = 0x14400002u;
const RecompilerPatch branch_patch{
"feature-branch", 0x80010000u, beq_v0_zero, bne_v0_zero, ""};
const std::string branch =
generate_first_instruction(beq_v0_zero, {branch_patch}, false);
check(branch.find("cpu->gpr[2] != cpu->gpr[0]") != std::string::npos,
"patch is applied before control-flow analysis");
std::vector<RecompilerPatch> merged{physical_alias};
PSXRecompV4::merge_recompiler_patches(merged, {physical_alias});
check(merged.size() == 1,
"config merge deduplicates an identical patch");
const RecompilerPatch conflicting_id{
"gameplay-rate", 0x00010004u, original, replacement, ""};
check_throws(
[&] {
PSXRecompV4::merge_recompiler_patches(merged, {conflicting_id});
},
"conflicting recompiler patches",
"config merge rejects cross-config ID conflicts");
PSXRecomp::CodeGenConfig negsub_config;
negsub_config.ws_cull_negsub_sites.insert(0x80010000u);
const std::string negsub = generate_first_instruction(
0x00041023u, {}, false, negsub_config); // subu v0,zero,a0
check(negsub.find("cpu->gpr[2] = 0u - cpu->gpr[4] - "
"(uint32_t)psx_ws_x_margin()") != std::string::npos,
"codegen emits configured negsub horizontal low-edge widen");
const std::string overlay_mismatch = generate_first_instruction(
0x00041021u, {}, true, negsub_config); // addu v0,zero,a0
check(overlay_mismatch.find("ws cull negsub") == std::string::npos,
"overlay nonmatching negsub variant remains unchanged");
PSXRecomp::CodeGenConfig vxrange_config;
vxrange_config.ws_cull_vxrange_sites.insert(0x80010000u);
const std::string vxrange = generate_first_instruction(
0x2C820140u, {}, false, vxrange_config); // sltiu v0,a0,0x140
check(vxrange.find("cpu->gpr[2] = psx_ws_cull_vxrange(cpu->gpr[4], 320)") !=
std::string::npos,
"codegen routes masked-u16 X-window sites through shared helper");
const std::string vxrange_overlay_mismatch = generate_first_instruction(
0x24820140u, {}, true, vxrange_config); // addiu v0,a0,0x140
check(vxrange_overlay_mismatch.find("ws cull masked-u16") == std::string::npos,
"overlay nonmatching masked-u16 variant remains unchanged");
PSXRecomp::CodeGenConfig depth_config;
depth_config.ws_cull_depth_sites.insert(0x80010000u);
const std::string signed_depth = generate_first_instruction(
0x28827FFFu, {}, false, depth_config); // slti v0,a0,0x7fff
check(signed_depth.find("psx_ws_depth_bound(32767)") != std::string::npos,
"codegen emits signed aspect-scaled depth bound");
const std::string unsigned_depth = generate_first_instruction(
0x2C82FFFFu, {}, false, depth_config); // sltiu v0,a0,-1
check(unsigned_depth.find("psx_ws_depth_bound(-1)") != std::string::npos,
"unsigned depth emit preserves MIPS immediate sign extension");
const std::string depth_overlay_mismatch = generate_first_instruction(
0x24827FFFu, {}, true, depth_config); // addiu v0,a0,0x7fff
check(depth_overlay_mismatch.find("ws cull depth") == std::string::npos,
"overlay nonmatching depth variant remains unchanged");
PSXRecomp::CodeGenConfig signed_bound_config;
signed_bound_config.ws_signed_x_bound_sites.push_back(
{0x80010000u, 0x2402FF00u}); // addiu v0,zero,-256
const std::string signed_pixel_bound = generate_first_instruction(
0x2402FF00u, {}, false, signed_bound_config);
check(signed_pixel_bound.find("psx_ws_screen_x_bound(-256)") !=
std::string::npos,
"codegen emits guarded signed screen-pixel X bound");
signed_bound_config.ws_signed_x_bound_sites[0] =
{0x80010000u, 0x3C02FFF0u}; // lui v0,0xfff0
const std::string signed_q16_bound = generate_first_instruction(
0x3C02FFF0u, {}, false, signed_bound_config);
check(signed_q16_bound.find(
"psx_ws_player_x_bound((int32_t)0xFFF00000)") !=
std::string::npos,
"codegen keeps guarded LUI X bounds on the Q16 helper");
const std::string signed_bound_overlay_mismatch = generate_first_instruction(
0x24020078u, {}, true, signed_bound_config);
check(signed_bound_overlay_mismatch.find("typed native-wide signed") ==
std::string::npos,
"overlay nonmatching signed-bound variant remains unchanged");
PSXRecomp::CodeGenConfig range_config;
range_config.ws_cull_range_sites.insert(0x80010000u);
const std::string range = generate_first_instruction(
0x2C8201C1u, {}, false, range_config); // sltiu v0,a0,0x1c1
check(range.find("2*psx_ws_x_margin()") != std::string::npos,
"native range emit widens by both horizontal margins");
auto activation_range_config = range_config;
activation_range_config.ws_cull_activation_guard_pixels = 256;
const std::string activation_range = generate_first_instruction(
0x2C8201C1u, {}, false,
activation_range_config); // sltiu v0,a0,0x1c1
check(activation_range.find(
"2*(psx_ws_x_margin() > 0 ? psx_ws_x_margin() + 256 : 0)") !=
std::string::npos,
"activation range gains an isolated resident-object lead");
PSXRecomp::CodeGenConfig activation_bias_config;
activation_bias_config.ws_cull_bias_sites.insert(0x80010000u);
activation_bias_config.ws_cull_activation_guard_pixels = 256;
const std::string activation_bias = generate_first_instruction(
0x248200E6u, {}, false,
activation_bias_config); // addiu v0,a0,230
check(activation_bias.find(
"(psx_ws_x_margin() > 0 ? psx_ws_x_margin() + 256 : 0)") !=
std::string::npos,
"activation bias gains the same isolated resident-object lead");
PSXRecomp::CodeGenConfig lower_bias_config;
lower_bias_config.ws_cull_bias_lower_sites.insert(0x80010000u);
for (const uint32_t opcode : {0x2082FFD0u, 0x2482FFD0u}) {
const std::string lower = generate_first_instruction(opcode, {}, false, lower_bias_config);
check(lower.find("((int32_t)-48 - psx_ws_x_margin())") != std::string::npos,
"ADDI/ADDIU lower endpoint expands left without changing camera source");
check(lower.find("PGXP_ALU(") != std::string::npos &&
lower.find("_pgx1 = cpu->gpr[4]") != std::string::npos,
"lower bound preserves native ALU precision tracking");
}
lower_bias_config.ws_cull_activation_guard_pixels = 32;
const std::string lower_guard = generate_first_instruction(
0x2482FFD0u, {}, false, lower_bias_config);
check(lower_guard.find("-48 - (psx_ws_x_margin() > 0 ? psx_ws_x_margin() + 32 : 0)") !=
std::string::npos,
"lower endpoint includes activation guard only while wide");
const std::string lower_mismatch = generate_first_instruction(
0x3482FFD0u, {}, true, lower_bias_config); // ORI variant is not an add.
check(lower_mismatch.find("psx_ws_x_margin()") == std::string::npos,
"lower bias leaves a nonmatching overlay opcode unchanged");
PSXRecomp::CodeGenConfig branch_keep_config;
branch_keep_config.ws_cull_branch_keep_sites.insert(0x80010000u);
const std::string branch_keep = generate_first_instruction(
0x14400002u, {}, false, branch_keep_config); // bne v0,zero,+2
check(branch_keep.find(
"psx_ws_x_margin() > 0 ? 0 : (cpu->gpr[2] != cpu->gpr[0])") !=
std::string::npos &&
branch_keep.find("ws branch keep") != std::string::npos,
"codegen emits guarded branch keep predicate");
PSXRecomp::CodeGenConfig nclip_exact_config;
nclip_exact_config.ws_cull_nclip_exact_sites.insert(0x80010000u);
const std::string nclip_exact = generate_first_instruction(
0x04400002u, {}, false, nclip_exact_config); // bltz v0,+2
check(nclip_exact.find(
"gte_nclip_exact_sign((int32_t)cpu->gpr[2], 0x80010000u) < 0") !=
std::string::npos &&
nclip_exact.find("ws exact nclip") != std::string::npos,
"codegen emits title-scoped exact NCLIP predicate");
for (uint32_t word : {0x18400002u, 0x1C400002u, 0x04410002u}) {
const std::string guarded = generate_first_instruction(word, {}, false, nclip_exact_config);
check(guarded.find("psx_ws_nclip_branch(0x80010000u,") != std::string::npos &&
guarded.find("guarded wide nclip") != std::string::npos,
"inclusive/keep NCLIP branches carry full-word identity to the host");
}
PSXRecomp::CodeGenConfig nclip_keep_config;
nclip_keep_config.ws_cull_nclip_keep_sites.insert(0x80010000u);
const std::string nclip_keep_blez = generate_first_instruction(
0x18400002u, {}, false, nclip_keep_config);
check(nclip_keep_blez.find("psx_ws_x_margin() > 0 ? 0 : ((int32_t)cpu->gpr[2] <= 0)") !=
std::string::npos,
"codegen NCLIP keep covers blez");
PSXRecomp::CodeGenConfig plane_nx_config;
plane_nx_config.ws_cull_plane_nx_sites.insert(0x80010000u);
const std::string plane_nx = generate_first_instruction(
0x8C84C828u, {}, false, plane_nx_config); // lw a0,-0x37d8(a0)
check(plane_nx.find("psx_ws_plane_nx((int32_t)psx_cyc_load_word") !=
std::string::npos,
"codegen routes side-plane nx loads through shared helper");
const std::string plane_nx_overlay_mismatch = generate_first_instruction(
0x9484C828u, {}, true, plane_nx_config); // lhu a0,-0x37d8(a0)
check(plane_nx_overlay_mismatch.find("ws cull plane nx") == std::string::npos,
"overlay nonmatching plane-nx variant remains unchanged");
PSXRecomp::CodeGenConfig xclip_config;
xclip_config.ws_cull_xclip_load_sites.insert(0x80010000u);
const std::string xclip = generate_first_instruction(
0x8C6200F8u, {}, false, xclip_config); // lw v0,0xf8(v1)
check(xclip.find("psx_ws_xclip_bound(psx_cyc_load_word") != std::string::npos,
"codegen routes per-prim bound loads through shared helper");
const std::string xclip_overlay_mismatch = generate_first_instruction(
0x246200F8u, {}, true, xclip_config); // addiu v0,v1,0xf8
check(xclip_overlay_mismatch.find("ws cull xclip") == std::string::npos,
"overlay nonmatching xclip variant remains unchanged");
PSXRecomp::CodeGenConfig keep_config;
keep_config.ws_cull_keep_sites.push_back(
{0x80010000u, 0x28821C01u, 1u}); // slti v0,a0,0x1c01
const std::string keep_true = generate_first_instruction(
0x28821C01u, {}, false, keep_config);
check(keep_true.find("psx_ws_cull_keep_result") != std::string::npos &&
keep_true.find(", 1u)") != std::string::npos,
"codegen emits guarded widescreen keep-true comparison");
keep_config.ws_cull_keep_sites[0] =
{0x80010000u, 0x0082202Au, 0u}; // slt a0,a0,v0
const std::string keep_false = generate_first_instruction(
0x0082202Au, {}, false, keep_config);
check(keep_false.find("psx_ws_cull_keep_result") != std::string::npos &&
keep_false.find(", 0u)") != std::string::npos,
"codegen emits guarded widescreen keep-false comparison");
const std::string keep_overlay_mismatch = generate_first_instruction(
0x0082202Bu, {}, true, keep_config); // sltu at same overlay VA
check(keep_overlay_mismatch.find("maximal object/model participation") ==
std::string::npos,
"overlay full-word mismatch leaves keep site unchanged");
PSXRecomp::CodeGenConfig scale_config_gen;
scale_config_gen.ws_cull_scale_sites.push_back(
{0x80010000u, 0x0062182Au, 1u, 160u}); // slt v1,v1,v0: scale rt
const std::string scale_rt = generate_first_instruction(
0x0062182Au, {}, false, scale_config_gen);
check(scale_rt.find("psx_ws_cull_scale((int32_t)") != std::string::npos &&
scale_rt.find(", 160)") != std::string::npos &&
scale_rt.find("(int32_t)cpu->gpr[3] < (int32_t)(uint32_t)psx_ws_cull_scale") !=
std::string::npos,
"codegen scales the configured rt bound of a signed frustum edge");
scale_config_gen.ws_cull_scale_sites[0] = {0x80010000u, 0x0043102Au, 0u, 160u};
const std::string scale_rs = generate_first_instruction(
0x0043102Au, {}, false, scale_config_gen);
check(scale_rs.find("(int32_t)(uint32_t)psx_ws_cull_scale((int32_t)cpu->gpr[2], 160) < (int32_t)cpu->gpr[3]") !=
std::string::npos,
"codegen scales the configured rs bound of the opposite edge");
const std::string scale_overlay_mismatch = generate_first_instruction(
0x0043102Bu, {}, true, scale_config_gen); // sltu at same overlay VA
check(scale_overlay_mismatch.find("psx_ws_cull_scale") == std::string::npos,
"overlay full-word mismatch leaves scale site unchanged");
PSXRecomp::CodeGenConfig angle_config;
angle_config.ws_cull_angle_sites.push_back(
{0x80010000u, 0x24020155u});
const std::string angle = generate_first_instruction(
0x24020155u, {}, false, angle_config);
check(angle.find("psx_ws_angle_widen(341u)") !=
std::string::npos,
"codegen emits exact terrain-frustum angle helper");
auto activation_isolated_angle_config = angle_config;
activation_isolated_angle_config.ws_cull_activation_guard_pixels = 256;
const std::string activation_isolated_angle = generate_first_instruction(
0x24020155u, {}, false, activation_isolated_angle_config);
check(activation_isolated_angle == angle,
"activation guard leaves terrain-frustum emission byte-identical");
const std::string angle_overlay_mismatch =
generate_first_instruction(0x240201C7u, {}, true, angle_config);
check(angle_overlay_mismatch.find("psx_ws_angle_widen") ==
std::string::npos,
"overlay full-word mismatch leaves terrain angle unchanged");
PSXRecomp::CodeGenConfig aspect_config;
aspect_config.ws_aspect_cone.sites.push_back(
{0x80010000u, 0x28620370u}); // slti v0,v1,0x370 reject predicate
aspect_config.ws_aspect_cone.object_reg = 19u;
aspect_config.ws_aspect_cone.x_reg = 16u;
aspect_config.ws_aspect_cone.z_reg = 17u;
aspect_config.ws_aspect_cone.y_reg = 18u;
const std::string aspect = generate_first_instruction(
0x28620370u, {}, false, aspect_config);
check(aspect.find(
"psx_ws_aspect_cone_result(0x80010000u") !=
std::string::npos &&
aspect.find("cpu->gpr[19]") != std::string::npos &&
aspect.find("(int32_t)(int16_t)cpu->gpr[16]") !=
std::string::npos &&
aspect.find("(int32_t)(int16_t)cpu->gpr[17]") !=
std::string::npos &&
aspect.find("(int32_t)(int16_t)cpu->gpr[18]") !=
std::string::npos,
"codegen emits full-word-guarded aspect-cone helper");
const std::string aspect_overlay_mismatch =
generate_first_instruction(0x28620358u, {}, true, aspect_config);
check(aspect_overlay_mismatch.find("psx_ws_aspect_cone_result") ==
std::string::npos,
"overlay full-word mismatch leaves aspect-cone site unchanged");
PSXRecomp::CodeGenConfig part_config;
PSXRecompV4::WidescreenAspectConeSite part_site;
part_site.address = 0x80010000u;
part_site.expected = 0x0082202Au; // slt a0,a0,v0
part_site.cosine_threshold = 856u;
part_site.object_reg = 20u;
part_site.x_reg = 19u;
part_site.z_reg = 18u;
part_site.y_reg = 17u;
part_site.queue_guard = false;
part_config.ws_aspect_cone.sites.push_back(part_site);
const std::string part = generate_first_instruction(
0x0082202Au, {}, false, part_config);
check(part.find("cpu->gpr[4] = psx_ws_aspect_cone_result") !=
std::string::npos &&
part.find("cpu->gpr[20]") != std::string::npos &&
part.find("(int32_t)(int16_t)cpu->gpr[19]") !=
std::string::npos &&
part.find("(int32_t)(int16_t)cpu->gpr[18]") !=
std::string::npos &&
part.find("(int32_t)(int16_t)cpu->gpr[17]") !=
std::string::npos,
"codegen supports exact per-child SLT cone metadata");
}
void gte_codegen_classification_tests() {
bool all_ok = true;
for (uint8_t reg = 0; reg < 32; ++reg) {
const auto expect = [&](uint32_t word, const std::string& call,
bool needs_helper, const char *kind) {
const std::string code = generate_first_instruction(word, {}, false);
const bool has_helper = code.find(call) != std::string::npos;
if (has_helper != needs_helper) {
fmt::print(stderr,
"FAIL full-game GTE {} reg {} helper={} expected={}\n",
kind, reg, has_helper, needs_helper);
all_ok = false;
}
};
const auto expect_zero_destination = [&](uint32_t word,
const std::string& read,
const char *kind) {
const std::string code = generate_first_instruction(word, {}, false);
if (code.find("cpu->gpr[0] =") != std::string::npos ||
code.find(read) == std::string::npos ||
code.find("psx_gte_read(cpu, 0)") == std::string::npos) {
fmt::print(stderr,
"FAIL full-game GTE {} reg {} writes $zero or drops read/timing\n",
kind, reg);
all_ok = false;
}
};
const uint32_t cop2 = 0x12u << 26;
expect(cop2 | (0x00u << 21) | (2u << 16) | (uint32_t(reg) << 11),
fmt::format("gte_read_data(cpu, {})", reg),
PSXRecompGTERegisters::data_read_needs_helper(reg), "MFC2");
expect(cop2 | (0x02u << 21) | (2u << 16) | (uint32_t(reg) << 11),
fmt::format("gte_read_ctrl(cpu, {})", reg),
PSXRecompGTERegisters::ctrl_read_needs_helper(reg), "CFC2");
expect_zero_destination(
cop2 | (0x00u << 21) | (0u << 16) | (uint32_t(reg) << 11),
PSXRecompGTERegisters::data_read_needs_helper(reg)
? fmt::format("gte_read_data(cpu, {})", reg)
: fmt::format("cpu->gte_data[{}]", reg),
"MFC2");
expect_zero_destination(
cop2 | (0x02u << 21) | (0u << 16) | (uint32_t(reg) << 11),
PSXRecompGTERegisters::ctrl_read_needs_helper(reg)
? fmt::format("gte_read_ctrl(cpu, {})", reg)
: fmt::format("cpu->gte_ctrl[{}]", reg),
"CFC2");
expect(cop2 | (0x04u << 21) | (2u << 16) | (uint32_t(reg) << 11),
fmt::format("gte_write_data(cpu, {}", reg),
PSXRecompGTERegisters::data_write_needs_helper(reg), "MTC2");
expect(cop2 | (0x06u << 21) | (2u << 16) | (uint32_t(reg) << 11),
fmt::format("gte_write_ctrl(cpu, {}", reg),
PSXRecompGTERegisters::ctrl_write_needs_helper(reg), "CTC2");
expect((0x32u << 26) | (3u << 21) | (uint32_t(reg) << 16),
fmt::format("gte_write_data(cpu, {}", reg),
PSXRecompGTERegisters::data_write_needs_helper(reg), "LWC2");
expect((0x3Au << 26) | (3u << 21) | (uint32_t(reg) << 16),
fmt::format("gte_read_data(cpu, {})", reg),
PSXRecompGTERegisters::data_read_needs_helper(reg), "SWC2");
}
check(all_ok, "full-game GTE helper classification covers all registers");
}
void jump_table_producer_codegen_test() {
constexpr uint32_t base = 0x80010000u;
constexpr uint32_t entry = base + 0x500u;
PSXRecomp::PS1Executable exe{};
exe.header.load_address = base;
exe.header.initial_pc = entry;
exe.header.file_size = 0x1000u;
exe.code_data.resize(0x1000u, 0u);
write_word(exe, base + 0x500u, 0x3C088001u); // lui t0,0x8001
write_word(exe, base + 0x504u, 0x25100A00u); // addiu s0,t0,0x0a00
write_word(exe, base + 0x50Cu, 0x2C620003u); // sltiu v0,v1,3
write_word(exe, base + 0x510u, 0x1040001Bu); // beq v0,zero,+0x580
write_word(exe, base + 0x518u, 0x00031080u); // sll v0,v1,2
write_word(exe, base + 0x51Cu, 0x00501021u); // addu v0,v0,s0
write_word(exe, base + 0x520u, 0x8C420000u); // lw v0,0(v0)
write_word(exe, base + 0x528u, 0x00400008u); // jr v0
const uint32_t cases[] = {
base + 0x540u, base + 0x550u, base + 0x560u};
for (size_t i = 0; i < 3u; i++) {
write_word(exe, cases[i], 0x24020001u + static_cast<uint32_t>(i));
write_word(exe, cases[i] + 4u, 0x03E00008u);
write_word(exe, base + 0xA00u + static_cast<uint32_t>(i * 4u),
cases[i]);
}
write_word(exe, base + 0x580u, 0x03E00008u);
write_word(exe, base + 0x590u,
0x08000000u | ((entry >> 2) & 0x03FFFFFFu));
PSXRecomp::Function function{};
function.start_addr = entry;
function.end_addr = base + 0x588u;
function.size = function.end_addr - function.start_addr;
function.name = "producer_switch";
function.producer_lo = base + 0x400u;
function.producer_hi = base + 0x900u; // table belongs to adjacent producer
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
const auto bounded_cfg = analyzer.analyze_function(function);
PSXRecomp::CodeGenerator bounded_generator(exe);
const std::string bounded =
bounded_generator.generate_function(function, bounded_cfg).full_code;
check(bounded.find("/* jump table") == std::string::npos,
"codegen cannot resurrect an adjacent-producer jump table");
function.producer_lo = 0u;
function.producer_hi = 0u;
const auto single_image_cfg = analyzer.analyze_function(function);
PSXRecomp::CodeGenerator single_image_generator(exe);
const std::string single_image = single_image_generator
.generate_function(function, single_image_cfg).full_code;
check(single_image.find("/* jump table") != std::string::npos,
"codegen still emits the same table for a single owned image");
// Exercise the actual overlapping-alias emitter path. The alias begins at
// a later block which jumps back through the host's complete table setup,
// so an unbounded alias really would expose the adjacent producer bytes.
// Producer ownership must survive Function -> alias CFG -> shared body.
PSXRecomp::Function alias = function;
alias.start_addr = base + 0x590u;
alias.end_addr = base + 0x598u;
alias.size = alias.end_addr - alias.start_addr;
alias.name = "producer_switch_alias";
alias.alias_walk_lo = entry;
alias.alias_group_entries = {alias.start_addr};
alias.producer_lo = base + 0x400u;
alias.producer_hi = base + 0x900u;
const auto alias_cfg = analyzer.analyze_function(alias);
check(alias_cfg.producer_lo == alias.producer_lo &&
alias_cfg.producer_hi == alias.producer_hi,
"alias CFG retains its host producer bounds");
PSXRecomp::CodeGenerator alias_generator(exe);
const auto alias_generated = alias_generator.generate_alias_group(
{&alias}, alias_cfg, "");
check(!alias_generated.empty() &&
alias_generated.front().full_code.find("/* jump table") ==
std::string::npos,
"alias emitter cannot expose an adjacent producer jump table");
alias.producer_lo = 0u;
alias.producer_hi = 0u;
const auto unbounded_alias_cfg = analyzer.analyze_function(alias);
PSXRecomp::CodeGenerator unbounded_alias_generator(exe);
const auto unbounded_alias_generated =
unbounded_alias_generator.generate_alias_group(
{&alias}, unbounded_alias_cfg, "");
check(!unbounded_alias_generated.empty() &&
unbounded_alias_generated.front().full_code.find("/* jump table") !=
std::string::npos,
"alias regression fixture reaches the table when ownership is absent");
// The scheduled variant must feed code generation too, not merely the
// discovery report. Its table base overwrites the guard in the BEQ delay
// slot, so the existing branch emitter must preserve the tested condition.
write_word(exe, base + 0x500u, 0u);
write_word(exe, base + 0x504u, 0u);
write_word(exe, base + 0x508u, 0x2C620003u);
write_word(exe, base + 0x50Cu, 0x1040001Cu);
write_word(exe, base + 0x510u, 0x3C028001u);
write_word(exe, base + 0x514u, 0x24420A00u);
write_word(exe, base + 0x518u, 0x00031880u);
write_word(exe, base + 0x51Cu, 0x00621821u);
write_word(exe, base + 0x520u, 0x8C620000u);
PSXRecomp::ControlFlowAnalyzer scheduled_analyzer(exe);
const auto scheduled_cfg = scheduled_analyzer.analyze_function(function);
PSXRecomp::CodeGenerator scheduled_generator(exe);
const auto scheduled = scheduled_generator.generate_function(
function, scheduled_cfg).full_code;
check(scheduled.find("/* jump table") != std::string::npos,
"codegen emits a switch with its table LUI in the bounds delay slot");
for (uint32_t target : cases)
check(scheduled.find(fmt::format("goto block_{:08X}", target)) != std::string::npos,
"every scheduled case has an emitted native control-flow edge");
}
void cfg_codegen_load_delay_test() {
constexpr uint32_t base = 0x80003590u;
PSXRecomp::PS1Executable exe{};
exe.header.load_address = base;
exe.header.initial_pc = base;
exe.header.file_size = 20u;
append_word(exe.code_data, 0x8F5A4C38u); // lw k0,0x4c38(k0)
append_word(exe.code_data, 0x03400825u); // move at,k0 (must see old k0)
append_word(exe.code_data, 0xAC3A4C38u); // sw k0,0x4c38(at)
append_word(exe.code_data, 0x03E00008u); // jr ra
append_word(exe.code_data, 0x00000000u); // delay-slot nop
PSXRecomp::Function function{};
function.start_addr = base;
function.end_addr = base + 20u;
function.size = 20u;
function.name = "cfg_load_delay";
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
const auto cfg = analyzer.analyze_function(function);
PSXRecomp::CodeGenerator generator(exe);
const std::string code = generator.generate_function(function, cfg).full_code;
const size_t deferred = code.find("uint32_t psx_ldd_80003590 =");
const size_t successor = code.find("cpu->gpr[1] = cpu->gpr[26]");
const size_t writeback = code.find(
"cpu->gpr[26] = psx_ldd_80003590; /* load-delay writeback */");
check(deferred != std::string::npos && successor != std::string::npos &&
writeback != std::string::npos && deferred < successor &&
successor < writeback,
"CFG codegen preserves MIPS-I dependent load-delay value semantics");
}
void mod_function_completion_codegen_test() {
PSXRecomp::CodeGenConfig config{};
config.mod_function_entry_funcs.insert(0x80010000u);
for (bool overlay : {false, true}) {
const auto code = generate_first_instruction(0x2402002Au, {}, overlay, config);
check(code.find("if (psx_mod_function_entry(cpu, 0x80010000u)) return;") != std::string::npos,
overlay ? "overlay body honors trusted mod completion" : "main body honors trusted mod completion");
}
const auto stock = generate_first_instruction(0x2402002Au, {}, false);
check(stock.find("psx_mod_function_entry(cpu,") == std::string::npos,
"unconfigured functions retain the stock entry path");
}
void mod_instruction_codegen_test(const fs::path& root) {
const auto path = write_config(root, "instruction-hooks",
"mod_instruction_sites = [\"0x80010000\", \"0x80010004\"]\n");
const auto parsed = PSXRecompV4::load_game_config(path);
check(parsed.mod_instruction_sites.size() == 2, "instruction sites parse");
auto changed = parsed;
changed.mod_instruction_sites.clear();
check(PSXRecompV4::overlay_codegen_config_hash(parsed) !=
PSXRecompV4::overlay_codegen_config_hash(changed), "instruction sites invalidate cached code");
const auto invalid = write_config(root, "instruction-unaligned",
"mod_instruction_sites = [\"0x80010001\"]\n");
check_throws([&] { PSXRecompV4::load_game_config(invalid); }, "aligned main RAM",
"instruction sites reject invalid addresses");
PSXRecomp::CodeGenConfig config{};
config.mod_instruction_sites = {0x80010000u, 0x80010004u};
for (bool overlay : {false, true}) {
for (uint32_t word : {0x2402002Au, 0x10400002u}) {
const auto code = generate_first_instruction(word, {}, overlay, config);
check(code.find(fmt::format("psx_mod_instruction(cpu, 0x80010000u, 0x{:08X}u);", word)) != std::string::npos,
"native/overlay emits instruction guard for ALU and branch");
check(code.find("psx_mod_instruction(cpu, 0x80010004u, 0x00000000u);") != std::string::npos,
"native/overlay includes sequential and branch delay-slot hooks");
}
}
check(generate_first_instruction(0x2402002Au, {}, false).find("psx_mod_instruction(cpu,") == std::string::npos,
"unconfigured code emits no instruction callbacks");
}
void local_viewport_tests(const fs::path& root) {
const std::string prefix = "\n[netplay]\nlocal_viewport=\"vertical_split\"\nlocal_viewport_renderer=\"projection\"\n";
const auto cfg = PSXRecompV4::load_game_config(write_config(root,"unsplit",prefix +
"local_viewport_width_sites=[\"0x80010000\"]\nlocal_viewport_state_addr=\"0x80018000\"\nlocal_viewport_state_values=[\"0x0\"]\n"));
check(cfg.netplay_local_viewport_width_sites == std::vector<uint32_t>{0x80010000}, "unsplit width sites parse");
check(cfg.netplay_local_viewport_renderer == "projection" && cfg.netplay_local_viewport_state_addr == 0x80018000 &&
cfg.netplay_local_viewport_state_values == std::vector<uint32_t>{0}, "unsplit renderer and pause gate parse");
auto changed = cfg; changed.netplay_local_viewport_width_sites.clear();
check(PSXRecompV4::overlay_codegen_config_hash(cfg) != PSXRecompV4::overlay_codegen_config_hash(changed), "unsplit sites invalidate cached code");
for (auto bad : {"0x80010001", "0x1F800000", "0x80800000"}) {
auto path = write_config(root,"unsplit-bad",prefix+"local_viewport_width_sites=[\""+bad+"\"]\n");
check_throws([&]{PSXRecompV4::load_game_config(path);}, "aligned main RAM", "unsplit rejects invalid site");
}
auto dup = write_config(root,"unsplit-duplicate",prefix+"local_viewport_width_sites=[\"0x80010000\",\"0xA0010000\"]\n");
check_throws([&]{PSXRecompV4::load_game_config(dup);},"duplicate", "unsplit rejects aliased duplicates");
auto no_projection = write_config(root,"unsplit-renderer","\n[netplay]\nlocal_viewport_width_sites=[\"0x80010000\"]\n");
check_throws([&]{PSXRecompV4::load_game_config(no_projection);},"requires projection", "unsplit width requires projection");
PSXRecomp::CodeGenConfig codegen{};
codegen.netplay_local_viewport_width_sites.insert(0x80010000);
for (bool overlay : {false,true}) {
auto code=generate_first_instruction(0x8F820EDCu,{},overlay,codegen);
check(code.find("psx_ws_local_viewport_width((int32_t)psx_cyc_load_word") != std::string::npos, "unsplit keeps LW cycle accounting");
}
check(generate_first_instruction(0x8F820EDCu,{},false).find("= (uint32_t)psx_ws_local_viewport_width") == std::string::npos, "ordinary loads remain native");
}
void cfg_fallthrough_reachability_test() {
constexpr uint32_t base = 0x80010000u;
PSXRecomp::PS1Executable exe{};
exe.header.load_address = exe.header.initial_pc = base;
exe.header.file_size = 24;
append_word(exe.code_data, 0x03e00008u); // entry returns
append_word(exe.code_data, 0);
append_word(exe.code_data, 0x08004004u); // unreachable jump to final block
append_word(exe.code_data, 0);
append_word(exe.code_data, 0x24020001u);
append_word(exe.code_data, 0);
PSXRecomp::Function f{};
f.start_addr = base; f.end_addr = base + 24; f.size = 24;
auto check_tail = [&](bool reachable, const char* name) {
const auto cfg = PSXRecomp::ControlFlowAnalyzer(exe).analyze_function(f);
check(!cfg.blocks.at(base + 16).predecessors.empty(),
std::string(name) + " retains the real incoming edge");
PSXRecomp::CodeGenerator generator(exe);
const auto named = generator.generate_function(f, cfg, "func_80010018").full_code;
const auto edge = generator.generate_function(f, cfg).full_code;
check((named.find("/* fallthrough to next function */") != std::string::npos) == reachable,
std::string(name) + " named final safety net uses entry reachability");
check((edge.find("/* image-edge fallthrough: tail-transfer */") != std::string::npos) == reachable,
std::string(name) + " image-edge final safety net uses entry reachability");
};
check_tail(false, "unreachable predecessor");
write_word(exe, base, 0x08004002u); // entry now reaches +8 -> +16
check_tail(true, "reachable predecessor");
write_word(exe, base, 0x03e00008u);
f.alias_walk_lo = base; f.start_addr = base + 16;
f.alias_group_entries = {base + 16};
check_tail(true, "independent alias entry");
}
} // namespace
int main() {
const fs::path root = fs::temp_directory_path() /
fmt::format("psxrecomp-patch-test-{}", reinterpret_cast<uintptr_t>(&failures));
fs::remove_all(root);
fs::create_directories(root);
try {
parser_tests(root);
capture_history_config_tests(root);
codegen_tests();
gte_codegen_classification_tests();
jump_table_producer_codegen_test();
cfg_codegen_load_delay_test();
mod_function_completion_codegen_test();
mod_instruction_codegen_test(root);
local_viewport_tests(root);
cfg_fallthrough_reachability_test();
} catch (const std::exception& e) {
fmt::print(stderr, "FAIL unexpected exception: {}\n", e.what());
++failures;
}
std::error_code ec;
fs::remove_all(root, ec);
fmt::print("\nGuarded recompiler patches: {}\n",
failures == 0 ? "all tests passed" : "failures detected");
return failures == 0 ? 0 : 1;
}