Files
psxrecomp/recompiler/tests/draw_distance_codegen_test.cpp
T
Shokunin dd5fdec6bb Add opt-in draw-distance clamps ([[draw_distance.clamp]])
Many PS1 renderers drop a primitive whose depth index falls past the end
of their ordering table. Raising that limit can write past the table, so
a title can now list such guards in game.toml with the register that
carries the index and the last safe value. While a trusted mod has called
psx_mod_set_draw_distance_clamp(1), generated main-EXE code clamps that
register before the guard runs, so far geometry is kept in the farthest
slot instead of popping in later.

- Off by default and reset at every session start, so netplay and every
  title that lists no sites run unchanged; with no sites the generated
  code and declarations are byte-identical.
- Main executable only: the full instruction word is the guard,
  generation fails on a mismatch or a load-delay hazard, captured
  overlay code keeps its own code (the sites are not part of the overlay
  config identity), and the dirty-RAM interpreter applies the clamp
  where the game's text image holds the listed word.
- Compatibility: code_generator.cpp is a codegen-hash input, so the
  codegen hash and the overlay-cache tag change for every title, sites
  or not. Savestates from earlier builds are refused, and overlay shards
  rebuild once on first visit. Generated code and behaviour are
  unchanged for a title without sites or with the switch off.
- Tests: draw_distance_codegen_test (config rules, emission, guards,
  declarations, overlay identity) and draw_distance_interp_test (store,
  switch, interpreted guards); the session-reset guard covers the switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:36:48 -07:00

377 lines
15 KiB
C++

// [[draw_distance.clamp]]: config parsing and validation, the emitted
// main-EXE clamp (and its absence everywhere else), the main-EXE guards, the
// shared declaration, and the overlay-cache identity (unchanged: overlays
// keep their own code at a listed address).
#include "code_generator.h"
#include "config_loader.h"
#include "control_flow.h"
#include "../../runtime/include/draw_distance.h"
#include <chrono>
#include <cstdlib>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <filesystem>
#include <fstream>
#include <initializer_list>
#include <string>
#include <vector>
#if defined(_WIN32)
#include <cstdlib>
#include <process.h>
#else
#include <sys/wait.h>
#include <unistd.h>
#endif
namespace fs = std::filesystem;
namespace {
constexpr uint32_t kBase = 0x80010000u;
int failures = 0;
void check(bool condition, const char* message) {
if (!condition) {
std::fprintf(stderr, "FAIL: %s\n", message);
++failures;
}
}
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));
}
fs::path write_temp_config(const char* stem, const std::string& body) {
const auto nonce = std::chrono::high_resolution_clock::now()
.time_since_epoch().count();
fs::path path = fs::temp_directory_path() /
(std::string(stem) + "-" + std::to_string(nonce) + ".toml");
std::ofstream out(path, std::ios::binary | std::ios::trunc);
out << body;
return path;
}
std::string base_config() {
return R"toml([game]
name = "Draw Distance 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";
}
std::string clamp(const char* address, const char* expected, int reg,
const char* max) {
return std::string("\n[[draw_distance.clamp]]\naddress = \"") + address +
"\"\nexpected = \"" + expected + "\"\nreg = " + std::to_string(reg) +
"\nmax = " + max + "\n";
}
PSXRecompV4::GameConfig load(const std::string& body) {
fs::path path = write_temp_config("draw-distance", body);
auto config = PSXRecompV4::load_game_config(path);
fs::remove(path);
return config;
}
bool load_throws_with(const std::string& body, const char* needle) {
fs::path path = write_temp_config("draw-distance-bad", body);
bool matched = false;
try {
(void)PSXRecompV4::load_game_config(path);
} catch (const std::exception& e) {
matched = std::string(e.what()).find(needle) != std::string::npos;
if (!matched) std::fprintf(stderr, " (threw: %s)\n", e.what());
}
fs::remove(path);
return matched;
}
// A function made of `words` followed by `jr ra ; nop`.
PSXRecomp::GeneratedFunction generate(std::initializer_list<uint32_t> words,
const PSXRecomp::CodeGenConfig& config) {
PSXRecomp::PS1Executable exe{};
exe.header.load_address = kBase;
exe.header.initial_pc = kBase;
for (uint32_t w : words) append_word(exe.code_data, w);
append_word(exe.code_data, 0x03E00008u);
append_word(exe.code_data, 0x00000000u);
const uint32_t size = static_cast<uint32_t>(exe.code_data.size());
exe.header.file_size = size;
PSXRecomp::Function function{};
function.start_addr = kBase;
function.end_addr = kBase + size;
function.size = size;
function.name = "clamp_site_test";
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
const auto cfg = analyzer.analyze_function(function);
PSXRecomp::CodeGenerator generator(exe, config);
return generator.generate_function(function, cfg);
}
// Runs `body` in a child and reports whether it exited with a failure status
// (the main-EXE guards call std::exit(1)). Windows has no fork: the Nth call
// re-runs this binary as `--exit-probe N`, which runs only the Nth body.
const char* g_self = nullptr;
int g_exit_probe = -1;
int g_probe_seq = 0;
template <class F>
bool exits_with_failure(F&& body) {
#if defined(_WIN32)
const int seq = g_probe_seq++;
if (g_exit_probe >= 0) {
if (seq != g_exit_probe) return false;
body();
std::fflush(nullptr);
std::_Exit(0);
}
std::fflush(nullptr);
const std::string quoted = std::string("\"") + g_self + "\"";
const std::string n = std::to_string(seq);
const intptr_t rc = _spawnl(_P_WAIT, g_self, quoted.c_str(), "--exit-probe",
n.c_str(), nullptr);
return rc > 0;
#else
(void)g_probe_seq;
std::fflush(nullptr);
const pid_t pid = fork();
if (pid == 0) {
if (!std::freopen("/dev/null", "w", stderr)) _exit(3);
body();
_exit(0);
}
int status = 0;
if (pid < 0 || waitpid(pid, &status, 0) != pid) return false;
return WIFEXITED(status) && WEXITSTATUS(status) != 0;
#endif
}
constexpr uint32_t kSltiuT2V0 = 0x2C4A01C0u; // sltiu t2, v0, 0x1C0
constexpr uint32_t kAddiuAtV0 = 0x2441FFFFu; // addiu at, v0, -1
constexpr uint32_t kSravV0 = 0x00561007u; // srav v0, s6, v0
constexpr uint32_t kLwV0 = 0x8C820060u; // lw v0, 0x60(a0)
constexpr uint32_t kLwV1 = 0x8C830060u; // lw v1, 0x60(a0)
constexpr uint32_t kBeqZero = 0x10000002u; // b +2 (beq zero, zero)
constexpr uint32_t kNop = 0x00000000u;
const char* kClampV0 =
"if (g_psx_draw_distance_clamp && (int32_t)cpu->gpr[2] > 447) "
"cpu->gpr[2] = (uint32_t)(447);";
PSXRecomp::CodeGenConfig with_site(uint32_t address, uint32_t expected,
uint32_t reg, int32_t max) {
PSXRecomp::CodeGenConfig config{};
PSXRecompV4::DrawDistanceClampSite site;
site.address = address;
site.expected = expected;
site.reg = reg;
site.max = max;
config.draw_distance_clamp_sites.push_back(site);
return config;
}
void reads_classifier() {
using PSXRecompV4::draw_distance_clamp_reads;
check(draw_distance_clamp_reads(kSltiuT2V0, 2), "sltiu reads rs");
check(!draw_distance_clamp_reads(kSltiuT2V0, 10), "sltiu does not read rt");
check(draw_distance_clamp_reads(kAddiuAtV0, 2), "addiu reads rs");
check(draw_distance_clamp_reads(kSravV0, 2) &&
draw_distance_clamp_reads(kSravV0, 22),
"srav reads rs and rt");
check(draw_distance_clamp_reads(0x00021080u, 2), "sll reads rt"); // sll v0,v0,2
check(draw_distance_clamp_reads(0x0062102Bu, 3), "sltu reads rs"); // sltu v0,v1,v0
check(!draw_distance_clamp_reads(0x3C02800Bu, 2), "lui reads nothing");
check(!draw_distance_clamp_reads(kLwV0, 4), "a load is not a clamp site");
check(!draw_distance_clamp_reads(0x10400002u, 2), "a branch is not a clamp site");
check(!draw_distance_clamp_reads(0x00430018u, 2), "mult is not a clamp site");
check(!draw_distance_clamp_reads(0x03E00008u, 31), "jr is not a clamp site");
check(!draw_distance_clamp_reads(kSltiuT2V0, 0), "reg 0 is refused");
}
void loader() {
auto none = load(base_config());
check(none.draw_distance_clamp_sites.empty(), "no clamps by default");
auto two = load(base_config() +
clamp("0x80061230", "0x2C4A01C0", 2, "0x1BF") +
clamp("0x80066FF8", "0x2441FFFF", 2, "447"));
check(two.draw_distance_clamp_sites.size() == 2, "two clamps parsed");
if (two.draw_distance_clamp_sites.size() == 2) {
const auto& a = two.draw_distance_clamp_sites[0];
const auto& b = two.draw_distance_clamp_sites[1];
check(a.address == 0x80061230u && a.expected == kSltiuT2V0 &&
a.reg == 2u && a.max == 0x1BF,
"first clamp fields");
check(b.address == 0x80066FF8u && b.expected == kAddiuAtV0 &&
b.max == 447,
"second clamp fields (decimal max)");
}
auto neg = load(base_config() + clamp("0x80061230", "0x2C4A01C0", 2, "-5"));
check(neg.draw_distance_clamp_sites.size() == 1 &&
neg.draw_distance_clamp_sites[0].max == -5,
"a negative max is accepted");
check(load_throws_with(base_config() + clamp("0x80061232", "0x2C4A01C0", 2, "1"),
"not instruction-aligned"),
"unaligned address refused");
check(load_throws_with(base_config() +
clamp("0x80061230", "0x2C4A01C0", 2, "1") +
clamp("0xA0061230", "0x2C4A01C0", 2, "1"),
"duplicate draw-distance clamp"),
"duplicate physical address refused");
check(load_throws_with(base_config() + clamp("0x80061230", "0x2C4A01C0", 0, "1"),
"reg must be 1..31"),
"reg 0 refused");
check(load_throws_with(base_config() + clamp("0x80061230", "0x2C4A01C0", 32, "1"),
"reg must be 1..31"),
"reg 32 refused");
check(load_throws_with(base_config() + clamp("0x80061230", "0x2C4A01C0", 3, "1"),
"must be an ALU instruction that reads reg 3"),
"a reg the instruction does not read is refused");
check(load_throws_with(base_config() + clamp("0x80061230", "0x8C820060", 4, "1"),
"must be an ALU instruction"),
"a load site is refused");
check(load_throws_with(base_config() +
clamp("0x80061230", "0x2C4A01C0", 2, "0x100000000"),
"not a signed 32-bit value"),
"max outside int32 refused");
}
void emission() {
// The clamp runs immediately before the guard it protects.
const auto cfg = with_site(kBase + 4u, kSltiuT2V0, 2, 447);
const auto out = generate({kSravV0, kSltiuT2V0, kNop}, cfg).full_code;
const size_t at = out.find(kClampV0);
const size_t guard = out.find("0x80010004:");
check(at != std::string::npos, "main-EXE site emits the clamp");
check(at != std::string::npos && guard != std::string::npos && at < guard,
"the clamp precedes the guard instruction");
check(at != std::string::npos &&
out.find(kClampV0, at + 1) == std::string::npos,
"exactly one clamp emitted");
check(at != std::string::npos && at >= 5 &&
out.compare(at - 5, 5, "\n ") == 0 &&
out.find("\n cpu->gpr[10] =", at) != std::string::npos,
"the clamp and the guard share the statement indent");
// The addiu form clamps the source the slot index is computed from.
const auto addiu = generate({kSravV0, kAddiuAtV0, kNop},
with_site(kBase + 4u, kAddiuAtV0, 2, 447)).full_code;
check(addiu.find(kClampV0) != std::string::npos,
"addiu site emits the clamp on its source");
// A site in a branch delay slot is clamped inside the slot.
const auto ds = generate({kBeqZero, kSltiuT2V0, kNop, kNop},
with_site(kBase + 4u, kSltiuT2V0, 2, 447)).full_code;
check(ds.find(kClampV0) != std::string::npos, "delay-slot site is clamped");
// No sites: nothing about the clamp anywhere.
PSXRecomp::CodeGenConfig plain{};
const auto vanilla = generate({kSravV0, kSltiuT2V0, kNop}, plain).full_code;
check(vanilla.find("draw_distance") == std::string::npos &&
vanilla.find("draw-distance") == std::string::npos,
"no sites: vanilla code");
// A site elsewhere leaves this instruction alone.
const auto other = generate({kSravV0, kSltiuT2V0, kNop},
with_site(kBase + 0x100u, kSltiuT2V0, 2, 447)).full_code;
check(other.find("draw-distance") == std::string::npos,
"unlisted address: vanilla code");
// Captured overlays keep their own code at a listed address, matching
// word or not.
auto overlay = with_site(kBase + 4u, kSltiuT2V0, 2, 447);
overlay.overlay_mode = true;
check(generate({kSravV0, kSltiuT2V0, kNop}, overlay).full_code.find(
"draw-distance") == std::string::npos,
"overlay variant with the listed word stays vanilla");
check(!exits_with_failure([&] { generate({kSravV0, kAddiuAtV0, kNop}, overlay); }),
"overlay variant with another word generates");
}
void guards() {
const auto cfg = with_site(kBase + 4u, kSltiuT2V0, 2, 447);
check(!exits_with_failure([&] { generate({kSravV0, kSltiuT2V0, kNop}, cfg); }),
"matching word generates");
check(exits_with_failure([&] { generate({kSravV0, kAddiuAtV0, kNop}, cfg); }),
"a main-EXE word mismatch fails generation");
check(exits_with_failure([&] { generate({kLwV0, kSltiuT2V0, kNop}, cfg); }),
"a site in the load-delay slot of its register fails generation");
check(!exits_with_failure([&] { generate({kLwV1, kSltiuT2V0, kNop}, cfg); }),
"a load into another register before the site is fine");
}
void shared_decls() {
PSXRecomp::PS1Executable exe{};
exe.header.load_address = kBase;
std::vector<PSXRecomp::GeneratedFunction> functions;
{
PSXRecomp::CodeGenerator plain(exe);
check(plain.build_shared_decls_header(functions).find(
"g_psx_draw_distance_clamp") == std::string::npos,
"no sites: the declarations do not change");
}
{
const auto cfg = with_site(kBase + 4u, kSltiuT2V0, 2, 447);
PSXRecomp::CodeGenerator gen(exe, cfg);
check(gen.build_shared_decls_header(functions).find(
"extern uint32_t g_psx_draw_distance_clamp;") != std::string::npos,
"sites: the switch is declared");
}
}
void overlay_identity_unchanged() {
auto none = load(base_config());
auto some = load(base_config() + clamp("0x80061230", "0x2C4A01C0", 2, "0x1BF"));
check(PSXRecompV4::overlay_codegen_config_hash(none) ==
PSXRecompV4::overlay_codegen_config_hash(some),
"clamps do not enter the overlay-cache identity");
}
void runtime_value() {
check(psx_draw_distance_clamp_value(0x1C0u, 0x1BF) == 0x1BFu, "clamps past max");
check(psx_draw_distance_clamp_value(0x7FFu, 0x1BF) == 0x1BFu, "clamps far past max");
check(psx_draw_distance_clamp_value(0x1BFu, 0x1BF) == 0x1BFu, "max is kept");
check(psx_draw_distance_clamp_value(5u, 0x1BF) == 5u, "near values unchanged");
check(psx_draw_distance_clamp_value(0u, 0x1BF) == 0u, "zero unchanged");
check(psx_draw_distance_clamp_value(0xFFFFFFFFu, 0x1BE) == 0xFFFFFFFFu,
"a negative value is left for the original predicate");
}
} // namespace
int main(int argc, char** argv) {
g_self = argv[0];
if (argc >= 3 && std::string(argv[1]) == "--exit-probe")
g_exit_probe = std::atoi(argv[2]);
reads_classifier();
loader();
emission();
guards();
shared_decls();
overlay_identity_unchanged();
runtime_value();
if (failures) {
std::fprintf(stderr, "draw_distance_codegen_test: %d failure(s)\n", failures);
return 1;
}
std::printf("draw_distance_codegen_test: OK\n");
return 0;
}