mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
42ddce61b5
(cherry picked from commit 6b6402e7f0)
772 lines
35 KiB
C++
772 lines
35 KiB
C++
// [widescreen.cull] bgez_sites and clip_edge_x_load_sites: config parsing,
|
|
// overlay-cache identity, emitted native code, main-EXE opcode guards (also
|
|
// for bltz_sites and branch_keep_sites), and the shared runtime math
|
|
// (ws_cull_edge.h) the emitted helpers call.
|
|
#include "code_generator.h"
|
|
#include "config_loader.h"
|
|
#include "control_flow.h"
|
|
#include "../../runtime/include/ws_cull_edge.h"
|
|
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <exception>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#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 = "Cull Edge 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"
|
|
|
|
[widescreen.cull]
|
|
guard_pixels = 0
|
|
slti_sites = ["0x80010020"]
|
|
bltz_sites = ["0x80010018"]
|
|
branch_keep_sites = ["0x80010040"]
|
|
)toml";
|
|
}
|
|
|
|
PSXRecompV4::GameConfig load(const std::string& body) {
|
|
fs::path path = write_temp_config("ws-cull-edge", 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("ws-cull-edge-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;
|
|
}
|
|
fs::remove(path);
|
|
return matched;
|
|
}
|
|
|
|
// A tiny function: <word> ; nop ; addiu v1,zero,1 ; jr ra ; nop. A branch in
|
|
// the first slot targets the jr (kBase+12).
|
|
PSXRecomp::GeneratedFunction generate_first_instruction(
|
|
uint32_t first_word, const PSXRecomp::CodeGenConfig& config) {
|
|
PSXRecomp::PS1Executable exe{};
|
|
exe.header.load_address = kBase;
|
|
exe.header.initial_pc = kBase;
|
|
exe.header.file_size = 20;
|
|
append_word(exe.code_data, first_word);
|
|
append_word(exe.code_data, 0x00000000u);
|
|
append_word(exe.code_data, 0x24030001u);
|
|
append_word(exe.code_data, 0x03E00008u);
|
|
append_word(exe.code_data, 0x00000000u);
|
|
|
|
PSXRecomp::Function function{};
|
|
function.start_addr = kBase;
|
|
function.end_addr = kBase + 20u;
|
|
function.size = 20u;
|
|
function.name = "ws_cull_edge_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), which a plain call cannot observe.
|
|
// Windows has no fork: the Nth call re-runs this binary as `--exit-probe N`,
|
|
// which repeats the tests but runs only the Nth body (every other call returns
|
|
// false unrun) and exits 0 when that body returns.
|
|
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 kBgezV0 = 0x04410002u; // bgez v0, +2
|
|
constexpr uint32_t kBltzV0 = 0x04400002u; // bltz v0, +2
|
|
constexpr uint32_t kLhV0 = 0x84820070u; // lh v0, 0x70(a0)
|
|
constexpr uint32_t kLhuV0 = 0x94820070u; // lhu v0, 0x70(a0)
|
|
constexpr uint32_t kLwV0 = 0x8C820070u; // lw v0, 0x70(a0)
|
|
constexpr uint32_t kAddiuV0 = 0x24820070u; // addiu v0, a0, 0x70
|
|
constexpr uint32_t kLhZero = 0x84800070u; // lh zero, 0x70(a0)
|
|
constexpr uint32_t kBltzalV0 = 0x04500002u; // bltzal v0, +2
|
|
constexpr uint32_t kBeqV0 = 0x10400002u; // beq v0, zero, +2
|
|
constexpr uint32_t kJumpJr = 0x08004003u; // j kBase+12 (the jr)
|
|
|
|
void loader_parses_new_kinds() {
|
|
auto config = load(base_config() + R"toml(bgez_sites = ["0x80010010", "0x80010014"]
|
|
clip_edge_x_load_sites = ["0x80010030"]
|
|
clip_edge_width = 368
|
|
)toml");
|
|
check(config.ws_cull_bgez_sites ==
|
|
std::vector<uint32_t>({0x80010010u, 0x80010014u}),
|
|
"loader reads bgez_sites");
|
|
check(config.ws_cull_clip_edge_x_load_sites ==
|
|
std::vector<uint32_t>({0x80010030u}),
|
|
"loader reads clip_edge_x_load_sites");
|
|
check(PSXRecompV4::ws_cull_clip_edge_width(config) == 368u,
|
|
"explicit clip_edge_width wins");
|
|
|
|
auto defaults = load(base_config());
|
|
check(defaults.ws_cull_bgez_sites.empty() &&
|
|
defaults.ws_cull_clip_edge_x_load_sites.empty(),
|
|
"new kinds default to empty");
|
|
check(PSXRecompV4::ws_cull_clip_edge_width(defaults) == 0x140u,
|
|
"clip_edge_width defaults to screen_w_imms[0]");
|
|
|
|
auto ape = load(base_config() + "screen_w_imms = [\"0x181\"]\n");
|
|
check(PSXRecompV4::ws_cull_clip_edge_width(ape) == 0x181u,
|
|
"clip_edge_width follows a per-game screen_w_imms");
|
|
|
|
check(load_throws_with(base_config() + "clip_edge_width = 0\n",
|
|
"clip_edge_width must be 1..1024"),
|
|
"loader rejects a zero clip_edge_width");
|
|
}
|
|
|
|
void hash_identity_only_changes_when_used() {
|
|
const std::string body = base_config();
|
|
const auto plain = load(body);
|
|
const auto explicit_empty = load(body + R"toml(bgez_sites = []
|
|
clip_edge_x_load_sites = []
|
|
clip_edge_width = 320
|
|
)toml");
|
|
const uint32_t plain_hash = PSXRecompV4::overlay_codegen_config_hash(plain);
|
|
check(plain_hash == PSXRecompV4::overlay_codegen_config_hash(explicit_empty),
|
|
"empty new lists (and an unused width) leave the overlay hash unchanged");
|
|
|
|
// Pinned against the hash layout before these kinds existed, computed
|
|
// from this exact config with the previous config_loader.cpp. A change
|
|
// here invalidates every title's overlay cache; do it deliberately.
|
|
check(plain_hash == 0xE3B85230u,
|
|
"hash of a config without the new kinds matches the pre-change layout");
|
|
|
|
const auto with_bgez = load(body + "bgez_sites = [\"0x80010010\"]\n");
|
|
check(plain_hash != PSXRecompV4::overlay_codegen_config_hash(with_bgez),
|
|
"bgez_sites enter the overlay hash when non-empty");
|
|
const auto with_clip = load(body + "clip_edge_x_load_sites = [\"0x80010030\"]\n");
|
|
const uint32_t clip_hash = PSXRecompV4::overlay_codegen_config_hash(with_clip);
|
|
check(plain_hash != clip_hash,
|
|
"clip_edge_x_load_sites enter the overlay hash when non-empty");
|
|
const auto with_clip_w = load(body + "clip_edge_x_load_sites = [\"0x80010030\"]\n"
|
|
"clip_edge_width = 368\n");
|
|
check(clip_hash != PSXRecompV4::overlay_codegen_config_hash(with_clip_w),
|
|
"clip_edge_width enters the hash with its sites");
|
|
}
|
|
|
|
void codegen_emits_bgez() {
|
|
PSXRecomp::CodeGenConfig config{};
|
|
config.emit_comments = true;
|
|
const auto vanilla = generate_first_instruction(kBgezV0, config).full_code;
|
|
check(vanilla.find("(int32_t)cpu->gpr[2] >= 0") != std::string::npos,
|
|
"unlisted bgez stays vanilla");
|
|
check(vanilla.find("psx_ws_cull_bgez") == std::string::npos,
|
|
"unlisted bgez does not call the helper");
|
|
|
|
config.ws_cull_bgez_sites.insert(kBase);
|
|
const auto widened = generate_first_instruction(kBgezV0, config).full_code;
|
|
check(widened.find("psx_ws_cull_bgez(cpu->gpr[2]) /* ws cull (left keep) */") !=
|
|
std::string::npos,
|
|
"listed bgez routes through psx_ws_cull_bgez");
|
|
}
|
|
|
|
void codegen_emits_clip_edge_loads() {
|
|
PSXRecomp::CodeGenConfig config{};
|
|
config.emit_comments = true;
|
|
config.ws_cull_clip_edge_x_load_sites.insert(kBase);
|
|
|
|
const auto lh = generate_first_instruction(kLhV0, config).full_code;
|
|
check(lh.find("cpu->gpr[2] = psx_ws_clip_edge_x((uint32_t)(int32_t)(int16_t)"
|
|
"psx_cyc_load_half(cpu, cpu->gpr[4] + 112, 2, 0x10u), 320u);") !=
|
|
std::string::npos,
|
|
"lh clip bound is sign-extended then widened against 320");
|
|
|
|
const auto lhu = generate_first_instruction(kLhuV0, config).full_code;
|
|
check(lhu.find("cpu->gpr[2] = psx_ws_clip_edge_x((uint32_t)"
|
|
"psx_cyc_load_half(cpu, cpu->gpr[4] + 112, 2, 0x10u), 320u);") !=
|
|
std::string::npos,
|
|
"lhu clip bound is zero-extended then widened");
|
|
|
|
config.ws_cull_clip_edge_width = 368;
|
|
const auto lw = generate_first_instruction(kLwV0, config).full_code;
|
|
check(lw.find("cpu->gpr[2] = psx_ws_clip_edge_x("
|
|
"psx_cyc_load_word(cpu, cpu->gpr[4] + 112, 2, 0x10u), 368u);") !=
|
|
std::string::npos,
|
|
"lw clip bound uses the configured width");
|
|
|
|
PSXRecomp::CodeGenConfig plain{};
|
|
const auto vanilla = generate_first_instruction(kLhV0, plain).full_code;
|
|
check(vanilla.find("psx_ws_clip_edge_x") == std::string::npos,
|
|
"unlisted lh stays vanilla");
|
|
}
|
|
|
|
void main_exe_guards_fail_the_build() {
|
|
PSXRecomp::CodeGenConfig bgez{};
|
|
bgez.ws_cull_bgez_sites.insert(kBase);
|
|
check(exits_with_failure([&] { generate_first_instruction(kBltzV0, bgez); }),
|
|
"a bgez site holding bltz fails main-EXE generation");
|
|
check(exits_with_failure([&] { generate_first_instruction(kAddiuV0, bgez); }),
|
|
"a bgez site holding a non-branch fails main-EXE generation");
|
|
|
|
PSXRecomp::CodeGenConfig clip{};
|
|
clip.ws_cull_clip_edge_x_load_sites.insert(kBase);
|
|
check(exits_with_failure([&] { generate_first_instruction(kAddiuV0, clip); }),
|
|
"a clip-edge site holding a non-load fails main-EXE generation");
|
|
check(exits_with_failure([&] { generate_first_instruction(kLhZero, clip); }),
|
|
"a clip-edge load into $zero fails main-EXE generation");
|
|
|
|
// Captured overlays may hold unrelated code at a listed address: keep it.
|
|
bgez.overlay_mode = true;
|
|
const auto overlay_bltz = generate_first_instruction(kBltzV0, bgez).full_code;
|
|
check(overlay_bltz.find("(int32_t)cpu->gpr[2] < 0") != std::string::npos &&
|
|
overlay_bltz.find("psx_ws_cull_bgez") == std::string::npos,
|
|
"overlay variant at a bgez site stays vanilla");
|
|
clip.overlay_mode = true;
|
|
const auto overlay_addiu = generate_first_instruction(kAddiuV0, clip).full_code;
|
|
check(overlay_addiu.find("psx_ws_clip_edge_x") == std::string::npos,
|
|
"overlay variant at a clip-edge site stays vanilla");
|
|
}
|
|
|
|
// bltz_sites and branch_keep_sites get the same main-EXE guard as bgez_sites
|
|
// (docs/WIDESCREEN.md "Explicit screen-X cull sites"); jumps, which never
|
|
// reach generate_branch_condition, are checked at the block exit.
|
|
void branch_site_guards() {
|
|
PSXRecomp::CodeGenConfig bltz{};
|
|
bltz.ws_cull_bltz_sites.insert(kBase);
|
|
check(generate_first_instruction(kBltzV0, bltz).full_code.find(
|
|
"psx_ws_cull_bltz(cpu->gpr[2])") != std::string::npos,
|
|
"a bltz site on bltz emits the helper");
|
|
check(!exits_with_failure([&] { generate_first_instruction(kBltzV0, bltz); }),
|
|
"a bltz site on bltz generates");
|
|
check(exits_with_failure([&] { generate_first_instruction(kBgezV0, bltz); }),
|
|
"a bltz site holding bgez fails main-EXE generation");
|
|
check(exits_with_failure([&] { generate_first_instruction(kBltzalV0, bltz); }),
|
|
"a bltz site holding bltzal fails main-EXE generation");
|
|
check(exits_with_failure([&] { generate_first_instruction(kAddiuV0, bltz); }),
|
|
"a bltz site holding a non-branch fails main-EXE generation");
|
|
check(exits_with_failure([&] { generate_first_instruction(kJumpJr, bltz); }),
|
|
"a bltz site holding a jump fails main-EXE generation");
|
|
|
|
PSXRecomp::CodeGenConfig bgez{};
|
|
bgez.ws_cull_bgez_sites.insert(kBase);
|
|
check(exits_with_failure([&] { generate_first_instruction(kJumpJr, bgez); }),
|
|
"a bgez site holding a jump fails main-EXE generation");
|
|
|
|
PSXRecomp::CodeGenConfig keep{};
|
|
keep.ws_cull_branch_keep_sites.insert(kBase);
|
|
check(generate_first_instruction(kBeqV0, keep).full_code.find(
|
|
"ws branch keep") != std::string::npos,
|
|
"a branch-keep site on beq keeps the branch while wide");
|
|
check(!exits_with_failure([&] { generate_first_instruction(kBeqV0, keep); }) &&
|
|
!exits_with_failure([&] { generate_first_instruction(kBgezV0, keep); }) &&
|
|
!exits_with_failure([&] { generate_first_instruction(kBltzalV0, keep); }),
|
|
"a branch-keep site on a conditional branch generates");
|
|
check(exits_with_failure([&] { generate_first_instruction(kAddiuV0, keep); }),
|
|
"a branch-keep site holding a non-branch fails main-EXE generation");
|
|
check(exits_with_failure([&] { generate_first_instruction(kJumpJr, keep); }),
|
|
"a branch-keep site holding a jump fails main-EXE generation");
|
|
|
|
// Captured overlays may hold unrelated code at a listed address: keep it.
|
|
bltz.overlay_mode = true;
|
|
keep.overlay_mode = true;
|
|
check(!exits_with_failure([&] { generate_first_instruction(kAddiuV0, bltz); }) &&
|
|
!exits_with_failure([&] { generate_first_instruction(kJumpJr, bltz); }) &&
|
|
!exits_with_failure([&] { generate_first_instruction(kAddiuV0, keep); }),
|
|
"overlay variants at bltz/branch-keep sites generate vanilla code");
|
|
check(generate_first_instruction(kBgezV0, bltz).full_code.find(
|
|
"psx_ws_cull_bltz") == std::string::npos,
|
|
"overlay variant at a bltz site stays vanilla");
|
|
}
|
|
|
|
// In overlay code a listed address whose instruction matches is widened like
|
|
// the main EXE (the dirty-RAM interpreter does the same), so the shard must
|
|
// link both helpers: overlay_dispatch_preamble.c.inc defines them
|
|
// (overlay_shim_compile_contract, overlay_widescreen_callbacks).
|
|
void overlay_matching_sites_emit_helpers() {
|
|
PSXRecomp::CodeGenConfig bgez{};
|
|
bgez.ws_cull_bgez_sites.insert(kBase);
|
|
bgez.overlay_mode = true;
|
|
check(generate_first_instruction(kBgezV0, bgez).full_code.find(
|
|
"psx_ws_cull_bgez(cpu->gpr[2])") != std::string::npos,
|
|
"overlay bgez at a bgez site emits psx_ws_cull_bgez");
|
|
PSXRecomp::CodeGenConfig clip{};
|
|
clip.ws_cull_clip_edge_x_load_sites.insert(kBase);
|
|
clip.overlay_mode = true;
|
|
check(generate_first_instruction(kLhV0, clip).full_code.find(
|
|
"psx_ws_clip_edge_x(") != std::string::npos,
|
|
"overlay lh at a clip-edge site emits psx_ws_clip_edge_x");
|
|
}
|
|
|
|
void shared_decls_include_new_helpers() {
|
|
PSXRecomp::PS1Executable exe{};
|
|
exe.header.load_address = kBase;
|
|
PSXRecomp::CodeGenerator generator(exe);
|
|
std::vector<PSXRecomp::GeneratedFunction> functions;
|
|
const auto decls = generator.build_shared_decls_header(functions);
|
|
check(decls.find("psx_ws_cull_bgez(uint32_t v)") != std::string::npos,
|
|
"shared declarations include psx_ws_cull_bgez");
|
|
check(decls.find("psx_ws_clip_edge_x(uint32_t v, uint32_t w)") != std::string::npos,
|
|
"shared declarations include psx_ws_clip_edge_x");
|
|
}
|
|
|
|
void masked_reject_sites() {
|
|
const std::string plain = base_config();
|
|
const std::string body = plain + R"toml(
|
|
[[widescreen.cull.masked_reject]]
|
|
address = "0x80010000"
|
|
expected = "0x14800002"
|
|
reject_mask = "0xFFFF0000"
|
|
)toml";
|
|
const auto loaded = load(body);
|
|
check(loaded.ws_cull_masked_reject_sites.size() == 1, "load guarded masked reject");
|
|
check(PSXRecompV4::overlay_codegen_config_hash(load(plain)) !=
|
|
PSXRecompV4::overlay_codegen_config_hash(loaded), "masked reject affects cache identity");
|
|
auto changed = loaded;
|
|
changed.ws_cull_masked_reject_sites[0].reject_mask = 0xFF000000u;
|
|
check(PSXRecompV4::overlay_codegen_config_hash(changed) !=
|
|
PSXRecompV4::overlay_codegen_config_hash(loaded), "reject mask affects cache identity");
|
|
PSXRecomp::CodeGenConfig config{};
|
|
config.ws_cull_masked_reject_sites = loaded.ws_cull_masked_reject_sites;
|
|
const auto code = generate_first_instruction(0x14800002u, config).full_code;
|
|
check(code.find("psx_ws_masked_reject(cpu->gpr[4], 0xFFFF0000u)") != std::string::npos,
|
|
"native branch emits guarded masked predicate");
|
|
config.overlay_mode = true;
|
|
const auto other = generate_first_instruction(0x14800003u, config).full_code;
|
|
check(other.find("psx_ws_masked_reject(") == std::string::npos,
|
|
"overlay with another instruction stays vanilla");
|
|
|
|
const std::string flag_body = plain + R"toml(
|
|
[[widescreen.cull.masked_reject]]
|
|
address = "0x80010000"
|
|
expected = "0x04400002"
|
|
reject_mask = "0x7F87A000"
|
|
)toml";
|
|
const auto flag_config = load(flag_body);
|
|
config.ws_cull_masked_reject_sites = flag_config.ws_cull_masked_reject_sites;
|
|
config.overlay_mode = false;
|
|
const auto flags = generate_first_instruction(kBltzV0, config).full_code;
|
|
check(flags.find("psx_ws_x_margin() > 0 ? psx_ws_masked_reject(cpu->gpr[2], 0x7F87A000u) : ((int32_t)cpu->gpr[2] < 0)") != std::string::npos,
|
|
"GTE summary branch preserves signed predicate at 4:3");
|
|
check(load_throws_with(plain + R"toml(
|
|
[[widescreen.cull.masked_reject]]
|
|
address = "0x80010000"
|
|
expected = "0x04500002"
|
|
reject_mask = "0x7F87A000"
|
|
)toml", "BLTZ reg"), "masked reject excludes link branch");
|
|
config.overlay_mode = true;
|
|
const auto mismatched_flags = generate_first_instruction(kBgezV0, config).full_code;
|
|
check(mismatched_flags.find("psx_ws_masked_reject(") == std::string::npos,
|
|
"GTE overlay opcode mismatch keeps signed vanilla branch");
|
|
}
|
|
|
|
// ---- [[widescreen.cull.sxy]] ----------------------------------------------
|
|
// The Wild 9 (SLUS-00425) shapes, relocated to kBase. Tri: the fold has GTE
|
|
// reads and stores between its ANDs and one instruction before the consumer.
|
|
// +00 and at,t0,s1 +04 mfc2 t3,$14 +08 and at,s4,at +0C sw t0,8(a0)
|
|
// +10 and at,t3,at +14 sub t0,s0,t0 +18 bne at,zero,+1 ; nop ; jr ra ; nop
|
|
// Quad: four contiguous ANDs, then the consumer.
|
|
const std::vector<uint32_t> kTriWords = {
|
|
0x01110824u, 0x480B7000u, 0x02810824u, 0xAC880008u, 0x01610824u,
|
|
0x02084022u, 0x14200001u, 0x00000000u, 0x03E00008u, 0x00000000u};
|
|
const std::vector<uint32_t> kQuadWords = {
|
|
0x01110824u, 0x02810824u, 0x01610824u, 0x01210824u, 0x14200001u,
|
|
0x00000000u, 0x03E00008u, 0x00000000u};
|
|
|
|
PSXRecomp::GeneratedFunction generate_words(const std::vector<uint32_t>& words,
|
|
const PSXRecomp::CodeGenConfig& config) {
|
|
PSXRecomp::PS1Executable exe{};
|
|
exe.header.load_address = kBase;
|
|
exe.header.initial_pc = kBase;
|
|
exe.header.file_size = (uint32_t)(4 * words.size());
|
|
for (uint32_t w : words) append_word(exe.code_data, w);
|
|
PSXRecomp::Function function{};
|
|
function.start_addr = kBase;
|
|
function.end_addr = kBase + 4u * (uint32_t)words.size();
|
|
function.size = 4u * (uint32_t)words.size();
|
|
function.name = "ws_cull_sxy_test";
|
|
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
|
|
const auto cfg = analyzer.analyze_function(function);
|
|
PSXRecomp::CodeGenerator generator(exe, config);
|
|
return generator.generate_function(function, cfg);
|
|
}
|
|
|
|
struct SxyToml {
|
|
std::string kind = "tri";
|
|
std::string vertex_regs = "[8, 20, 11]";
|
|
std::string fold_addresses = R"(["0x80010000", "0x80010008", "0x80010010"])";
|
|
std::string fold_expected = R"(["0x01110824", "0x02810824", "0x01610824"])";
|
|
std::string consumer_address = "0x80010018";
|
|
std::string consumer_expected = "0x14200001";
|
|
std::string str() const {
|
|
return "\n[[widescreen.cull.sxy]]\nkind = \"" + kind + "\"\n" +
|
|
"vertex_regs = " + vertex_regs + "\n" +
|
|
"fold_addresses = " + fold_addresses + "\n" +
|
|
"fold_expected = " + fold_expected + "\n" +
|
|
"consumer_address = \"" + consumer_address + "\"\n" +
|
|
"consumer_expected = \"" + consumer_expected + "\"\n";
|
|
}
|
|
};
|
|
|
|
SxyToml quad_toml() {
|
|
SxyToml q;
|
|
q.kind = "quad";
|
|
q.vertex_regs = "[8, 20, 11, 9]";
|
|
q.fold_addresses = R"(["0x80010000", "0x80010004", "0x80010008", "0x8001000C"])";
|
|
q.fold_expected = R"(["0x01110824", "0x02810824", "0x01610824", "0x01210824"])";
|
|
q.consumer_address = "0x80010010";
|
|
return q;
|
|
}
|
|
|
|
void sxy_loader() {
|
|
const auto tri = load(base_config() + SxyToml{}.str());
|
|
check(tri.ws_cull_sxy_sites.size() == 1, "loader reads a tri sxy site");
|
|
const auto& t = tri.ws_cull_sxy_sites.at(0);
|
|
check(t.vertices == 3 && t.vertex_regs == std::vector<uint32_t>({8, 20, 11}) &&
|
|
t.fold_addresses == std::vector<uint32_t>({0x80010000u, 0x80010008u, 0x80010010u}) &&
|
|
t.fold_expected == std::vector<uint32_t>({0x01110824u, 0x02810824u, 0x01610824u}) &&
|
|
t.consumer_address == 0x80010018u && t.consumer_expected == 0x14200001u,
|
|
"tri sxy site fields");
|
|
const auto quad = load(base_config() + quad_toml().str());
|
|
check(quad.ws_cull_sxy_sites.size() == 1 && quad.ws_cull_sxy_sites[0].vertices == 4,
|
|
"loader reads a quad sxy site");
|
|
check(load(base_config()).ws_cull_sxy_sites.empty(), "sxy defaults to empty");
|
|
|
|
// Operands may be written in either order; beq is a consumer as well.
|
|
SxyToml swapped;
|
|
swapped.fold_expected = R"(["0x02280824", "0x00340824", "0x002B0824"])";
|
|
swapped.consumer_expected = "0x10010001";
|
|
check(load(base_config() + swapped.str()).ws_cull_sxy_sites.size() == 1,
|
|
"swapped AND operands and a beq zero,at consumer are accepted");
|
|
|
|
auto bad = [&](SxyToml site, const char* needle, const char* what) {
|
|
check(load_throws_with(base_config() + site.str(), needle), what);
|
|
};
|
|
SxyToml s;
|
|
s = {}; s.kind = "pent";
|
|
bad(s, "kind must be", "unknown kind");
|
|
s = {}; s.vertex_regs = "[8, 20]";
|
|
bad(s, "needs exactly 3", "tri with two vertex_regs");
|
|
s = quad_toml(); s.fold_expected = R"(["0x01110824", "0x02810824", "0x01610824"])";
|
|
bad(s, "needs exactly 4", "quad with three AND words");
|
|
s = {}; s.vertex_regs = "[8, 8, 11]";
|
|
bad(s, "must be distinct", "repeated vertex register");
|
|
s = {}; s.vertex_regs = "[8, 20, 0]";
|
|
bad(s, "not a GPR", "$zero vertex register");
|
|
s = {}; s.vertex_regs = "[8, 20, 1]";
|
|
bad(s, "is the fold register", "vertex register equal to the fold register");
|
|
s = {}; s.vertex_regs = "[8, 20, 12]";
|
|
bad(s, "does not read vertex register 12", "AND does not read its vertex");
|
|
s = {}; s.vertex_regs = "[8, 20, 17]";
|
|
s.fold_expected = R"(["0x01140824", "0x02810824", "0x02210824"])";
|
|
bad(s, "separate mask register", "first AND combines two vertices");
|
|
s = {}; s.fold_expected = R"(["0x01110824", "0x02811024", "0x01610824"])";
|
|
bad(s, "same destination", "AND into another register");
|
|
s = {}; s.fold_expected = R"(["0x01110825", "0x02810824", "0x01610824"])";
|
|
bad(s, "must be `and r", "OR in the fold");
|
|
s = {}; s.fold_expected = R"(["0x01110824", "0x02910824", "0x01610824"])";
|
|
bad(s, "with the fold register", "later AND not chained through the fold register");
|
|
s = {}; s.fold_expected = R"(["0x01100024", "0x02800024", "0x01600024"])";
|
|
bad(s, "must not write $zero", "fold into $zero");
|
|
s = {}; s.consumer_expected = "0x14220001";
|
|
bad(s, "beq/bne of the fold register", "consumer compares with another register");
|
|
s = {}; s.consumer_expected = "0x04200001";
|
|
bad(s, "beq/bne of the fold register", "consumer is bltz");
|
|
s = {}; s.consumer_address = "0x8001002C";
|
|
bad(s, "within 4 instructions", "consumer too far from the fold");
|
|
s = {}; s.fold_addresses = R"(["0x80010008", "0x80010000", "0x80010010"])";
|
|
bad(s, "must follow", "fold addresses out of order");
|
|
s = {}; s.consumer_address = "0x80010010";
|
|
bad(s, "must follow", "consumer on the last AND");
|
|
s = {}; s.fold_addresses = R"(["0x80010000", "0xA0010008", "0x80010010"])";
|
|
bad(s, "segment of the first AND", "fold crossing segments");
|
|
s = {}; s.fold_addresses = R"(["0x80010000", "0x8001000A", "0x80010010"])";
|
|
bad(s, "word-aligned", "misaligned AND");
|
|
check(load_throws_with(base_config() + SxyToml{}.str() + quad_toml().str(),
|
|
"already used by another sxy site"),
|
|
"two sites sharing a physical address");
|
|
}
|
|
|
|
void sxy_hash_identity() {
|
|
const auto plain = load(base_config());
|
|
const auto with = load(base_config() + SxyToml{}.str());
|
|
check(PSXRecompV4::overlay_codegen_config_hash(plain) !=
|
|
PSXRecompV4::overlay_codegen_config_hash(with),
|
|
"an sxy site changes the overlay-cache identity");
|
|
auto changed = with;
|
|
changed.ws_cull_sxy_sites[0].consumer_expected = 0x10200001u;
|
|
check(PSXRecompV4::overlay_codegen_config_hash(changed) !=
|
|
PSXRecompV4::overlay_codegen_config_hash(with),
|
|
"the consumer word is part of the overlay-cache identity");
|
|
changed = with;
|
|
changed.ws_cull_sxy_sites[0].vertex_regs[2] = 12u;
|
|
check(PSXRecompV4::overlay_codegen_config_hash(changed) !=
|
|
PSXRecompV4::overlay_codegen_config_hash(with),
|
|
"the vertex registers are part of the overlay-cache identity");
|
|
}
|
|
|
|
bool has_text(const std::string& code, const char* needle) {
|
|
return code.find(needle) != std::string::npos;
|
|
}
|
|
|
|
void sxy_codegen() {
|
|
PSXRecomp::CodeGenConfig config{};
|
|
const auto vanilla_tri = generate_words(kTriWords, config).full_code;
|
|
check(!has_text(vanilla_tri, "psx_ws_cull_sxy_and"), "no sxy site: vanilla ANDs");
|
|
|
|
config.ws_cull_sxy_sites = load(base_config() + SxyToml{}.str()).ws_cull_sxy_sites;
|
|
const auto tri = generate_words(kTriWords, config).full_code;
|
|
check(has_text(tri, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[17], cpu->gpr[8]);"),
|
|
"tri AND 0 seeds from the mask with vertex t0");
|
|
check(has_text(tri, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[1], cpu->gpr[20]);"),
|
|
"tri AND 1 folds vertex s4");
|
|
check(has_text(tri, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[1], cpu->gpr[11]);"),
|
|
"tri AND 2 folds vertex t3");
|
|
check(!has_text(tri, "cpu->gpr[1] = cpu->gpr[8] & cpu->gpr[17];"),
|
|
"tri vanilla AND is replaced");
|
|
check(!exits_with_failure([&] { generate_words(kTriWords, config); }),
|
|
"a matching tri fold generates");
|
|
|
|
PSXRecomp::CodeGenConfig qconfig{};
|
|
qconfig.ws_cull_sxy_sites = load(base_config() + quad_toml().str()).ws_cull_sxy_sites;
|
|
const auto quad = generate_words(kQuadWords, qconfig).full_code;
|
|
check(has_text(quad, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[17], cpu->gpr[8]);") &&
|
|
has_text(quad, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[1], cpu->gpr[20]);") &&
|
|
has_text(quad, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[1], cpu->gpr[11]);") &&
|
|
has_text(quad, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[1], cpu->gpr[9]);"),
|
|
"quad folds all four vertices");
|
|
|
|
// The guest's own consumer branch is untouched.
|
|
check(has_text(quad, "cpu->gpr[1] != cpu->gpr[0]") || has_text(quad, "cpu->gpr[1] != 0"),
|
|
"the consumer bne is the vanilla branch");
|
|
|
|
// Any word of the fold or the consumer that differs: main EXE fails,
|
|
// an overlay variant keeps every AND vanilla.
|
|
for (size_t i : {size_t(1), size_t(2), size_t(6)}) {
|
|
auto words = kTriWords;
|
|
if (i == 1) words[2] = 0x02810825u; // the second AND is an OR
|
|
if (i == 2) words[4] = 0x01710824u; // the last AND reads s1
|
|
if (i == 6) words[6] = 0x10200001u; // beq instead of bne
|
|
check(exits_with_failure([&] { generate_words(words, config); }),
|
|
"a mismatching tri fold fails main-EXE generation");
|
|
auto overlay = config;
|
|
overlay.overlay_mode = true;
|
|
const auto code = generate_words(words, overlay).full_code;
|
|
check(!has_text(code, "psx_ws_cull_sxy_and"),
|
|
"an overlay variant with any mismatching word stays vanilla");
|
|
}
|
|
auto overlay = config;
|
|
overlay.overlay_mode = true;
|
|
check(has_text(generate_words(kTriWords, overlay).full_code,
|
|
"psx_ws_cull_sxy_and(cpu->gpr[17], cpu->gpr[8])"),
|
|
"an overlay with the exact fold emits the helper");
|
|
|
|
// Declared only for a config that lists sxy sites.
|
|
PSXRecomp::PS1Executable exe{};
|
|
exe.header.load_address = kBase;
|
|
std::vector<PSXRecomp::GeneratedFunction> functions;
|
|
check(!has_text(PSXRecomp::CodeGenerator(exe).build_shared_decls_header(functions),
|
|
"psx_ws_cull_sxy_and"),
|
|
"a config without sxy sites does not declare the helper");
|
|
check(has_text(PSXRecomp::CodeGenerator(exe, config).build_shared_decls_header(functions),
|
|
"extern uint32_t psx_ws_cull_sxy_and(uint32_t acc, uint32_t sxy);"),
|
|
"a config with sxy sites declares the helper");
|
|
}
|
|
|
|
uint32_t pack_sxy(int x, int y) {
|
|
return ((uint32_t)(uint16_t)(int16_t)y << 16) | (uint16_t)(int16_t)x;
|
|
}
|
|
|
|
uint32_t sxy_fold(const uint32_t* v, int n, uint32_t mask, int32_t m) {
|
|
uint32_t r = psx_ws_sxy_and_value(mask, v[0], m);
|
|
for (int k = 1; k < n; k++) r = psx_ws_sxy_and_value(r, v[k], m);
|
|
return r;
|
|
}
|
|
|
|
// Every tri (and a quad sweep) over the edge values: at margin 0 the fold is
|
|
// the vanilla AND word for any mask; widened, it differs from vanilla only in
|
|
// bit 15, which then means "every X < -m". Bit 31 (all Y above the screen)
|
|
// and the right/bottom side (a separate chain in the guest) are unchanged.
|
|
void sxy_runtime_math() {
|
|
const int xs[] = {-1024, -200, -54, -53, -52, -1, 0, 1, 160, 319, 320, 1023};
|
|
const int ys[] = {-1024, -1, 0, 1, 239, 240};
|
|
std::vector<uint32_t> pts;
|
|
for (int x : xs) for (int y : ys) pts.push_back(pack_sxy(x, y));
|
|
const uint32_t masks[] = {0x80008000u, 0xFFFFFFFFu, 0x00008000u, 0x80000000u};
|
|
int bad0 = 0, badm = 0;
|
|
for (uint32_t mask : masks)
|
|
for (uint32_t a : pts) for (uint32_t b : pts) for (uint32_t c : pts) {
|
|
const uint32_t v[3] = {a, b, c};
|
|
const uint32_t vanilla = mask & a & b & c;
|
|
if (sxy_fold(v, 3, mask, 0) != vanilla) bad0++;
|
|
for (int32_t m : {53, 192}) {
|
|
bool all_left = true;
|
|
for (uint32_t p : v) all_left &= (int16_t)(p & 0xFFFFu) < -m;
|
|
const uint32_t want = (vanilla & ~0x8000u) |
|
|
(all_left ? (vanilla & 0x8000u) : 0u);
|
|
if (sxy_fold(v, 3, mask, m) != want) badm++;
|
|
}
|
|
}
|
|
for (uint32_t a : pts) for (uint32_t d : pts) {
|
|
const uint32_t v[4] = {a, pack_sxy(-300, 10), pack_sxy(-60, -5), d};
|
|
const uint32_t vanilla = 0x80008000u & v[0] & v[1] & v[2] & v[3];
|
|
if (sxy_fold(v, 4, 0x80008000u, 0) != vanilla) bad0++;
|
|
bool all_left = true;
|
|
for (uint32_t p : v) all_left &= (int16_t)(p & 0xFFFFu) < -53;
|
|
const uint32_t want = (vanilla & ~0x8000u) | (all_left ? (vanilla & 0x8000u) : 0u);
|
|
if (sxy_fold(v, 4, 0x80008000u, 53) != want) badm++;
|
|
}
|
|
check(bad0 == 0, "the sxy fold is the vanilla AND at margin 0");
|
|
check(badm == 0, "widened, the sxy fold differs only in bit 15 = every X < -m");
|
|
|
|
// Spot checks at 16:9 on 320 wide (margin 53), mask 0x80008000.
|
|
const uint32_t mask = 0x80008000u;
|
|
uint32_t v[3] = {pack_sxy(-60, 10), pack_sxy(-54, 20), pack_sxy(-100, 30)};
|
|
check(sxy_fold(v, 3, mask, 53) == 0x8000u, "all vertices left of -m: rejected");
|
|
v[1] = pack_sxy(-53, 20);
|
|
check(sxy_fold(v, 3, mask, 53) == 0u, "one vertex in the revealed band: kept");
|
|
check(sxy_fold(v, 3, mask, 0) == 0x8000u, "the same triangle is rejected at 4:3");
|
|
uint32_t up[3] = {pack_sxy(-10, -1), pack_sxy(100, -5), pack_sxy(5, -9)};
|
|
check(sxy_fold(up, 3, mask, 53) == 0x80000000u, "all above the screen: still rejected");
|
|
check(psx_ws_sxy_and_value(0x8000u, pack_sxy(-5, 0), -7) == 0x8000u,
|
|
"a negative margin is treated as 0");
|
|
}
|
|
|
|
void runtime_math() {
|
|
// Identity at 4:3 (margin 0) for every input.
|
|
for (int32_t v : {-1000, -54, -53, -1, 0, 1, 98, 222, 319, 320, 321, 1000}) {
|
|
check(psx_ws_cull_bgez_value(v, 0) == (v >= 0 ? 1 : 0),
|
|
"bgez is vanilla at margin 0");
|
|
check(psx_ws_clip_edge_x_value((uint32_t)v, 320u, 0) == (uint32_t)v,
|
|
"clip edge is the identity at margin 0");
|
|
}
|
|
// 16:9 on a 320-wide display: margin 53.
|
|
check(psx_ws_cull_bgez_value(-53, 53) == 1, "bgez keeps x = -m");
|
|
check(psx_ws_cull_bgez_value(-54, 53) == 0, "bgez rejects x < -m");
|
|
check(psx_ws_cull_bgez_value(-5, 53) == 1, "bgez keeps the revealed band");
|
|
check(psx_ws_clip_edge_x_value(0u, 320u, 53) == (uint32_t)-53,
|
|
"left screen edge moves to -m");
|
|
check(psx_ws_clip_edge_x_value(320u, 320u, 53) == 373u,
|
|
"right screen edge moves to W+m");
|
|
check(psx_ws_clip_edge_x_value(98u, 320u, 53) == 98u &&
|
|
psx_ws_clip_edge_x_value(222u, 320u, 53) == 222u,
|
|
"interior viewport edges (mirror) are unchanged");
|
|
check(psx_ws_clip_edge_x_value(160u, 320u, 53) == 160u,
|
|
"split-screen centre edge is unchanged");
|
|
check(psx_ws_clip_edge_x_value(0xFFFFFFFFu, 320u, 53) == 0xFFFFFFFFu,
|
|
"a negative bound other than 0 is unchanged");
|
|
check(psx_ws_cull_bgez_value(-1, -7) == 0,
|
|
"a negative margin is treated as 0");
|
|
}
|
|
|
|
} // 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]);
|
|
if (!std::freopen("NUL", "w", stderr)) return 3;
|
|
}
|
|
loader_parses_new_kinds();
|
|
hash_identity_only_changes_when_used();
|
|
codegen_emits_bgez();
|
|
codegen_emits_clip_edge_loads();
|
|
main_exe_guards_fail_the_build();
|
|
branch_site_guards();
|
|
overlay_matching_sites_emit_helpers();
|
|
shared_decls_include_new_helpers();
|
|
runtime_math();
|
|
masked_reject_sites();
|
|
sxy_loader();
|
|
sxy_hash_identity();
|
|
sxy_codegen();
|
|
sxy_runtime_math();
|
|
|
|
if (g_exit_probe >= 0) return 3; // no call had that probe index
|
|
if (failures != 0) {
|
|
std::fprintf(stderr, "ws_cull_edge_codegen_test: %d failure(s)\n",
|
|
failures);
|
|
return 1;
|
|
}
|
|
std::puts("PASS: ws cull edge kinds");
|
|
return 0;
|
|
}
|