From f32d52900e31fe0860c1a707dbf7be34a44e51e1 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:16:59 +0900 Subject: [PATCH] [Testing] Add some cpu backend tests --- src/xenia/cpu/test_module.cc | 8 +- src/xenia/cpu/test_module.h | 3 +- .../cpu/testing/backend_integration_test.cc | 335 ++++++++++++++++++ .../cpu/testing/set_rounding_mode_test.cc | 8 +- 4 files changed, 349 insertions(+), 5 deletions(-) diff --git a/src/xenia/cpu/test_module.cc b/src/xenia/cpu/test_module.cc index 181680daf..8453be680 100644 --- a/src/xenia/cpu/test_module.cc +++ b/src/xenia/cpu/test_module.cc @@ -26,7 +26,8 @@ namespace passes = xe::cpu::compiler::passes; TestModule::TestModule(Processor* processor, const std::string_view name, std::function contains_address, - std::function generate) + std::function generate, + bool skip_cf_simplification) : Module(processor), name_(name), contains_address_(contains_address), @@ -39,7 +40,10 @@ TestModule::TestModule(Processor* processor, const std::string_view name, // Merge blocks early. This will let us use more context in other passes. // The CFG is required for simplification and dirtied by it. compiler_->AddPass(std::make_unique()); - compiler_->AddPass(std::make_unique()); + if (!skip_cf_simplification) { + compiler_->AddPass( + std::make_unique()); + } compiler_->AddPass(std::make_unique()); // Passes are executed in the order they are added. Multiple of the same diff --git a/src/xenia/cpu/test_module.h b/src/xenia/cpu/test_module.h index 7dc52dbe1..880d21610 100644 --- a/src/xenia/cpu/test_module.h +++ b/src/xenia/cpu/test_module.h @@ -26,7 +26,8 @@ class TestModule : public Module { public: TestModule(Processor* processor, const std::string_view name, std::function contains_address, - std::function generate); + std::function generate, + bool skip_cf_simplification = false); ~TestModule() override; const std::string& name() const override { return name_; } diff --git a/src/xenia/cpu/testing/backend_integration_test.cc b/src/xenia/cpu/testing/backend_integration_test.cc index bd091a79f..2d89ba321 100644 --- a/src/xenia/cpu/testing/backend_integration_test.cc +++ b/src/xenia/cpu/testing/backend_integration_test.cc @@ -11,6 +11,7 @@ #include #include +#include #include "xenia/base/platform.h" #if XE_ARCH_AMD64 @@ -123,3 +124,337 @@ TEST_CASE("GUEST_TRAMPOLINE_BASIC", "[backend]") { // Clean up the trampoline. processor->backend()->FreeGuestTrampoline(trampoline_addr); } + +// ============================================================================= +// Host -> Guest -> Host round-trip via BuiltinFunction (GuestToHostThunk) +// ============================================================================= +// Tests the full thunk chain: host C++ calls into JIT'd guest code, which +// calls a builtin (host C++ function) via GuestToHostThunk, then returns. +// This exercises HostToGuestThunk entry, GuestToHostThunk transition, and +// proper return to guest code and back to host. + +static std::atomic builtin_call_count{0}; +static void BuiltinHandler(ppc::PPCContext* ctx, void* arg0, void* arg1) { + builtin_call_count.fetch_add(1); + // Write a known marker so the test can verify the builtin actually ran. + ctx->r[3] = 0xDEADBEEF; +} + +TEST_CASE("HOST_GUEST_HOST_ROUNDTRIP", "[backend]") { + builtin_call_count = 0; + + auto memory = std::make_unique(); + memory->Initialize(); + + std::unique_ptr backend; +#if XE_ARCH_AMD64 + backend.reset(new xe::cpu::backend::x64::X64Backend()); +#elif XE_ARCH_ARM64 + backend.reset(new xe::cpu::backend::a64::A64Backend()); +#endif + REQUIRE(backend); + + auto processor = std::make_unique(memory.get(), nullptr); + processor->Setup(std::move(backend)); + + // Define a builtin function that the guest code will call. + auto* builtin_fn = + processor->DefineBuiltin("TestBuiltin", BuiltinHandler, nullptr, nullptr); + REQUIRE(builtin_fn != nullptr); + + // Create a test module with a guest function that calls the builtin. + auto module = std::make_unique( + processor.get(), "Test", + [](uint32_t address) { return address == 0x80000000; }, + [builtin_fn](HIRBuilder& b) { + // Store a pre-call marker in r[4]. + StoreGPR(b, 4, b.LoadConstantUint64(0x11111111)); + // Call the builtin — this goes through GuestToHostThunk. + b.CallExtern(builtin_fn); + // Store a post-call marker in r[5] to prove we returned properly. + StoreGPR(b, 5, b.LoadConstantUint64(0x22222222)); + b.Return(); + return true; + }, + /*skip_cf_simplification=*/true); + processor->AddModule(std::move(module)); + processor->backend()->CommitExecutableRange(0x80000000, 0x80010000); + + auto fn = processor->ResolveFunction(0x80000000); + REQUIRE(fn != nullptr); + + uint32_t stack_size = 64 * 1024; + uint32_t stack_address = memory->SystemHeapAlloc(stack_size); + auto thread_state = std::make_unique(processor.get(), 0x100, + stack_address + stack_size); + auto ctx = thread_state->context(); + ctx->lr = 0xBCBCBCBC; + ctx->r[3] = 0; + ctx->r[4] = 0; + ctx->r[5] = 0; + + fn->Call(thread_state.get(), uint32_t(ctx->lr)); + + // Verify the builtin ran. + REQUIRE(builtin_call_count == 1); + // Verify the builtin wrote its marker. + REQUIRE(ctx->r[3] == 0xDEADBEEF); + // Verify pre-call code ran. + REQUIRE(ctx->r[4] == 0x11111111); + // Verify post-call code ran (guest code continued after GuestToHostThunk). + REQUIRE(ctx->r[5] == 0x22222222); + + memory->SystemHeapFree(stack_address); +} + +// ============================================================================= +// GPR preservation across GuestToHostThunk +// ============================================================================= +// Tests that callee-saved GPRs used by the register allocator survive a +// host call via GuestToHostThunk. We load values into several GPRs before +// the call, invoke a builtin, then read them back after. + +static void EmptyBuiltin(ppc::PPCContext* ctx, void* arg0, void* arg1) { + // Intentionally empty — we just want to exercise the thunk transition. +} + +TEST_CASE("GPR_PRESERVATION_ACROSS_HOST_CALL", "[backend]") { + auto memory = std::make_unique(); + memory->Initialize(); + + std::unique_ptr backend; +#if XE_ARCH_AMD64 + backend.reset(new xe::cpu::backend::x64::X64Backend()); +#elif XE_ARCH_ARM64 + backend.reset(new xe::cpu::backend::a64::A64Backend()); +#endif + REQUIRE(backend); + + auto processor = std::make_unique(memory.get(), nullptr); + processor->Setup(std::move(backend)); + + auto* builtin_fn = + processor->DefineBuiltin("EmptyBuiltin", EmptyBuiltin, nullptr, nullptr); + + // Load known values into r[10]-r[15] (via context load/store, which the + // register allocator maps to callee-saved GPRs), call the builtin, then + // copy them to r[3]-r[8] for verification. + auto module = std::make_unique( + processor.get(), "Test", + [](uint32_t address) { return address == 0x80000000; }, + [builtin_fn](HIRBuilder& b) { + // Load known values from context slots. + auto v0 = LoadGPR(b, 10); + auto v1 = LoadGPR(b, 11); + auto v2 = LoadGPR(b, 12); + auto v3 = LoadGPR(b, 13); + auto v4 = LoadGPR(b, 14); + auto v5 = LoadGPR(b, 15); + // Call host — this must preserve all the above. + b.CallExtern(builtin_fn); + // Store them back to different slots for verification. + StoreGPR(b, 3, v0); + StoreGPR(b, 4, v1); + StoreGPR(b, 5, v2); + StoreGPR(b, 6, v3); + StoreGPR(b, 7, v4); + StoreGPR(b, 8, v5); + b.Return(); + return true; + }, + /*skip_cf_simplification=*/true); + processor->AddModule(std::move(module)); + processor->backend()->CommitExecutableRange(0x80000000, 0x80010000); + + auto fn = processor->ResolveFunction(0x80000000); + REQUIRE(fn != nullptr); + + uint32_t stack_size = 64 * 1024; + uint32_t stack_address = memory->SystemHeapAlloc(stack_size); + auto thread_state = std::make_unique(processor.get(), 0x100, + stack_address + stack_size); + auto ctx = thread_state->context(); + ctx->lr = 0xBCBCBCBC; + + // Set known values in source registers. + ctx->r[10] = 0xAAAAAAAA00000001ULL; + ctx->r[11] = 0xBBBBBBBB00000002ULL; + ctx->r[12] = 0xCCCCCCCC00000003ULL; + ctx->r[13] = 0xDDDDDDDD00000004ULL; + ctx->r[14] = 0xEEEEEEEE00000005ULL; + ctx->r[15] = 0xFFFFFFFF00000006ULL; + + fn->Call(thread_state.get(), uint32_t(ctx->lr)); + + REQUIRE(ctx->r[3] == 0xAAAAAAAA00000001ULL); + REQUIRE(ctx->r[4] == 0xBBBBBBBB00000002ULL); + REQUIRE(ctx->r[5] == 0xCCCCCCCC00000003ULL); + REQUIRE(ctx->r[6] == 0xDDDDDDDD00000004ULL); + REQUIRE(ctx->r[7] == 0xEEEEEEEE00000005ULL); + REQUIRE(ctx->r[8] == 0xFFFFFFFF00000006ULL); + + memory->SystemHeapFree(stack_address); +} + +// ============================================================================= +// VEC register preservation across GuestToHostThunk +// ============================================================================= +// Tests that vector registers allocated by the JIT survive a host call. +// Loads vec128 values into VRs, calls a builtin, then reads them back. + +static void NeonClobberBuiltin(ppc::PPCContext* ctx, void* arg0, void* arg1) { + // This function intentionally does nothing, but the ABI allows it to + // clobber caller-saved NEON/XMM registers. The compiler might use them + // for local variables, memcpy, etc. The thunk must save/restore them. +} + +TEST_CASE("VEC_PRESERVATION_ACROSS_HOST_CALL", "[backend]") { + auto memory = std::make_unique(); + memory->Initialize(); + + std::unique_ptr backend; +#if XE_ARCH_AMD64 + backend.reset(new xe::cpu::backend::x64::X64Backend()); +#elif XE_ARCH_ARM64 + backend.reset(new xe::cpu::backend::a64::A64Backend()); +#endif + REQUIRE(backend); + + auto processor = std::make_unique(memory.get(), nullptr); + processor->Setup(std::move(backend)); + + auto* builtin_fn = processor->DefineBuiltin( + "NeonClobberBuiltin", NeonClobberBuiltin, nullptr, nullptr); + + // Load vec128 values from v[10]-v[13], call builtin, store to v[3]-v[6]. + auto module = std::make_unique( + processor.get(), "Test", + [](uint32_t address) { return address == 0x80000000; }, + [builtin_fn](HIRBuilder& b) { + auto vec0 = LoadVR(b, 10); + auto vec1 = LoadVR(b, 11); + auto vec2 = LoadVR(b, 12); + auto vec3 = LoadVR(b, 13); + // Call host — thunk must preserve VEC regs. + b.CallExtern(builtin_fn); + // Store back for verification. + StoreVR(b, 3, vec0); + StoreVR(b, 4, vec1); + StoreVR(b, 5, vec2); + StoreVR(b, 6, vec3); + b.Return(); + return true; + }, + /*skip_cf_simplification=*/true); + processor->AddModule(std::move(module)); + processor->backend()->CommitExecutableRange(0x80000000, 0x80010000); + + auto fn = processor->ResolveFunction(0x80000000); + REQUIRE(fn != nullptr); + + uint32_t stack_size = 64 * 1024; + uint32_t stack_address = memory->SystemHeapAlloc(stack_size); + auto thread_state = std::make_unique(processor.get(), 0x100, + stack_address + stack_size); + auto ctx = thread_state->context(); + ctx->lr = 0xBCBCBCBC; + + // Set known vec128 values in source VRs. + ctx->v[10] = vec128i(0x11111111, 0x22222222, 0x33333333, 0x44444444); + ctx->v[11] = vec128i(0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC, 0xDDDDDDDD); + ctx->v[12] = vec128i(0x01020304, 0x05060708, 0x090A0B0C, 0x0D0E0F10); + ctx->v[13] = vec128i(0xDEADBEEF, 0xCAFEBABE, 0x12345678, 0x9ABCDEF0); + + fn->Call(thread_state.get(), uint32_t(ctx->lr)); + + REQUIRE(ctx->v[3] == vec128i(0x11111111, 0x22222222, 0x33333333, 0x44444444)); + REQUIRE(ctx->v[4] == vec128i(0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC, 0xDDDDDDDD)); + REQUIRE(ctx->v[5] == vec128i(0x01020304, 0x05060708, 0x090A0B0C, 0x0D0E0F10)); + REQUIRE(ctx->v[6] == vec128i(0xDEADBEEF, 0xCAFEBABE, 0x12345678, 0x9ABCDEF0)); + + memory->SystemHeapFree(stack_address); +} + +// ============================================================================= +// Basic guest code execution — context load/store round-trip +// ============================================================================= +// The simplest possible test: enter guest code, read a context value, write +// it to another slot, return. Exercises HostToGuestThunk and epilog. +TEST_CASE("BASIC_GUEST_EXECUTION", "[backend]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, LoadGPR(b, 4)); + b.Return(); + }); + + test.Run( + [](PPCContext* ctx) { ctx->r[4] = 0x123456789ABCDEF0ULL; }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0x123456789ABCDEF0ULL); }); +} + +// ============================================================================= +// Multiple builtin calls in sequence +// ============================================================================= +// Exercises that the GuestToHostThunk properly restores state so that +// multiple host calls from the same guest function work correctly. + +static std::atomic multi_call_counter{0}; +static void CountingBuiltin(ppc::PPCContext* ctx, void* arg0, void* arg1) { + multi_call_counter.fetch_add(1); + ctx->r[3] = ctx->r[3] + 1; +} + +TEST_CASE("MULTIPLE_BUILTIN_CALLS", "[backend]") { + multi_call_counter = 0; + + auto memory = std::make_unique(); + memory->Initialize(); + + std::unique_ptr backend; +#if XE_ARCH_AMD64 + backend.reset(new xe::cpu::backend::x64::X64Backend()); +#elif XE_ARCH_ARM64 + backend.reset(new xe::cpu::backend::a64::A64Backend()); +#endif + REQUIRE(backend); + + auto processor = std::make_unique(memory.get(), nullptr); + processor->Setup(std::move(backend)); + + auto* builtin_fn = processor->DefineBuiltin( + "CountingBuiltin", CountingBuiltin, nullptr, nullptr); + + auto module = std::make_unique( + processor.get(), "Test", + [](uint32_t address) { return address == 0x80000000; }, + [builtin_fn](HIRBuilder& b) { + // Initialize r[3] = 0. + StoreGPR(b, 3, b.LoadConstantUint64(0)); + // Call builtin three times — each increments r[3]. + b.CallExtern(builtin_fn); + b.CallExtern(builtin_fn); + b.CallExtern(builtin_fn); + b.Return(); + return true; + }, + /*skip_cf_simplification=*/true); + processor->AddModule(std::move(module)); + processor->backend()->CommitExecutableRange(0x80000000, 0x80010000); + + auto fn = processor->ResolveFunction(0x80000000); + REQUIRE(fn != nullptr); + + uint32_t stack_size = 64 * 1024; + uint32_t stack_address = memory->SystemHeapAlloc(stack_size); + auto thread_state = std::make_unique(processor.get(), 0x100, + stack_address + stack_size); + auto ctx = thread_state->context(); + ctx->lr = 0xBCBCBCBC; + ctx->r[3] = 0; + + fn->Call(thread_state.get(), uint32_t(ctx->lr)); + + REQUIRE(multi_call_counter == 3); + REQUIRE(ctx->r[3] == 3); + + memory->SystemHeapFree(stack_address); +} diff --git a/src/xenia/cpu/testing/set_rounding_mode_test.cc b/src/xenia/cpu/testing/set_rounding_mode_test.cc index 330516b4a..69e68cc70 100644 --- a/src/xenia/cpu/testing/set_rounding_mode_test.cc +++ b/src/xenia/cpu/testing/set_rounding_mode_test.cc @@ -44,12 +44,14 @@ TEST_CASE("SET_ROUNDING_MODE_TOWARD_POS_INF", "[instr]") { // 2^-24 = 5.960464477539063e-08 ctx->f[5] = std::ldexp(1.0, -24); }, - [](PPCContext* ctx) { + [&test](PPCContext* ctx) { auto result = static_cast(ctx->f[3]); // With round-toward-positive-infinity, 1.0f + 2^-24 should round // up to the next representable float above 1.0f. float expected = std::nextafterf(1.0f, 2.0f); REQUIRE(result == expected); + // Reset to nearest so subsequent tests aren't affected. + test.processors[0]->backend()->SetGuestRoundingMode(ctx, 0); }); } @@ -69,10 +71,12 @@ TEST_CASE("SET_ROUNDING_MODE_TOWARD_ZERO", "[instr]") { ctx->f[4] = 1.0; ctx->f[5] = std::ldexp(1.0, -24); }, - [](PPCContext* ctx) { + [&test](PPCContext* ctx) { auto result = static_cast(ctx->f[3]); // With round-toward-zero, 1.0f + 2^-24 should truncate to 1.0f. REQUIRE(result == 1.0f); + // Reset to nearest so subsequent tests aren't affected. + test.processors[0]->backend()->SetGuestRoundingMode(ctx, 0); }); }