[Testing] Add some cpu backend tests

This commit is contained in:
Herman S.
2026-03-17 13:16:59 +09:00
parent 9fbcdae5dc
commit f32d52900e
4 changed files with 349 additions and 5 deletions

View File

@@ -26,7 +26,8 @@ namespace passes = xe::cpu::compiler::passes;
TestModule::TestModule(Processor* processor, const std::string_view name,
std::function<bool(uint32_t)> contains_address,
std::function<bool(hir::HIRBuilder&)> generate)
std::function<bool(hir::HIRBuilder&)> 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<passes::ControlFlowAnalysisPass>());
compiler_->AddPass(std::make_unique<passes::ControlFlowSimplificationPass>());
if (!skip_cf_simplification) {
compiler_->AddPass(
std::make_unique<passes::ControlFlowSimplificationPass>());
}
compiler_->AddPass(std::make_unique<passes::ControlFlowAnalysisPass>());
// Passes are executed in the order they are added. Multiple of the same

View File

@@ -26,7 +26,8 @@ class TestModule : public Module {
public:
TestModule(Processor* processor, const std::string_view name,
std::function<bool(uint32_t)> contains_address,
std::function<bool(hir::HIRBuilder&)> generate);
std::function<bool(hir::HIRBuilder&)> generate,
bool skip_cf_simplification = false);
~TestModule() override;
const std::string& name() const override { return name_; }

View File

@@ -11,6 +11,7 @@
#include <atomic>
#include <cmath>
#include <cstring>
#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<int> 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>();
memory->Initialize();
std::unique_ptr<xe::cpu::backend::Backend> 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<Processor>(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<TestModule>(
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<ThreadState>(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>();
memory->Initialize();
std::unique_ptr<xe::cpu::backend::Backend> 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<Processor>(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<TestModule>(
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<ThreadState>(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>();
memory->Initialize();
std::unique_ptr<xe::cpu::backend::Backend> 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<Processor>(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<TestModule>(
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<ThreadState>(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<int> 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>();
memory->Initialize();
std::unique_ptr<xe::cpu::backend::Backend> 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<Processor>(memory.get(), nullptr);
processor->Setup(std::move(backend));
auto* builtin_fn = processor->DefineBuiltin(
"CountingBuiltin", CountingBuiltin, nullptr, nullptr);
auto module = std::make_unique<TestModule>(
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<ThreadState>(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);
}

View File

@@ -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<float>(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<float>(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);
});
}