(WIP) Add a native PowerPC testing frontend

This commit is contained in:
Dr. Chat 2017-05-08 22:21:43 -05:00
parent 095f65c19e
commit ccd6d4b199
4 changed files with 763 additions and 0 deletions

View File

@ -5,6 +5,14 @@ location(build_root)
targetdir(build_bin)
objdir(build_obj)
-- Define an ARCH variable
-- Only use this to enable architecture-specific functionality.
if os.is("linux") then
ARCH = os.outputof("uname -p")
else
ARCH = "unknown"
end
includedirs({
".",
"src",
@ -84,6 +92,13 @@ filter("platforms:Linux")
"pthread",
})
filter({"platforms:Linux", "language:C++", "toolset:gcc"})
buildoptions({
"--std=c++11",
})
links({
})
filter({"platforms:Linux", "language:C++", "toolset:clang"})
buildoptions({
"-std=c++14",

View File

@ -0,0 +1,523 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2017 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include <gflags/gflags.h>
#include "xenia/base/filesystem.h"
#include "xenia/base/logging.h"
#include "xenia/base/main.h"
#include "xenia/base/math.h"
#include "xenia/base/memory.h"
#include "xenia/base/platform.h"
#include "xenia/base/string_util.h"
#include "xenia/cpu/ppc/ppc_context.h"
#if XE_COMPILER_MSVC
#include "xenia/base/platform_win.h"
#endif // XE_COMPILER_MSVC
DEFINE_string(test_path, "src/xenia/cpu/ppc/testing/",
"Directory scanned for test files.");
DEFINE_string(test_bin_path, "src/xenia/cpu/ppc/testing/bin/",
"Directory with binary outputs of the test files.");
extern "C" void xe_call_native(void* context, void* fn);
namespace xe {
namespace cpu {
namespace test {
struct Context {
uint64_t r[32];
double f[32];
vec128_t v[32]; // For now, only support 32 vector registers.
uint32_t cr; // Condition register
};
typedef std::vector<std::pair<std::string, std::string>> AnnotationList;
const uint32_t START_ADDRESS = 0x00000000;
struct TestCase {
TestCase(uint32_t address, std::string& name)
: address(address), name(name) {}
uint32_t address;
std::string name;
AnnotationList annotations;
};
class TestSuite {
public:
TestSuite(const std::wstring& src_file_path) : src_file_path(src_file_path) {
name = src_file_path.substr(src_file_path.find_last_of(xe::kPathSeparator) +
1);
name = ReplaceExtension(name, L"");
map_file_path = xe::to_wstring(FLAGS_test_bin_path) + name + L".map";
bin_file_path = xe::to_wstring(FLAGS_test_bin_path) + name + L".bin";
}
bool Load() {
if (!ReadMap(map_file_path)) {
XELOGE("Unable to read map for test %ls", src_file_path.c_str());
return false;
}
if (!ReadAnnotations(src_file_path)) {
XELOGE("Unable to read annotations for test %ls", src_file_path.c_str());
return false;
}
return true;
}
std::wstring name;
std::wstring src_file_path;
std::wstring map_file_path;
std::wstring bin_file_path;
std::vector<TestCase> test_cases;
private:
std::wstring ReplaceExtension(const std::wstring& path,
const std::wstring& new_extension) {
std::wstring result = path;
auto last_dot = result.find_last_of('.');
result.replace(result.begin() + last_dot, result.end(), new_extension);
return result;
}
TestCase* FindTestCase(const std::string& name) {
for (auto& test_case : test_cases) {
if (test_case.name == name) {
return &test_case;
}
}
return nullptr;
}
bool ReadMap(const std::wstring& map_file_path) {
FILE* f = fopen(xe::to_string(map_file_path).c_str(), "r");
if (!f) {
return false;
}
char line_buffer[BUFSIZ];
while (fgets(line_buffer, sizeof(line_buffer), f)) {
if (!strlen(line_buffer)) {
continue;
}
// 0000000000000000 t test_add1\n
char* newline = strrchr(line_buffer, '\n');
if (newline) {
*newline = 0;
}
char* t_test_ = strstr(line_buffer, " t test_");
if (!t_test_) {
continue;
}
std::string address(line_buffer, t_test_ - line_buffer);
std::string name(t_test_ + strlen(" t test_"));
test_cases.emplace_back(START_ADDRESS + std::stoul(address, 0, 16), name);
}
fclose(f);
return true;
}
bool ReadAnnotations(const std::wstring& src_file_path) {
TestCase* current_test_case = nullptr;
FILE* f = fopen(xe::to_string(src_file_path).c_str(), "r");
if (!f) {
return false;
}
char line_buffer[BUFSIZ];
while (fgets(line_buffer, sizeof(line_buffer), f)) {
if (!strlen(line_buffer)) {
continue;
}
// Eat leading whitespace.
char* start = line_buffer;
while (*start == ' ') {
++start;
}
if (strncmp(start, "test_", strlen("test_")) == 0) {
// Global test label.
std::string label(start + strlen("test_"), strchr(start, ':'));
current_test_case = FindTestCase(label);
if (!current_test_case) {
XELOGE("Test case %s not found in corresponding map for %ls",
label.c_str(), src_file_path.c_str());
return false;
}
} else if (strlen(start) > 3 && start[0] == '#' && start[1] == '_') {
// Annotation.
// We don't actually verify anything here.
char* next_space = strchr(start + 3, ' ');
if (next_space) {
// Looks legit.
std::string key(start + 3, next_space);
std::string value(next_space + 1);
while (value.find_last_of(" \t\n") == value.size() - 1) {
value.erase(value.end() - 1);
}
if (!current_test_case) {
XELOGE("Annotation outside of test case in %ls",
src_file_path.c_str());
return false;
}
current_test_case->annotations.emplace_back(key, value);
}
}
}
fclose(f);
return true;
}
};
class TestRunner {
public:
TestRunner() {
memory_size_ = 64 * 1024 * 1024;
memory_ = memory::AllocFixed(nullptr, memory_size_,
memory::AllocationType::kReserveCommit,
memory::PageAccess::kExecuteReadWrite);
context_ = memory::AlignedAlloc<Context>(32);
}
~TestRunner() {
memory::DeallocFixed(memory_, memory_size_,
memory::DeallocationType::kDecommitRelease);
memory::AlignedFree(context_);
}
bool Setup(TestSuite& suite) {
// TODO: Load the test suite into memory.
FILE* file = filesystem::OpenFile(suite.bin_file_path, "rb");
if (!file) {
XELOGE("Failed to open file %s!", suite.bin_file_path);
return false;
}
fseek(file, 0, SEEK_END);
uint32_t file_length = static_cast<uint32_t>(ftell(file));
fseek(file, 0, SEEK_SET);
if (file_length > memory_size_) {
XELOGE("Bin file %s is too big!", suite.bin_file_path);
return false;
}
// Read entire file into our memory.
fread(memory_, file_length, 1, file);
fclose(file);
return true;
}
bool Run(TestCase& test_case) {
// Setup test state from annotations.
if (!SetupTestState(test_case)) {
XELOGE("Test setup failed");
return false;
}
// Execute test.
xe_call_native(reinterpret_cast<void*>(context_),
reinterpret_cast<uint8_t*>(memory_) + test_case.address);
return CheckTestResults(test_case);
}
bool CompareRegWithString(const char* name, const char* value,
char* out_value, size_t out_value_size,
Context* ctx) const {
int n;
if (sscanf(name, "r%d", &n) == 1) {
uint64_t expected = string_util::from_string<uint64_t>(value);
if (ctx->r[n] != expected) {
std::snprintf(out_value, out_value_size, "%016" PRIX64, ctx->r[n]);
return false;
}
return true;
} else if (sscanf(name, "f%d", &n) == 1) {
if (std::strstr(value, "0x")) {
// Special case: Treat float as integer.
uint64_t expected = string_util::from_string<uint64_t>(value, true);
union {
double f;
uint64_t u;
} f2u;
f2u.f = ctx->f[n];
if (f2u.u != expected) {
std::snprintf(out_value, out_value_size, "%016" PRIX64, f2u.u);
return false;
}
} else {
double expected = string_util::from_string<double>(value);
// TODO(benvanik): epsilon
if (ctx->f[n] != expected) {
std::snprintf(out_value, out_value_size, "%f", ctx->f[n]);
return false;
}
}
return true;
} else if (sscanf(name, "v%d", &n) == 1) {
vec128_t expected = string_util::from_string<vec128_t>(value);
if (ctx->v[n] != expected) {
std::snprintf(out_value, out_value_size, "[%.8X, %.8X, %.8X, %.8X]",
ctx->v[n].i32[0], ctx->v[n].i32[1], ctx->v[n].i32[2],
ctx->v[n].i32[3]);
return false;
}
return true;
} else if (std::strcmp(name, "cr") == 0) {
// TODO(DrChat)
/*
uint64_t actual = ctx->cr();
uint64_t expected = string_util::from_string<uint64_t>(value);
if (actual != expected) {
std::snprintf(out_value, out_value_size, "%016" PRIX64, actual);
return false;
}
*/
return false; // true;
} else {
assert_always("Unrecognized register name: %s\n", name);
return false;
}
}
bool SetRegFromString(const char* name, const char* value, Context* ctx) {
int n;
if (sscanf(name, "r%d", &n) == 1) {
ctx->r[n] = string_util::from_string<uint64_t>(value);
} else if (sscanf(name, "f%d", &n) == 1) {
ctx->f[n] = string_util::from_string<double>(value);
} else if (sscanf(name, "v%d", &n) == 1) {
ctx->v[n] = string_util::from_string<vec128_t>(value);
} else if (std::strcmp(name, "cr") == 0) {
// this->set_cr(string_util::from_string<uint64_t>(value));
} else {
printf("Unrecognized register name: %s\n", name);
}
}
bool SetupTestState(TestCase& test_case) {
for (auto& it : test_case.annotations) {
if (it.first == "REGISTER_IN") {
size_t space_pos = it.second.find(" ");
auto reg_name = it.second.substr(0, space_pos);
auto reg_value = it.second.substr(space_pos + 1);
if (!SetRegFromString(reg_name.c_str(), reg_value.c_str(), context_)) {
return false;
}
} else if (it.first == "MEMORY_IN") {
XELOGW("Warning: MEMORY_IN unimplemented");
return false;
/*
size_t space_pos = it.second.find(" ");
auto address_str = it.second.substr(0, space_pos);
auto bytes_str = it.second.substr(space_pos + 1);
uint32_t address = std::strtoul(address_str.c_str(), nullptr, 16);
auto p = memory->TranslateVirtual(address);
const char* c = bytes_str.c_str();
while (*c) {
while (*c == ' ') ++c;
if (!*c) {
break;
}
char ccs[3] = {c[0], c[1], 0};
c += 2;
uint32_t b = std::strtoul(ccs, nullptr, 16);
*p = static_cast<uint8_t>(b);
++p;
}
*/
}
}
return true;
}
bool CheckTestResults(TestCase& test_case) {
char actual_value[2048];
bool any_failed = false;
for (auto& it : test_case.annotations) {
if (it.first == "REGISTER_OUT") {
size_t space_pos = it.second.find(" ");
auto reg_name = it.second.substr(0, space_pos);
auto reg_value = it.second.substr(space_pos + 1);
if (!CompareRegWithString(reg_name.c_str(), reg_value.c_str(),
actual_value, xe::countof(actual_value),
context_)) {
any_failed = true;
XELOGE("Register %s assert failed:\n", reg_name.c_str());
XELOGE(" Expected: %s == %s\n", reg_name.c_str(), reg_value.c_str());
XELOGE(" Actual: %s == %s\n", reg_name.c_str(), actual_value);
}
} else if (it.first == "MEMORY_OUT") {
XELOGW("Warning: MEMORY_OUT unimplemented");
any_failed = true;
/*
size_t space_pos = it.second.find(" ");
auto address_str = it.second.substr(0, space_pos);
auto bytes_str = it.second.substr(space_pos + 1);
uint32_t address = std::strtoul(address_str.c_str(), nullptr, 16);
auto base_address = memory->TranslateVirtual(address);
auto p = base_address;
const char* c = bytes_str.c_str();
while (*c) {
while (*c == ' ') ++c;
if (!*c) {
break;
}
char ccs[3] = {c[0], c[1], 0};
c += 2;
uint32_t current_address =
address + static_cast<uint32_t>(p - base_address);
uint32_t expected = std::strtoul(ccs, nullptr, 16);
uint8_t actual = *p;
if (expected != actual) {
any_failed = true;
XELOGE("Memory %s assert failed:\n", address_str.c_str());
XELOGE(" Expected: %.8X %.2X\n", current_address, expected);
XELOGE(" Actual: %.8X %.2X\n", current_address, actual);
}
++p;
}
*/
}
}
return !any_failed;
}
void* memory_;
size_t memory_size_;
Context* context_;
};
bool DiscoverTests(std::wstring& test_path,
std::vector<std::wstring>& test_files) {
auto file_infos = xe::filesystem::ListFiles(test_path);
for (auto& file_info : file_infos) {
if (file_info.name != L"." && file_info.name != L".." &&
file_info.name.rfind(L".s") == file_info.name.size() - 2) {
test_files.push_back(xe::join_paths(test_path, file_info.name));
}
}
return true;
}
#if XE_COMPILER_MSVC
int filter(unsigned int code) {
if (code == EXCEPTION_ILLEGAL_INSTRUCTION) {
return EXCEPTION_EXECUTE_HANDLER;
}
return EXCEPTION_CONTINUE_SEARCH;
}
#endif // XE_COMPILER_MSVC
void ProtectedRunTest(TestSuite& test_suite, TestRunner& runner,
TestCase& test_case, int& failed_count,
int& passed_count) {
#if XE_COMPILER_MSVC
__try {
#endif // XE_COMPILER_MSVC
if (!runner.Setup(test_suite)) {
XELOGE(" TEST FAILED SETUP");
++failed_count;
}
if (runner.Run(test_case)) {
++passed_count;
} else {
XELOGE(" TEST FAILED");
++failed_count;
}
#if XE_COMPILER_MSVC
} __except (filter(GetExceptionCode())) {
XELOGE(" TEST FAILED (UNSUPPORTED INSTRUCTION)");
++failed_count;
}
#endif // XE_COMPILER_MSVC
}
bool RunTests(const std::wstring& test_name) {
int result_code = 1;
int failed_count = 0;
int passed_count = 0;
auto test_path_root =
xe::fix_path_separators(xe::to_wstring(FLAGS_test_path));
std::vector<std::wstring> test_files;
if (!DiscoverTests(test_path_root, test_files)) {
return false;
}
if (!test_files.size()) {
XELOGE("No tests discovered - invalid path?");
return false;
}
XELOGI("%d tests discovered.", (int)test_files.size());
XELOGI("");
std::vector<TestSuite> test_suites;
bool load_failed = false;
for (auto& test_path : test_files) {
TestSuite test_suite(test_path);
if (!test_name.empty() && test_suite.name != test_name) {
continue;
}
if (!test_suite.Load()) {
XELOGE("TEST SUITE %ls FAILED TO LOAD", test_path.c_str());
load_failed = true;
continue;
}
test_suites.push_back(std::move(test_suite));
}
if (load_failed) {
return false;
}
TestRunner runner;
for (auto& test_suite : test_suites) {
XELOGI("%ls.s:", test_suite.name.c_str());
for (auto& test_case : test_suite.test_cases) {
XELOGI(" - %s", test_case.name.c_str());
ProtectedRunTest(test_suite, runner, test_case, failed_count,
passed_count);
}
XELOGI("");
}
XELOGI("");
XELOGI("Total tests: %d", failed_count + passed_count);
XELOGI("Passed: %d", passed_count);
XELOGI("Failed: %d", failed_count);
return failed_count ? false : true;
}
int main(const std::vector<std::wstring>& args) {
// Grab test name, if present.
std::wstring test_name;
if (args.size() >= 2) {
test_name = args[1];
}
return RunTests(test_name) ? 0 : 1;
}
} // namespace test
} // namespace cpu
} // namespace xe
DEFINE_ENTRY_POINT(L"xenia-cpu-ppc-test", L"xenia-cpu-ppc-test [test name]",
xe::cpu::test::main);

View File

@ -0,0 +1,195 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2017 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
// r3 = context
// this does not touch r1, r3, r4, r13
.load_registers_ctx:
ld r0, 0x00(r3)
// r1 cannot be used
ld r2, 0x10(r3)
// r3 will be loaded before the call
// r4 will be loaded before the call
ld r5, 0x28(r3)
ld r6, 0x30(r3)
ld r7, 0x38(r3)
ld r8, 0x40(r3)
ld r9, 0x48(r3)
ld r10, 0x50(r3)
ld r11, 0x58(r3)
ld r12, 0x60(r3)
// r13 cannot be used (OS use only)
ld r14, 0x70(r3)
ld r15, 0x78(r3)
ld r16, 0x80(r3)
ld r17, 0x88(r3)
ld r18, 0x90(r3)
ld r19, 0x98(r3)
ld r20, 0xA0(r3)
ld r21, 0xA8(r3)
ld r22, 0xB0(r3)
ld r23, 0xB8(r3)
ld r24, 0xC0(r3)
ld r25, 0xC8(r3)
ld r26, 0xD0(r3)
ld r27, 0xD8(r3)
ld r28, 0xE0(r3)
ld r29, 0xE8(r3)
ld r30, 0xF0(r3)
ld r31, 0xF8(r3)
blr
// r3 = context
// this does not save r1, r3, r13
.save_registers_ctx:
std r0, 0x00(r3)
// r1 cannot be used
std r2, 0x10(r3)
// r3 will be saved later
std r4, 0x20(r3)
std r5, 0x28(r3)
std r6, 0x30(r3)
std r7, 0x38(r3)
std r8, 0x40(r3)
std r9, 0x48(r3)
std r10, 0x50(r3)
std r11, 0x58(r3)
std r12, 0x60(r3)
// r13 cannot be used (OS use only)
std r14, 0x70(r3)
std r15, 0x78(r3)
std r16, 0x80(r3)
std r17, 0x88(r3)
std r18, 0x90(r3)
std r19, 0x98(r3)
std r20, 0xA0(r3)
std r21, 0xA8(r3)
std r22, 0xB0(r3)
std r23, 0xB8(r3)
std r24, 0xC0(r3)
std r25, 0xC8(r3)
std r26, 0xD0(r3)
std r27, 0xD8(r3)
std r28, 0xE0(r3)
std r29, 0xE8(r3)
std r30, 0xF0(r3)
std r31, 0xF8(r3)
blr
// void xe_call_native(Context* ctx, void* func)
.globl xe_call_native
xe_call_native:
mflr r12
stw r12, -0x8(r1)
stwu r1, -0x380(r1) // 0x200(gpr + fp) + 0x200(vr)
// Save nonvolatile registers on the stack.
std r2, 0x110(r1)
std r3, 0x118(r1) // Store the context, this will be needed later.
std r14, 0x170(r1)
std r15, 0x178(r1)
std r16, 0x180(r1)
std r17, 0x188(r1)
std r18, 0x190(r1)
std r19, 0x198(r1)
std r20, 0x1A0(r1)
std r21, 0x1A8(r1)
std r22, 0x1B0(r1)
std r23, 0x1B8(r1)
std r24, 0x1C0(r1)
std r25, 0x1C8(r1)
std r26, 0x1D0(r1)
std r27, 0x1D8(r1)
std r28, 0x1E0(r1)
std r29, 0x1E8(r1)
std r30, 0x1F0(r1)
std r31, 0x1F8(r1)
stfd f14, 0x270(r1)
stfd f15, 0x278(r1)
stfd f16, 0x280(r1)
stfd f17, 0x288(r1)
stfd f18, 0x290(r1)
stfd f19, 0x298(r1)
stfd f20, 0x2A0(r1)
stfd f21, 0x2A8(r1)
stfd f22, 0x2B0(r1)
stfd f23, 0x2B8(r1)
stfd f24, 0x2C0(r1)
stfd f25, 0x2C8(r1)
stfd f26, 0x2D0(r1)
stfd f27, 0x2D8(r1)
stfd f28, 0x2E0(r1)
stfd f29, 0x2E8(r1)
stfd f30, 0x2F0(r1)
stfd f31, 0x2F8(r1)
// Load registers from context
bl load_registers_ctx
// Call the test routine
mtctr r4
ld r4, 0x28(r3)
ld r3, 0x20(r3)
bctrl
// Temporarily store r3 into the stack (in the place of r0)
std r3, 0x100(r1)
// Store registers into context
ld r3, 0x118(r1)
bl save_registers_ctx
// Now store r3
ld r4, 0x100(r1)
std r4, 0x20(r3)
// Restore nonvolatile registers from the stack
ld r2, 0x110(r1)
ld r14, 0x170(r1)
ld r15, 0x178(r1)
ld r16, 0x180(r1)
ld r17, 0x188(r1)
ld r18, 0x190(r1)
ld r19, 0x198(r1)
ld r20, 0x1A0(r1)
ld r21, 0x1A8(r1)
ld r22, 0x1B0(r1)
ld r23, 0x1B8(r1)
ld r24, 0x1C0(r1)
ld r25, 0x1C8(r1)
ld r26, 0x1D0(r1)
ld r27, 0x1D8(r1)
ld r28, 0x1E0(r1)
ld r29, 0x1E8(r1)
ld r30, 0x1F0(r1)
ld r31, 0x1F8(r1)
lfd f14, 0x270(r1)
lfd f15, 0x278(r1)
lfd f16, 0x280(r1)
lfd f17, 0x288(r1)
lfd f18, 0x290(r1)
lfd f19, 0x298(r1)
lfd f20, 0x2A0(r1)
lfd f21, 0x2A8(r1)
lfd f22, 0x2B0(r1)
lfd f23, 0x2B8(r1)
lfd f24, 0x2C0(r1)
lfd f25, 0x2C8(r1)
lfd f26, 0x2D0(r1)
lfd f27, 0x2D8(r1)
lfd f28, 0x2E0(r1)
lfd f29, 0x2E8(r1)
lfd f30, 0x2F0(r1)
lfd f31, 0x2F8(r1)
addi r1, r1, 0x380
lwz r12, -0x8(r1)
mtlr r12
blr

View File

@ -36,3 +36,33 @@ project("xenia-cpu-ppc-tests")
-- xenia-base needs this
links({"xenia-ui"})
if ARCH == "ppc64" or ARCH == "powerpc64" then
project("xenia-cpu-ppc-nativetests")
uuid("E381E8EE-65CD-4D5E-9223-D9C03B2CE78C")
kind("ConsoleApp")
language("C++")
links({
"xenia-base",
"xenia-core",
})
files({
"ppc_testing_native_main.cc",
"../../../base/main_"..platform_suffix..".cc",
})
files({
"*.s",
})
includedirs({
project_root.."/third_party/gflags/src",
})
filter("files:*.s")
flags({"ExcludeFromBuild"})
filter({})
files({
"ppc_testing_native_thunks.s",
})
end