mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-17 22:01:34 +02:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
280 lines
10 KiB
C++
280 lines
10 KiB
C++
// Tests for config/switch_tables.toml against config/nfsmw.toml's leaf
|
|
// function bounds: the jump tables XenonRecomp turns into C++ switches.
|
|
// XenonAnalyse takes a table's index register, label count and default from
|
|
// the bounds check before it (a cmplwi and a bgt/ble within 32
|
|
// instructions). A table with no bounds check of its own got the compare of
|
|
// the code before it, across a return: in v0.1.2 sub_822E7CE0 (the
|
|
// Performance Tuning row's label, GitHub issue #8) switched on r3 over one
|
|
// label instead of r4 over seven, so every row read "Steering"; two more
|
|
// leaf functions (sub_822C7720, sub_822C89F8) had the same fault.
|
|
// survey/fix_switch_tables.py now drops such tables and
|
|
// survey/find_missing_tables.py decodes them from the dispatch. Checked
|
|
// here, with no game files:
|
|
// - every table inside a leaf function the config bounds keeps its labels
|
|
// and its default (0 = a conditional return) inside that function: a
|
|
// borrowed default lies before the function's start;
|
|
// - the three tables as the guest code dispatches them;
|
|
// - the v0.1.2 entries, planted, fail the first check.
|
|
// From the repo root:
|
|
// clang++ -std=c++20 -O2 tests/switch_tables_test.cpp -o build/switch_tables_test
|
|
// build/switch_tables_test config/switch_tables.toml config/nfsmw.toml
|
|
#include <cinttypes>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <fstream>
|
|
#include <regex>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
static int g_failures = 0;
|
|
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
|
|
|
struct Table
|
|
{
|
|
uint32_t base = 0;
|
|
uint32_t r = 99;
|
|
bool hasDefault = false;
|
|
uint32_t defaultLabel = 0;
|
|
std::vector<uint32_t> labels;
|
|
};
|
|
|
|
struct Function
|
|
{
|
|
uint32_t address = 0;
|
|
uint32_t size = 0;
|
|
bool Contains(uint32_t a) const { return a >= address && a < address + size; }
|
|
};
|
|
|
|
static std::string ReadFile(const char* path)
|
|
{
|
|
std::ifstream f(path, std::ios::binary);
|
|
std::stringstream s;
|
|
s << f.rdbuf();
|
|
return s.str();
|
|
}
|
|
|
|
// The subset of TOML the generators write: [[switch]] blocks of
|
|
// `base = 0x..`, `r = N`, `default = 0x..` and a `labels = [ 0x.., ]` list.
|
|
static std::vector<Table> ParseTables(const std::string& text)
|
|
{
|
|
std::vector<Table> tables;
|
|
std::istringstream in(text);
|
|
std::string line;
|
|
bool inLabels = false;
|
|
std::smatch m;
|
|
static const std::regex kKey(R"(^\s*(base|r|default)\s*=\s*(0x[0-9A-Fa-f]+|\d+)\s*$)");
|
|
static const std::regex kLabel(R"(^\s*(0x[0-9A-Fa-f]+)\s*,?\s*$)");
|
|
while (std::getline(in, line))
|
|
{
|
|
if (line.rfind("[[switch]]", 0) == 0)
|
|
{
|
|
tables.emplace_back();
|
|
inLabels = false;
|
|
continue;
|
|
}
|
|
if (tables.empty() || line.empty() || line[0] == '#')
|
|
continue;
|
|
Table& t = tables.back();
|
|
if (inLabels)
|
|
{
|
|
if (line.find(']') != std::string::npos)
|
|
inLabels = false;
|
|
else if (std::regex_match(line, m, kLabel))
|
|
t.labels.push_back(uint32_t(std::stoul(m[1].str(), nullptr, 16)));
|
|
continue;
|
|
}
|
|
if (line.rfind("labels", 0) == 0)
|
|
{
|
|
inLabels = true;
|
|
continue;
|
|
}
|
|
if (std::regex_match(line, m, kKey))
|
|
{
|
|
uint32_t v = uint32_t(std::stoul(m[2].str(), nullptr, 0));
|
|
if (m[1] == "base")
|
|
t.base = v;
|
|
else if (m[1] == "r")
|
|
t.r = v;
|
|
else
|
|
{
|
|
t.hasDefault = true;
|
|
t.defaultLabel = v;
|
|
}
|
|
}
|
|
}
|
|
return tables;
|
|
}
|
|
|
|
static std::vector<Function> ParseFunctions(const std::string& text)
|
|
{
|
|
std::vector<Function> functions;
|
|
static const std::regex kFunction(R"(\{\s*address\s*=\s*(0x[0-9A-Fa-f]+)\s*,\s*size\s*=\s*(0x[0-9A-Fa-f]+)\s*\})");
|
|
for (std::sregex_iterator it(text.begin(), text.end(), kFunction), end; it != end; ++it)
|
|
functions.push_back({ uint32_t(std::stoul((*it)[1].str(), nullptr, 16)), uint32_t(std::stoul((*it)[2].str(), nullptr, 16)) });
|
|
return functions;
|
|
}
|
|
|
|
// Tables inside a bounded leaf function with a label or a default outside
|
|
// it, as "base: what" lines.
|
|
static std::vector<std::string> Escapes(const std::vector<Table>& tables, const std::vector<Function>& functions)
|
|
{
|
|
std::vector<std::string> out;
|
|
char buf[160];
|
|
for (const Table& t : tables)
|
|
for (const Function& f : functions)
|
|
{
|
|
if (!f.Contains(t.base))
|
|
continue;
|
|
if (t.hasDefault && t.defaultLabel != 0 && !f.Contains(t.defaultLabel))
|
|
{
|
|
snprintf(buf, sizeof(buf), "%08X: default %08X outside %08X+%X", t.base, t.defaultLabel, f.address, f.size);
|
|
out.push_back(buf);
|
|
}
|
|
for (uint32_t label : t.labels)
|
|
if (!f.Contains(label))
|
|
{
|
|
snprintf(buf, sizeof(buf), "%08X: label %08X outside %08X+%X", t.base, label, f.address, f.size);
|
|
out.push_back(buf);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// The table XenonRecomp uses at a bctr: the last [[switch]] keyed at or
|
|
// before it within the dispatch (XenonAnalyse keys the lis 20 bytes before
|
|
// the bctr, find_missing_tables.py the rlwinm 12 bytes before).
|
|
static const Table* TableAt(const std::vector<Table>& tables, uint32_t bctr)
|
|
{
|
|
const Table* best = nullptr;
|
|
for (const Table& t : tables)
|
|
if (t.base <= bctr && bctr - t.base <= 0x20 && (!best || t.base > best->base))
|
|
best = &t;
|
|
return best;
|
|
}
|
|
|
|
static const Function* FunctionAt(const std::vector<Function>& functions, uint32_t a)
|
|
{
|
|
for (const Function& f : functions)
|
|
if (f.Contains(a))
|
|
return &f;
|
|
return nullptr;
|
|
}
|
|
|
|
// The guest's dispatch: index register, table entries, last instruction.
|
|
struct Dispatch
|
|
{
|
|
const char* what;
|
|
uint32_t function;
|
|
uint32_t bctr;
|
|
uint32_t index;
|
|
std::vector<uint32_t> labels;
|
|
uint32_t end; // one past the last case's blr
|
|
};
|
|
|
|
static const Dispatch kDispatches[] = {
|
|
// Performance Tuning: the row's label string hash by slider (r4, 0-6:
|
|
// Steering ... Nitrous, then Turbo or Supercharger by r3).
|
|
{ "tuning row label", 0x822E7CE0, 0x822E7CF4, 4,
|
|
{ 0x822E7D14, 0x822E7D20, 0x822E7D2C, 0x822E7D38, 0x822E7D44, 0x822E7D50, 0x822E7D5C }, 0x822E7D80 },
|
|
// A count and its value from one of ten fields of the record in r3
|
|
// (x2000, x450, x250, ...), stored to *r4 and *r5.
|
|
{ "count and value by r6", 0x822C7720, 0x822C7734, 6,
|
|
{ 0x822C7760, 0x822C7774, 0x822C7788, 0x822C779C, 0x822C77B0,
|
|
0x822C77C4, 0x822C77D8, 0x822C77EC, 0x822C7800, 0x822C7814 }, 0x822C7828 },
|
|
// 0-6 -> 0-6 (the identity, written as a table).
|
|
{ "enum by r3", 0x822C89F8, 0x822C8A0C, 3,
|
|
{ 0x822C8A2C, 0x822C8A34, 0x822C8A5C, 0x822C8A3C, 0x822C8A44, 0x822C8A4C, 0x822C8A54 }, 0x822C8A64 },
|
|
};
|
|
|
|
// v0.1.2's entries for those dispatches (XenonAnalyse's output) and their
|
|
// function bounds.
|
|
static const char kReleased[] = R"(
|
|
[[switch]]
|
|
base = 0x822C7720
|
|
r = 11
|
|
default = 0x822C76E4
|
|
labels = [
|
|
0x822C7760,
|
|
0x822C7774,
|
|
]
|
|
|
|
[[switch]]
|
|
base = 0x822C89F8
|
|
r = 10
|
|
default = 0x822C89D0
|
|
labels = [
|
|
0x822C8A2C,
|
|
0x822C8A34,
|
|
]
|
|
|
|
[[switch]]
|
|
base = 0x822E7CE0
|
|
r = 3
|
|
default = 0x822E7BC4
|
|
labels = [
|
|
0x822E7D14,
|
|
]
|
|
)";
|
|
static const char kReleasedFunctions[] = R"(
|
|
{ address = 0x822C7720, size = 0x68 },
|
|
{ address = 0x822C89F8, size = 0x44 },
|
|
{ address = 0x822E7CE0, size = 0x40 },
|
|
)";
|
|
|
|
static bool Matches(const Table* t, const Function* f, const Dispatch& d)
|
|
{
|
|
return t && f && t->r == d.index && t->labels == d.labels && f->address == d.function && f->address + f->size >= d.end;
|
|
}
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
if (argc < 3)
|
|
{
|
|
fprintf(stderr, "usage: switch_tables_test <config/switch_tables.toml> <config/nfsmw.toml>\n");
|
|
return 2;
|
|
}
|
|
std::vector<Table> tables = ParseTables(ReadFile(argv[1]));
|
|
std::vector<Function> functions = ParseFunctions(ReadFile(argv[2]));
|
|
printf("switch_tables_test: %zu tables, %zu bounded leaf functions\n", tables.size(), functions.size());
|
|
CHECK(tables.size() > 500, "%zu tables: the file wasn't read", tables.size());
|
|
CHECK(functions.size() > 100, "%zu functions: the file wasn't read", functions.size());
|
|
for (const Table& t : tables)
|
|
CHECK(t.base != 0 && t.r < 32 && !t.labels.empty(), "an entry at %08X didn't parse (r %u, %zu labels)", t.base, t.r, t.labels.size());
|
|
|
|
for (const std::string& e : Escapes(tables, functions))
|
|
CHECK(false, "%s", e.c_str());
|
|
|
|
for (const Dispatch& d : kDispatches)
|
|
{
|
|
const Table* t = TableAt(tables, d.bctr);
|
|
const Function* f = FunctionAt(functions, d.bctr);
|
|
CHECK(t != nullptr, "%s: no table for the bctr at %08X", d.what, d.bctr);
|
|
CHECK(f != nullptr, "%s: no bounded function holds %08X", d.what, d.bctr);
|
|
if (!t || !f)
|
|
continue;
|
|
CHECK(t->r == d.index, "%s: switches on r%u, the guest indexes with r%u", d.what, t->r, d.index);
|
|
CHECK(t->labels == d.labels, "%s: %zu labels, the guest's table has %zu", d.what, t->labels.size(), d.labels.size());
|
|
CHECK(!t->hasDefault, "%s: a default label %08X, the guest has no bounds check", d.what, t->defaultLabel);
|
|
CHECK(f->address == d.function && f->address + f->size >= d.end,
|
|
"%s: function %08X+%X, the cases run %08X..%08X", d.what, f->address, f->size, d.function, d.end);
|
|
}
|
|
|
|
// Planted: the released entries must fail both checks.
|
|
std::vector<Table> released = ParseTables(kReleased);
|
|
std::vector<Function> releasedFunctions = ParseFunctions(kReleasedFunctions);
|
|
std::vector<std::string> escapes = Escapes(released, releasedFunctions);
|
|
CHECK(escapes.size() == 3, "the v0.1.2 entries escape their functions %zu times, expected 3 (one default each)", escapes.size());
|
|
for (const Dispatch& d : kDispatches)
|
|
CHECK(!Matches(TableAt(released, d.bctr), FunctionAt(releasedFunctions, d.bctr), d),
|
|
"%s: the v0.1.2 entry passes as the guest's dispatch", d.what);
|
|
|
|
if (g_failures)
|
|
{
|
|
printf("switch_tables_test: %d FAILED\n", g_failures);
|
|
return 1;
|
|
}
|
|
printf("switch_tables_test: all passed\n");
|
|
return 0;
|
|
}
|