diff --git a/premake5.lua b/premake5.lua index a6a732e54..8626ec7fb 100644 --- a/premake5.lua +++ b/premake5.lua @@ -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", diff --git a/src/xenia/cpu/ppc/testing/ppc_testing_native_main.cc b/src/xenia/cpu/ppc/testing/ppc_testing_native_main.cc new file mode 100644 index 000000000..4a52c0232 --- /dev/null +++ b/src/xenia/cpu/ppc/testing/ppc_testing_native_main.cc @@ -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 + +#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> 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 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(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(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(context_), + reinterpret_cast(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(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(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(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(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(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(value); + } else if (sscanf(name, "f%d", &n) == 1) { + ctx->f[n] = string_util::from_string(value); + } else if (sscanf(name, "v%d", &n) == 1) { + ctx->v[n] = string_util::from_string(value); + } else if (std::strcmp(name, "cr") == 0) { + // this->set_cr(string_util::from_string(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(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(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& 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 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 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& 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); diff --git a/src/xenia/cpu/ppc/testing/ppc_testing_native_thunks.s b/src/xenia/cpu/ppc/testing/ppc_testing_native_thunks.s new file mode 100644 index 000000000..00b251950 --- /dev/null +++ b/src/xenia/cpu/ppc/testing/ppc_testing_native_thunks.s @@ -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 \ No newline at end of file diff --git a/src/xenia/cpu/ppc/testing/premake5.lua b/src/xenia/cpu/ppc/testing/premake5.lua index c1f4f4cc9..5aaca98f4 100644 --- a/src/xenia/cpu/ppc/testing/premake5.lua +++ b/src/xenia/cpu/ppc/testing/premake5.lua @@ -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 \ No newline at end of file